Add local networking and test server
Introduce a localhost-only networking milestone and a tiny Python test server to verify packet flow from the plugin to an external process. Added F4T::Networking API (include and src) which uses non-blocking Winsock to connect to 127.0.0.1:7777, send JSON newline-separated transform packets, and handle reconnect/backoff and disconnects safely. Plugin changes: call ConnectToLocalServer() at load, register DisconnectFromLocalServer on exit, and send transform packets from main.cpp when logging player movement. Build: xmake updated to include new headers and link ws2_32. Server: server/server.py implements a simple multi-client TCP server that prints parsed transform packets; server/README.md and docs/dev-log.md updated to document the test setup and results. All networking failures are non-fatal so the game continues if the server isn't running.
This commit is contained in:
+19
-3
@@ -2,19 +2,35 @@
|
||||
|
||||
This folder is for the external multiplayer test server.
|
||||
|
||||
The server is responsible for managing connected clients and broadcasting player state.
|
||||
The current server is a tiny local-only TCP test server. It listens on
|
||||
`127.0.0.1:7777`, accepts one or more clients, reads newline-separated JSON
|
||||
packets, and prints received player transform packets.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
cd server
|
||||
python server.py
|
||||
```
|
||||
|
||||
Then launch Fallout 4 through F4SE and move the player. The plugin should keep
|
||||
logging player position changes to `Fallout4Together.log`, and this server
|
||||
should print matching `transform` packets.
|
||||
|
||||
If the server is not running, the plugin should log a warning and Fallout 4
|
||||
should continue launching normally.
|
||||
|
||||
## First Server Goals
|
||||
|
||||
- Start local server
|
||||
- Accept client connections
|
||||
- Assign player IDs
|
||||
- Receive transform packets
|
||||
- Broadcast transform packets
|
||||
- Handle disconnects
|
||||
|
||||
## Not Planned Yet
|
||||
|
||||
- Remote actor spawning
|
||||
- Gameplay synchronization
|
||||
- Authentication
|
||||
- Public matchmaking
|
||||
- Anti-cheat
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import socket
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 7777
|
||||
ACCEPT_TIMEOUT_SECONDS = 0.5
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
print(message, flush=True)
|
||||
|
||||
|
||||
def handle_client(connection: socket.socket, address: tuple[str, int]) -> None:
|
||||
client = f"{address[0]}:{address[1]}"
|
||||
log(f"Client connected: {client}")
|
||||
|
||||
with connection:
|
||||
buffer = ""
|
||||
|
||||
try:
|
||||
while True:
|
||||
chunk = connection.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
buffer += chunk.decode("utf-8", errors="replace")
|
||||
while "\n" in buffer:
|
||||
line, buffer = buffer.split("\n", 1)
|
||||
handle_line(client, line.strip())
|
||||
except ConnectionResetError:
|
||||
log(f"Client disconnected unexpectedly: {client}")
|
||||
finally:
|
||||
log(f"Client disconnected: {client}")
|
||||
|
||||
|
||||
def handle_line(client: str, line: str) -> None:
|
||||
if not line:
|
||||
return
|
||||
|
||||
try:
|
||||
packet = json.loads(line)
|
||||
except json.JSONDecodeError as error:
|
||||
log(f"Invalid JSON from {client}: {error}: {line}")
|
||||
return
|
||||
|
||||
print_packet(client, packet)
|
||||
|
||||
|
||||
def print_packet(client: str, packet: dict[str, Any]) -> None:
|
||||
if packet.get("type") != "transform":
|
||||
log(f"Packet from {client}: {packet}")
|
||||
return
|
||||
|
||||
try:
|
||||
x = float(packet["x"])
|
||||
y = float(packet["y"])
|
||||
z = float(packet["z"])
|
||||
angle_z = float(packet["angleZ"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
log(f"Malformed transform packet from {client}: {packet}")
|
||||
return
|
||||
|
||||
log(
|
||||
"Transform from "
|
||||
f"{client}: x={x:.2f}, y={y:.2f}, z={z:.2f}, angleZ={angle_z:.2f}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
|
||||
if hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
|
||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
|
||||
server_socket.bind((HOST, PORT))
|
||||
server_socket.listen()
|
||||
server_socket.settimeout(ACCEPT_TIMEOUT_SECONDS)
|
||||
|
||||
log(f"Fallout 4 Together local test server listening on {HOST}:{PORT}")
|
||||
log("Waiting for newline-separated JSON transform packets...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
connection, address = server_socket.accept()
|
||||
except socket.timeout:
|
||||
# Wake periodically so Ctrl+C is handled promptly in Windows terminals.
|
||||
continue
|
||||
|
||||
thread = threading.Thread(target=handle_client, args=(connection, address), daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
log("\nServer stopped.")
|
||||
Reference in New Issue
Block a user