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()