150 lines
6.0 KiB
Python
150 lines
6.0 KiB
Python
|
import socket
|
|||
|
import threading
|
|||
|
import json
|
|||
|
import time
|
|||
|
import uuid
|
|||
|
|
|||
|
HOST = '127.0.0.1'
|
|||
|
PORT = 65432
|
|||
|
|
|||
|
# Global dictionaries for event subscriptions, client IDs, and game state.
|
|||
|
event_subscriptions = {} # event_name -> list of client sockets
|
|||
|
client_ids = {} # conn -> client_id
|
|||
|
client_states = {} # conn -> {"client_id", "username", "x", "y", "keys"}
|
|||
|
lock = threading.Lock()
|
|||
|
|
|||
|
MOVE_SPEED = 200 # pixels per second
|
|||
|
|
|||
|
def handle_client(conn, addr):
|
|||
|
client_id = str(uuid.uuid4())
|
|||
|
with lock:
|
|||
|
client_ids[conn] = client_id
|
|||
|
# Initialize state: default username "Guest", starting position, and an empty set of pressed keys.
|
|||
|
client_states[conn] = {"client_id": client_id, "username": "Guest", "x": 100, "y": 100, "keys": set()}
|
|||
|
print(f"[NEW CONNECTION] {addr} connected as {client_id}")
|
|||
|
|
|||
|
# Send a dedicated "self_id" event so the client knows its unique id.
|
|||
|
try:
|
|||
|
msg = json.dumps({'event': 'self_id', 'payload': {'client_id': client_id}}) + "\n"
|
|||
|
conn.sendall(msg.encode())
|
|||
|
except Exception as e:
|
|||
|
print(f"[ERROR] sending self_id: {e}")
|
|||
|
|
|||
|
# Broadcast that a new client connected.
|
|||
|
send_event("client_connect", {"client_id": client_id})
|
|||
|
|
|||
|
try:
|
|||
|
file_obj = conn.makefile('r')
|
|||
|
for line in file_obj:
|
|||
|
line = line.strip()
|
|||
|
if not line:
|
|||
|
continue
|
|||
|
try:
|
|||
|
msg = json.loads(line)
|
|||
|
msg_type = msg.get('type')
|
|||
|
if msg_type == 'register_event':
|
|||
|
event_name = msg.get('event')
|
|||
|
with lock:
|
|||
|
event_subscriptions.setdefault(event_name, []).append(conn)
|
|||
|
print(f"[REGISTER] {client_id} subscribed to '{event_name}'")
|
|||
|
elif msg_type == 'set_username':
|
|||
|
username = msg.get('username', 'Guest')
|
|||
|
with lock:
|
|||
|
if conn in client_states:
|
|||
|
client_states[conn]['username'] = username
|
|||
|
print(f"[USERNAME] {client_id} set username to '{username}'")
|
|||
|
elif msg_type == 'keydown':
|
|||
|
key = msg.get('key')
|
|||
|
with lock:
|
|||
|
if conn in client_states:
|
|||
|
client_states[conn]['keys'].add(key)
|
|||
|
print(f"[KEYDOWN] {client_id} key '{key}' pressed")
|
|||
|
elif msg_type == 'keyup':
|
|||
|
key = msg.get('key')
|
|||
|
with lock:
|
|||
|
if conn in client_states and key in client_states[conn]['keys']:
|
|||
|
client_states[conn]['keys'].remove(key)
|
|||
|
print(f"[KEYUP] {client_id} key '{key}' released")
|
|||
|
elif msg_type == 'ping':
|
|||
|
# Respond immediately with a pong, echoing the timestamp.
|
|||
|
timestamp = msg.get('timestamp')
|
|||
|
response = json.dumps({'event': 'pong', 'payload': {'timestamp': timestamp}}) + "\n"
|
|||
|
conn.sendall(response.encode())
|
|||
|
# Additional message types can be handled here.
|
|||
|
except json.JSONDecodeError:
|
|||
|
print(f"[ERROR] Invalid JSON from {client_id}: {line}")
|
|||
|
except Exception as e:
|
|||
|
print(f"[ERROR] Exception with {client_id}: {e}")
|
|||
|
finally:
|
|||
|
with lock:
|
|||
|
for subs in event_subscriptions.values():
|
|||
|
if conn in subs:
|
|||
|
subs.remove(conn)
|
|||
|
client_ids.pop(conn, None)
|
|||
|
client_states.pop(conn, None)
|
|||
|
print(f"[DISCONNECT] {client_id} disconnected.")
|
|||
|
send_event("client_disconnect", {"client_id": client_id})
|
|||
|
conn.close()
|
|||
|
|
|||
|
def send_event(event_name, payload):
|
|||
|
"""
|
|||
|
Broadcast an event to all clients subscribed to the given event.
|
|||
|
"""
|
|||
|
message = json.dumps({'event': event_name, 'payload': payload}) + "\n"
|
|||
|
with lock:
|
|||
|
receivers = event_subscriptions.get(event_name, [])
|
|||
|
for client in receivers[:]: # iterate over a copy to allow removals
|
|||
|
try:
|
|||
|
client.sendall(message.encode())
|
|||
|
except Exception as e:
|
|||
|
print(f"[ERROR] Failed to send to client, removing: {e}")
|
|||
|
receivers.remove(client)
|
|||
|
|
|||
|
def game_loop():
|
|||
|
"""
|
|||
|
Main game loop: update each client’s position based on currently pressed keys,
|
|||
|
and broadcast the overall game state (including server time) to all clients.
|
|||
|
"""
|
|||
|
dt = 0.1 #100 ms;
|
|||
|
|
|||
|
while True:
|
|||
|
time.sleep(dt)
|
|||
|
with lock:
|
|||
|
# Update each client's position.
|
|||
|
for state in client_states.values():
|
|||
|
dx, dy = 0, 0
|
|||
|
if 'left' in state['keys']:
|
|||
|
dx -= MOVE_SPEED * dt
|
|||
|
if 'right' in state['keys']:
|
|||
|
dx += MOVE_SPEED * dt
|
|||
|
if 'up' in state['keys']:
|
|||
|
dy -= MOVE_SPEED * dt
|
|||
|
if 'down' in state['keys']:
|
|||
|
dy += MOVE_SPEED * dt
|
|||
|
state['x'] += dx
|
|||
|
state['y'] += dy
|
|||
|
# Build payload: include current server time and each client’s state.
|
|||
|
players_payload = {}
|
|||
|
for state in client_states.values():
|
|||
|
players_payload[state['client_id']] = {
|
|||
|
"username": state['username'],
|
|||
|
"x": state['x'],
|
|||
|
"y": state['y']
|
|||
|
}
|
|||
|
payload = {"server_time": time.time(), "players": players_payload}
|
|||
|
send_event("state_update", payload)
|
|||
|
|
|||
|
def start_server():
|
|||
|
print("[STARTING] Server is starting...")
|
|||
|
threading.Thread(target=game_loop, daemon=True).start()
|
|||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|||
|
s.bind((HOST, PORT))
|
|||
|
s.listen()
|
|||
|
print(f"[LISTENING] Server is listening on {HOST}:{PORT}")
|
|||
|
while True:
|
|||
|
conn, addr = s.accept()
|
|||
|
threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
start_server()
|