365 lines
11 KiB
Python
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()
|