danis-race/Multiplayer_Server.py
2024-07-05 18:22:38 +02:00

365 lines
11 KiB
Python

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