# GNU GPL v3 or later # ( C ) J.Y.Amihud ( blenderdumbass ) 2024 import os import sys import zlib import json import time import threading import subprocess import Settings from Common import * from Multiplayer_Shared import * from http.server import BaseHTTPRequestHandler, HTTPServer class handler(BaseHTTPRequestHandler): def do_POST(self): data = self.Recieve() response = ParseRequest(data) self.Send(response) 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): # I don't want any logs. return 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) # Timing data resp["timing"] = Timing(data) resp["serverTime"] = time.time() 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 Timing(data): users = LoadData("users", {}) room = data.get('login', {}).get("room", "") resp = {} for userId in users: user = users[userId] if user.get("room") == room: resp[userId] = {} resp[userId]["ping"] = user.get("ping") resp[userId]["timestamp"] = user.get("timestamp") return resp def Login(data): resp = {} newuser = False # Checking userId if not data.get("userId"): resp["userId"] = RandomString() # 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() # Storring users data. users = LoadData("users", {}) if data.get("userId") and data.get("userId") not in users: newuser = True if data.get("userId"): users[data["userId"]] = data SaveData("users", users) if newuser: userId = data.get("userId") name = data.get("username") room = Safe(data.get("room")) NotifyOthers(userId, name+" joined game.", room) print(Format(userId), "JOINED GAME!") return resp def ChangeOwnership(data): print(Format(data.get("userId")), "GRABBED", Format(data.get("netId"))) 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: print(Format(data.get("netId")),"netId:",data.get("netId"), "NOT FOUND, WHILE CHANGING OWNERSHIP!") 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, "name":obj.get("name"), "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) 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 stripts 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 Format(netId, room=True): objects = LoadData("objects",{}) users = LoadData("users",{}) if netId in users: name = users[netId].get("username", "Unknown") room = users[netId].get("room", "Unknown Room") ownerId = netId owner = "" else: name = objects.get(netId, {}).get("name", "Unknown Object") room = objects.get(netId, {}).get("room", "Unknown Room") ownerId = objects.get(netId, {}).get("ownerId") owner = users.get(ownerId, {}).get("username", "Unknown") string = IDcolor(room)+" "+room+" "+IDcolor(netId)+" "+netId[-4:]+" "+name if owner and ownerId: string = string + " " + IDcolor(ownerId) + " by "+ownerId[-4:]+" " + owner string = string + " " + clr["norm"] return string ###### RUNNING THE SERVER ##### def ArgumentsHandler(): port = 6969 ipv6 = "-4" if "--help" in sys.argv or "-h" in sys.argv: print(""" This is the Multiplayer server for Dani's Race. It is technically an HTTP server that handles POST requests send by the game. Those requests are in a JSON format compressed with python's ZLIB library. There is no webpage, so trying to access it with a browser will probably spit out an error. """+clr["bold"]+clr["tdyl"]+"""RUN THIS SERVER FROM THE "Scripts" FOLDER!"""+clr["norm"]+""" --help , -h : This Help Text. --port """+clr["tdyl"]+""""""+clr["norm"]+""" : Sets up a port on which the server will listen. --ipv6 , -6 : Use IPv6 connection for global network. """) exit() if "--port" in sys.argv: try: port = int(sys.argv[ sys.argv.index("--port")+1 ]) except: print("Didn't specify port number.\nExample ( for port 8080 ): $ python3 Multiplayer_Server.py --port 8080") exit() if "--ipv6" in sys.argv or "-6" in sys.argv: ipv6 = "-6" return port, ipv6 PORT, IPV6 = ArgumentsHandler() print(clr["bold"]+"Dani's Race Multiplayer Server!"+clr["norm"]) ###### CLEANUPS ##### def CleanUps(): while True: time.sleep(3) # 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, he is gone. if user.get("timestamp", 0) < time.time() -10: NotifyOthers(userId, name+" left the game.", room) print(Format(userId), "LEFT GAME!") delete.append(userId) for i in delete: del users[i] SaveData("users", users) 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) cleanups = threading.Thread(target=CleanUps) cleanups.daemon = True cleanups.start() print("Started cleanups thread.") print(clr["bold"]+"Starting server:", clr["norm"]) try: IP = subprocess.check_output(["hostname", "-I"]).decode("utf-8").split(" ")[0] except: try: # Some versions of hostname just have the -i option that acts like -I in # other versions. IP = subprocess.check_output(["hostname", "-i"]).decode("utf-8").split(" ")[0] except: IP = clr["tdrd"]+"127.0.0.1"+clr["norm"] if not IP or IP == "\n": IP = clr["tdrd"]+"127.0.0.1"+clr["norm"] print(" Local IP:", clr["bold"], IP, clr["norm"]) try: GLOBALIP = subprocess.check_output(["curl", "-s", IPV6, "ifconfig.co"]).decode("utf-8")[:-1] print(" Global IP:", clr["bold"], GLOBALIP, clr["norm"]) except: GLOBALIP = None print(" Global IP:",clr["tdrd"]+clr["bold"], "No connection to Global Network.", clr["norm"]) print(" Port:", clr["bold"], PORT, clr["norm"]) print(" Local Hostname:", clr["bold"], "http://"+IP+":"+str(PORT), clr["norm"]) if GLOBALIP: if IPV6 == "-4": print(" Global Hostname:", clr["bold"], "http://"+GLOBALIP+":"+str(PORT), clr["norm"]) else: print(" Global Hostname:", clr["bold"], "http://["+GLOBALIP+"]:"+str(PORT), clr["norm"]) print() serve = HTTPServer(("", PORT), handler) try: serve.serve_forever() except KeyboardInterrupt: print() print("Server Exited!")