482 lines
13 KiB
Python
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!")
|