diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index da661d1a..dea0e02d 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -12,28 +12,21 @@ #include "mxgeometry/mxgeometry3d.h" #include "realtime/realtime.h" #include "roi/legoroi.h" -#include #include #include #include +#include #include using namespace Multiplayer; -// clang-format off -static const char* g_vehicleROINames[VEHICLE_COUNT] = { - "copter", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul" -}; +static const char* g_vehicleROINames[VEHICLE_COUNT] = + {"copter", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"}; -static const char* g_rideAnimNames[VEHICLE_COUNT] = { - NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL -}; +static const char* g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL}; -static const char* g_rideVehicleROINames[VEHICLE_COUNT] = { - NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL -}; -// clang-format on +static const char* g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL}; static bool IsLargeVehicle(int8_t p_vehicleType) { @@ -43,10 +36,9 @@ static bool IsLargeVehicle(int8_t p_vehicleType) RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId) : m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f), m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()), - m_hasReceivedUpdate(false), m_walkAnim(nullptr), m_walkRoiMap(nullptr), m_walkRoiMapSize(0), - m_animTime(0.0f), m_idleTime(0.0f), m_wasMoving(false), m_idleAnim(nullptr), m_idleRoiMap(nullptr), - m_idleRoiMapSize(0), m_idleAnimTime(0.0f), m_rideAnim(nullptr), m_rideRoiMap(nullptr), - m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), + m_hasReceivedUpdate(false), m_walkAnim(nullptr), m_walkRoiMap(nullptr), m_walkRoiMapSize(0), m_animTime(0.0f), + m_idleTime(0.0f), m_wasMoving(false), m_idleAnim(nullptr), m_idleRoiMap(nullptr), m_idleRoiMapSize(0), + m_idleAnimTime(0.0f), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE) { SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", LegoActor::GetActorName(p_actorId), p_peerId); @@ -412,7 +404,12 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime) MxMatrix transform(m_roi->GetLocal2World()); LegoTreeNode* root = m_idleAnim->GetRoot(); for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { - LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) timeInCycle, m_idleRoiMap); + LegoROI::ApplyAnimationTransformation( + root->GetChild(i), + transform, + (LegoTime) timeInCycle, + m_idleRoiMap + ); } } } diff --git a/extensions/src/multiplayer/server/.gitignore b/extensions/src/multiplayer/server/.gitignore new file mode 100644 index 00000000..41a25789 --- /dev/null +++ b/extensions/src/multiplayer/server/.gitignore @@ -0,0 +1,2 @@ +.wrangler/ +node_modules/ diff --git a/extensions/src/multiplayer/server/package-lock.json b/extensions/src/multiplayer/server/package-lock.json new file mode 100644 index 00000000..29dae7f4 --- /dev/null +++ b/extensions/src/multiplayer/server/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "ws": "^8.19.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/extensions/src/multiplayer/server/package.json b/extensions/src/multiplayer/server/package.json new file mode 100644 index 00000000..4414d1d5 --- /dev/null +++ b/extensions/src/multiplayer/server/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "ws": "^8.19.0" + } +} diff --git a/extensions/src/multiplayer/server/relay-local.mjs b/extensions/src/multiplayer/server/relay-local.mjs new file mode 100644 index 00000000..5c20b8e4 --- /dev/null +++ b/extensions/src/multiplayer/server/relay-local.mjs @@ -0,0 +1,104 @@ +import { createServer } from "http"; +import { WebSocketServer } from "ws"; + +const PORT = process.env.PORT || 8787; +const rooms = new Map(); + +function getRoom(roomId) { + if (!rooms.has(roomId)) { + rooms.set(roomId, { connections: new Map(), nextPeerId: 1 }); + } + return rooms.get(roomId); +} + +const server = createServer((req, res) => { + if (req.url === "/" || req.url === "/health") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok" })); + } else { + res.writeHead(404); + res.end("Not Found"); + } +}); + +const wss = new WebSocketServer({ server }); + +wss.on("connection", (ws, req) => { + const pathParts = (req.url || "").split("/").filter(Boolean); + if (pathParts.length !== 2 || pathParts[0] !== "room") { + console.log(`[REJECT] Invalid path: ${req.url}`); + ws.close(1008, "Invalid path"); + return; + } + + const roomId = pathParts[1]; + const room = getRoom(roomId); + const peerId = room.nextPeerId++; + const peerIdStr = String(peerId); + room.connections.set(peerIdStr, ws); + console.log(`[CONNECT] Peer ${peerId} joined room "${roomId}" (${room.connections.size} peers)`); + + // Send the peer its assigned ID as the first message + const idMsg = Buffer.alloc(5); + idMsg.writeUInt8(0xff, 0); + idMsg.writeUInt32LE(peerId, 1); + ws.send(idMsg); + + ws.on("message", (data) => { + if (!(data instanceof Buffer) || data.length < 9) { + return; + } + + const msgType = data.readUInt8(0); + console.log(`[MSG] Peer ${peerId} sent type=${msgType} len=${data.length}`); + + // Stamp the peerId into the message header (bytes 1-4) + const stamped = Buffer.from(data); + stamped.writeUInt32LE(peerId, 1); + + // Broadcast to all other peers in this room + let sent = 0; + for (const [id, peer] of room.connections) { + if (id !== peerIdStr) { + try { + peer.send(stamped); + sent++; + } catch { + room.connections.delete(id); + } + } + } + console.log(`[RELAY] Forwarded to ${sent} peers`); + }); + + const onClose = () => { + console.log(`[DISCONNECT] Peer ${peerId} left room "${roomId}" (${room.connections.size - 1} peers remaining)`); + room.connections.delete(peerIdStr); + + // Broadcast LEAVE message to remaining peers + const leaveMsg = Buffer.alloc(9); + leaveMsg.writeUInt8(2, 0); // MSG_LEAVE + leaveMsg.writeUInt32LE(peerId, 1); + leaveMsg.writeUInt32LE(0, 5); // sequence 0 + + for (const [, peer] of room.connections) { + try { + peer.send(leaveMsg); + } catch { + // Ignore send errors on cleanup + } + } + + // Clean up empty rooms + if (room.connections.size === 0) { + rooms.delete(pathParts[1]); + } + }; + + ws.on("close", onClose); + ws.on("error", onClose); +}); + +server.listen(PORT, () => { + console.log(`Relay server listening on http://localhost:${PORT}`); +}); diff --git a/extensions/src/multiplayer/server/wrangler.toml b/extensions/src/multiplayer/server/wrangler.toml new file mode 100644 index 00000000..f9be0229 --- /dev/null +++ b/extensions/src/multiplayer/server/wrangler.toml @@ -0,0 +1,12 @@ +name = "isle-relay" +main = "relay.ts" +compatibility_date = "2024-01-01" + +[durable_objects] +bindings = [ + { name = "GAME_ROOM", class_name = "GameRoom" } +] + +[[migrations]] +tag = "v1" +new_sqlite_classes = ["GameRoom"]