DanisRace/Scripts/Multiplayer_Server.py
Victorious Children Studios db92590087 Version 08-08-24
2024-08-27 15:34:26 +03:00

482 lines
13 KiB
Python

# 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 *
RAMDATA = {}
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
if "--useram" in sys.argv:
RAMDATA[folder+str(name)] = data
return
# 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)+"/"
if "--useram" in sys.argv:
return RAMDATA[folder+str(name)]
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"]+"""<number>"""+clr["norm"]+""" : Sets up a port on which the
server will listen.
--ipv6 , -6 : Use IPv6 connection for global network.
--useram : Use the RAM memory to write the data to, instead of disk.
( Faster, but if server crashes, the state is lost ).
""")
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!")