diff --git a/Multiplayer_Server.py b/Multiplayer_Server.py new file mode 100644 index 0000000..706ea14 --- /dev/null +++ b/Multiplayer_Server.py @@ -0,0 +1,365 @@ +import os +import zlib +import json +import time +import threading +import shutil +from pathlib import Path +from http.server import BaseHTTPRequestHandler, HTTPServer +from collections import deque + +import Settings +from Multiplayer_Shared import * + +MAX_HISTORY_LENGTH = 100 # Store last 100 data points + +class GameServerHandler(BaseHTTPRequestHandler): + player_count_history = deque(maxlen=MAX_HISTORY_LENGTH) + + def do_POST(self): + data = self.Recieve() + response = ParseRequest(data) + self.Send(response) + + def do_GET(self): + if self.path == '/health': + self.send_response(200) + self.send_header('Content-type', 'text/plain') + self.end_headers() + self.wfile.write(b"OK") + else: + self.send_error(404) + + def Send(self, data): + # Compressing + data = json.dumps(data) + data = data.encode("utf-8") + data = zlib.compress(data) + + # Sending + self.send_response(200) + self.send_header("Content-type","application/zip") + self.end_headers() + self.wfile.write(data) + + def Recieve(self): + length = int(self.headers.get('content-length')) + data = self.rfile.read(length) + data = zlib.decompress(data) + data = json.loads(data) + return data + + def log_message(self, format, *args): + log_entry = f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {format%args}" + log_entries = LoadData("server_log", []) + log_entries.append(log_entry) + if len(log_entries) > 100: # Keep only the last 100 entries + log_entries = log_entries[-100:] + SaveData("server_log", log_entries) + +def ParseRequest(data): + resp = {} + + for key in data: + payload = data[key] + + if "login" == key: + resp[key] = Login(payload) + elif "change-ownership" == key: + resp[key] = ChangeOwnership(payload) + elif "scene" == key: + resp[key] = Scene(data) + elif "vision" == key: + pass + else: + print(key, payload) + + userId = data.get("login", {}).get("userId") + room = Safe(data.get("login", {}).get("room")) + messages = LoadData("messages", {}, room) + if messages.get(userId): + resp["message"] = messages[userId].pop(0) + SaveData("messages", messages, room) + + return resp + +def Login(data): + resp = {} + newuser = False + + # Checking userId + if not data.get("userId"): + resp["userId"] = RandomString() + newuser = True + + # Checking room + if not data.get("room"): + resp["room"] = "MainRoom" + elif data.get("room") != Safe(data.get("room", "")): + resp["room"] = Safe(data["room"]) + + data["timestamp"] = time.time() + + # Storing users data. + users = LoadData("users", {}) + + if data.get("userId"): + users[data["userId"]] = data + + SaveData("users", users) + + if newuser: + userId = resp.get("userId") + name = data.get("username") + room = Safe(data.get("room")) + NotifyOthers(userId, f"{name} joined game.", room) + log_user_joined(name, room) + + return resp + +def ChangeOwnership(data): + objects = LoadData("objects", {}) + if data.get("netId") in objects: + obj = objects[data.get("netId")] + obj["ownerId"] = data.get("userId") + + SaveData("objects", objects) + + # Changing ownership in chunk + addr = obj.get("chunk") + room = Safe(obj.get("room")) + chunk = LoadData(addr, {}, room) + for o in chunk: + if o.get("netId") == data.get("netId"): + o["ownerId"] = data.get("userId") + SaveData(addr, chunk, room) + + return True + else: + return False + +def Scene(data): + scene = data["scene"] + userId = data.get("login", {}).get("userId") + room = Safe(data.get("login", {}).get("room", "MainRoom")) + + # We are not updating scene if there is no userId yet. + if not userId: + return + + # Reading scene payload. + resp = {} + objects = LoadData("objects", {}) + + for addr in scene: + chunk = scene[addr] + + for obj in chunk: + # Some people might want to cheat by inputing + # values for other users. + if obj.get("netId"): + obj["ownerId"] = objects.get(obj.get("netId"), {}).get("ownerId") + if not obj["ownerId"]: + obj["ownerId"] = userId + + if obj.get("ownerId") and obj.get("ownerId") != userId: + chunk.remove(obj) + + # Saving chunks data. + for addr in scene: + chunk = scene[addr] + saved = LoadData(addr, [], room) + add = [] + + # Cleaning up + for obj in saved: + if obj.get("netId") not in objects or objects[obj.get("netId")].get("chunk") != addr: + saved.remove(obj) + + ids = [] + for obj in saved: + ids.append(obj.get("netId")) + + for obj in chunk: + # If object is Dani and it has no netId + # we assume that somebody new joined the + # room. + if obj.get("name") == "Dani_Box" and not obj.get("netId"): + obj["netId"] = userId + obj["ownerId"] = userId + add.append(obj) + elif not obj.get("netId"): + obj["netId"] = RandomString() + obj["ownerId"] = userId + add.append(obj) + + if obj.get("netId") not in ids: + saved.append(obj) + else: + saved[ids.index(obj.get("netId"))] = obj + + objects[obj["netId"]] = {"chunk":addr, + "timestamp":time.time(), + "ownerId":obj.get("ownerId"), + "room":room} + + SaveData(addr, saved, room) + + if addr in data.get("vision", []): + resp[addr] = [] + for obj in saved: + if obj.get("ownerId") != userId: + resp[addr].append(obj) + for obj in add: + resp[addr].append(obj) + + SaveData("objects", objects) + + return resp + +def Notify(userId, message, room): + messages = LoadData("messages", {}, room) + if userId not in messages: + messages[userId] = [] + messages[userId].append(message) + SaveData("messages", messages, room) + +def NotifyOthers(userId, message, room): + users = LoadData("users", {}) + for user in users: + if user != userId and users[user].get("room") == room: + Notify(user, message, room) + + print(f"Room: {room} : {message}") + +def SaveData(name, data, room=None): + # Making folder for server stuff + folder = Settings.get_settings_folder()+"/server/" + if room: folder = folder+str(room)+"/" + try: os.makedirs(folder) + except: pass + + # Saving the json + with open(folder+str(name)+".json", "w") as save: + json.dump(data, save, indent=4) + +def LoadData(name, otherwise=None, room=None): + try: + folder = Settings.get_settings_folder()+"/server/" + if room: folder = folder+str(room)+"/" + with open(folder+str(name)+".json") as o: + return json.load(o) + except: return otherwise + +def Safe(string): + # This function strips strings from any unsafe + # characters. + good = "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_" + new = "" + for i in string: + if i in good: new = new + i + else: new = new + "_" + + return new + +def log_user_joined(name, room): + log_entry = f"User '{name}' joined room '{room}'." + log_entries = LoadData("server_log", []) + log_entries.append(log_entry) + if len(log_entries) > 100: + log_entries = log_entries[-100:] + SaveData("server_log", log_entries) + +def log_user_left(name, room): + log_entry = f"User '{name}' left room '{room}'." + log_entries = LoadData("server_log", []) + log_entries.append(log_entry) + if len(log_entries) > 100: + log_entries = log_entries[-100:] + SaveData("server_log", log_entries) + +def cleanup_rooms(): + base_dir = Path(Settings.get_settings_folder()) / "server" + users = LoadData("users", {}) + + for item in base_dir.iterdir(): + if item.is_dir(): + room_name = item.name + # Check if any user is in this room + users_in_room = [user for user in users.values() if user.get("room") == room_name] + if not users_in_room: + print(f"Cleaning up room: {room_name}") + shutil.rmtree(item) + log_cleanup(room_name) + +def log_cleanup(room_name): + log_entry = f"Room '{room_name}' has been cleaned up due to inactivity." + log_entries = LoadData("server_log", []) + log_entries.append(log_entry) + if len(log_entries) > 100: + log_entries = log_entries[-100:] + SaveData("server_log", log_entries) + +def CleanUps(): + while True: + time.sleep(30) # Run every 30 seconds + + # Cleaning up users + users = LoadData("users", {}) + delete = [] + for userId in users: + user = users[userId] + room = user.get("room") + name = user.get("username") + + # If user missing for 10 seconds, they are gone. + if user.get("timestamp", 0) < time.time() - 10: + NotifyOthers(userId, f"{name} left the game.", room) + delete.append(userId) + log_user_left(name, room) + + for i in delete: + del users[i] + + SaveData("users", users) + + # Cleaning up objects + objects = LoadData("objects", {}) + delete = [] + for netId in objects: + obj = objects[netId] + + # Deleting objects after 5 missing seconds + if obj.get("timestamp", 0) < time.time() - 5: + delete.append(netId) + + for i in delete: + del objects[i] + + SaveData("objects", objects) + + # Cleaning up empty rooms + cleanup_rooms() + +def health_check_thread(): + health_port = 6971 + health_server = HTTPServer(("", health_port), GameServerHandler) + print(f"Health check server running on port {health_port}") + health_server.serve_forever() + +if __name__ == "__main__": + PORT = 6969 + serve = HTTPServer(("", PORT), GameServerHandler) + print(f"Game server running on port {PORT}") + + # Start the cleanup thread + cleanups = threading.Thread(target=CleanUps) + cleanups.setDaemon(True) + cleanups.start() + + # Start the health check thread + health_thread = threading.Thread(target=health_check_thread) + health_thread.setDaemon(True) + health_thread.start() + + serve.serve_forever() \ No newline at end of file diff --git a/web-ui.py b/web-ui.py new file mode 100644 index 0000000..cd81e13 --- /dev/null +++ b/web-ui.py @@ -0,0 +1,398 @@ +import json +import time +import requests +from http.server import BaseHTTPRequestHandler, HTTPServer +from collections import deque + +import Settings +from Multiplayer_Server import LoadData, SaveData + +MAX_HISTORY_LENGTH = 100 # Store last 100 data points + +class WebUIHandler(BaseHTTPRequestHandler): + player_count_history = deque(maxlen=MAX_HISTORY_LENGTH) + + def do_GET(self): + if self.path == '/': + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(self.generate_html().encode()) + elif self.path == '/data': + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(self.get_server_data()).encode()) + else: + self.send_error(404) + + def generate_html(self): + html = """ + + + + + + Dani's Race - Retro Dashboard + + + + +
+

