danis-race/Multiplayer_Server.py

365 lines
11 KiB
Python
Raw Permalink Normal View History

2024-07-05 18:22:38 +02:00
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()