Dani's Race Dashboard

+
+ Connect to the game server at: https://dani.madiator.com:6969 +
+
Server Status: Checking...
+
+
+
+

Active Users

+
-
+
+
+

Total Objects

+
-
+
+
+

Total Rooms

+
-
+
+
+

Server Uptime

+
-
+
+
+

Player Count History

+ +

Active Rooms:

+
+

Server Log:

+
+
+ + + + """ + return html + + def get_server_data(self): + try: + # Try to get health status from the game server + health_response = requests.get("http://localhost:6971/health", timeout=5) + server_status = "Online" if health_response.status_code == 200 else "Offline" + except requests.RequestException: + server_status = "Offline" + + users = LoadData("users", {}) + objects = LoadData("objects", {}) + rooms = {} + for user_id, user_data in users.items(): + room = user_data.get("room", "Unknown") + if room not in rooms: + rooms[room] = [] + rooms[room].append(user_data.get("username", f"User-{user_id[:5]}")) + + # Get the last 20 log entries + log_entries = LoadData("server_log", [])[-20:] + + # Update player count history + self.player_count_history.append(len(users)) + + return { + "server_status": server_status, + "active_users": len(users), + "total_objects": len(objects), + "rooms": rooms, + "log": log_entries, + "player_count_history": list(self.player_count_history), + "server_timestamp": time.time() + } + +if __name__ == "__main__": + PORT = 6970 + serve = HTTPServer(("", PORT), WebUIHandler) + print(f"Web UI server running on port {PORT}") + serve.serve_forever()