From e95dc82c97fab54f3dc54b263cfeec43e777d16e Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Sun, 26 Apr 2026 19:06:05 -0400 Subject: [PATCH 01/14] fix: restore Solaris travel fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/world/world-scene.ts | 6 ++++-- world-map.json | 30 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/world/world-scene.ts b/src/world/world-scene.ts index e0e8067..73496be 100644 --- a/src/world/world-scene.ts +++ b/src/world/world-scene.ts @@ -61,6 +61,7 @@ import { COMSTAR_ACCESS_ACTION_TYPE, FALLBACK_MECH_ID, SOLARIS_TRAVEL_CONTEXT_ID, + SOLARIS_TRAVEL_ACTION_TYPE, GLOBAL_COMSTAR_MENU_ITEMS, worldMapByRoomId, WORLD_MECH_BY_ID, @@ -461,8 +462,8 @@ export function buildSceneInitForSession(session: ClientSession) { // Room-type-aware action buttons. // actionType 4 is reserved for the fixed lower-left world icon path. - // Solaris map access now hangs off tram location icons instead of a top-row - // Travel button, so only keep room-local actions here. + // Solaris map access primarily hangs off tram location icons, but keep a + // top-row Travel fallback so players can recover if they get stuck. // actionType 5 → "Fight" (enter combat; handled by cmd-5 dispatch in server-world.ts). // actionType 6 → "Mech"/"Mech Bay" (opens the 3-step mech picker). // The client hard-codes button 0x100 as a local-only Help slot and only @@ -475,6 +476,7 @@ export function buildSceneInitForSession(session: ClientSession) { const readyRoomLabel = isArena ? getArenaReadyRoomLabelForSession(session) : undefined; const arenaOptions: Array<{ type: number; label: string }> = [ { type: 0, label: 'Help' }, + { type: SOLARIS_TRAVEL_ACTION_TYPE, label: 'Travel' }, ]; if (isArena) { arenaOptions.push({ type: 6, label: 'Mech' }); diff --git a/world-map.json b/world-map.json index d77800c..8503e74 100644 --- a/world-map.json +++ b/world-map.json @@ -903,7 +903,7 @@ "icon": 1, "exits": { "north": null, - "south": null, + "south": 2141, "west": null, "east": null }, @@ -1341,18 +1341,18 @@ }, { "roomId": 2141, - "type": "street", + "type": "terminal", "sector": "blackhills", "exits": { - "north": null, + "north": 170, "south": 2142, "west": null, - "east": null + "east": 167 }, "_gridCol": 8, "_gridRow": 15, - "icon": 54, - "_name": "Road" + "icon": 17, + "_name": "Public Data Terminal" }, { "roomId": 2140, @@ -1394,8 +1394,8 @@ "exits": { "north": null, "south": null, - "west": null, - "east": null + "west": 2141, + "east": 2138 }, "_gridCol": 11, "_gridRow": 15, @@ -1404,12 +1404,12 @@ { "roomId": 2138, "type": "street", - "sector": "silesia", + "sector": "blackhills", "exits": { "north": null, "south": null, - "west": null, - "east": 2137 + "west": 167, + "east": null }, "_gridCol": 12, "_gridRow": 15, @@ -1423,7 +1423,7 @@ "exits": { "north": 2121, "south": null, - "west": 2138, + "west": null, "east": null }, "_gridCol": 13, @@ -1809,7 +1809,7 @@ "north": 2144, "south": null, "west": 168, - "east": null + "east": 2069 }, "_gridCol": 6, "_gridRow": 18, @@ -1823,7 +1823,7 @@ "exits": { "north": 169, "south": 2068, - "west": null, + "west": 2143, "east": null }, "_gridCol": 8, @@ -2812,4 +2812,4 @@ "description": "Optional lower scene-header text, mirrored from SOLARIS.MAP for stock rooms.", "clientMapDescription": "Optional boolean. When true, the client should use the room's description in the map/scene header context for stock rooms." } -} \ No newline at end of file +} From 85d23a0aa94bc5162338b564ca1854ca2dc7837c Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 09:03:52 -0400 Subject: [PATCH 02/14] feat: add REST health API on port 3002 Adds a lightweight HTTP server alongside the existing ARIES TCP servers so the Godot client (and any other tooling) can check server liveness without needing database connectivity beyond what the server already has. - src/api.ts: GET /health -> { ok, version, name } on configurable host/port - src/config.ts: API_PORT (default 3002), API_HOST (default 127.0.0.1) - src/server.ts: startApiServer() wired up after TCP servers; banner updated Port 3002 avoids the existing allocations: 2000/2001 ARIES TCP, 3000 mpbt-web, 3001 mpbt-web NestJS API, 3010 launcher Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ src/config.ts | 6 ++++++ src/server.ts | 7 +++++++ 3 files changed, 61 insertions(+) create mode 100644 src/api.ts diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..96e28ab --- /dev/null +++ b/src/api.ts @@ -0,0 +1,48 @@ +/** + * MPBT REST API server — modern client adapter. + * + * Provides a lightweight HTTP server on API_PORT (default 3000) for the + * Godot 4 client. The ARIES TCP protocol (ports 2000/2001) is unaffected. + * + * Endpoints: + * GET /health → { ok: true, version, name } + */ + +import * as http from 'http'; +import { readFileSync } from 'fs'; +import { Logger } from './util/logger.js'; + +const _pkg = JSON.parse( + readFileSync(new URL('../package.json', import.meta.url), 'utf8'), +) as { version: string }; + +export function startApiServer(log: Logger, host: string, port: number): http.Server { + const apiLog = log.child('api'); + + const server = http.createServer((req, res) => { + const pathname = req.url?.split('?')[0] ?? '/'; + + if (req.method === 'GET' && pathname === '/health') { + const body = JSON.stringify({ ok: true, version: _pkg.version, name: 'mpbt-server' }); + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + }); + res.end(body); + return; + } + + res.writeHead(404); + res.end(); + }); + + server.on('error', (err: Error) => { + apiLog.error('HTTP server error: %s', err.message); + }); + + server.listen(port, host, () => { + apiLog.info('HTTP server listening on %s:%d', host, port); + }); + + return server; +} diff --git a/src/config.ts b/src/config.ts index 60d2fcb..c27a8cb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -73,3 +73,9 @@ export const MPBT_LOG_LEVEL = readLogLevelEnv('MPBT_LOG_LEVEL', 'info'); // Packet hex captures are useful during protocol work, but they add sustained disk // I/O on every send/receive. Keep them opt-in for regular local playtesting. export const MPBT_CAPTURE_ENABLED = readBooleanEnv('MPBT_CAPTURE', false); + +// REST API server for the modern Godot client. Listens on a separate port from +// the ARIES TCP server. Set API_HOST=0.0.0.0 to expose to LAN clients. +// Port 3002 keeps this separate from mpbt-web (3000) and mpbt-web/api (3001). +export const API_PORT = readNonNegativeIntEnv('API_PORT', 3002); +export const API_HOST = process.env['API_HOST'] ?? '127.0.0.1'; diff --git a/src/server.ts b/src/server.ts index b84b71b..7414867 100644 --- a/src/server.ts +++ b/src/server.ts @@ -42,6 +42,7 @@ import { PlayerRegistry, ClientSession } from './state/players.js'; import { launchRegistry } from './state/launch.js'; import { replaceSessionForReconnect } from './state/session-replacement.js'; import { startWorldServer } from './server-world.js'; +import { startApiServer } from './api.js'; import { Logger } from './util/logger.js'; import { CaptureLogger } from './util/capture.js'; import { verifyOrRegister } from './db/accounts.js'; @@ -51,6 +52,8 @@ import { SOCKET_IDLE_TIMEOUT_MS, MPBT_LOG_LEVEL, MPBT_CAPTURE_ENABLED, + API_PORT, + API_HOST, } from './config.js'; // ── Global state ────────────────────────────────────────────────────────────── @@ -715,6 +718,9 @@ server.on('error', (err: Error) => { // Shares the same player registry and logger as the lobby server. startWorldServer(log, players); +// Start the REST API server for the modern Godot client. +startApiServer(log, API_HOST, API_PORT); + server.listen(ARIES_PORT, '0.0.0.0', () => { const addr = server.address() as net.AddressInfo; log.info('═══════════════════════════════════════════════════════'); @@ -728,6 +734,7 @@ server.listen(ARIES_PORT, '0.0.0.0', () => { ' Packet captures: %s', MPBT_CAPTURE_ENABLED ? 'enabled → captures/' : 'disabled (set MPBT_CAPTURE=1 to enable)', ); + log.info(' REST API: http://%s:%d/health', API_HOST, API_PORT); log.info('═══════════════════════════════════════════════════════'); }); From 4f8d357214067d474a6ea01a11ad36ece29a6950 Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 15:08:23 -0400 Subject: [PATCH 03/14] feat: reconstruct world-map.json from verified sources only Replace the 183-room hand-crafted navigation graph (which contained fabricated synthetic IDs in the 2000-series and unverified descriptions) with a clean reconstruction derived exclusively from two authoritative sources: - SOLARIS.MAP binary (room IDs, names, coordinates, lore descriptions) - Retail in-room screenshots (sub-room names, descriptions, exits) Changes: - 6 sector meta-rooms (IDs 1-6): SOLARIS.MAP data only; no screenshots - 26 named locations (IDs 146-171): SOLARIS.MAP coordinates + lore descriptions merged with verified in-room descriptions from screenshots - 27 screenshot sub-rooms (IDs 10001-10027): bars and street rooms visible in retail screenshots (e.g. Hodie's Hideaway, The Iron Throne, Uncle Skippy's House of Madness, The Bubbling Kettel) - 35 stub rooms (IDs 10028-10062): named destinations extracted from exit description text; no description data, exits null; type='stub' - Single shared Public Data Terminal stub (10028) referenced by all 25 non-Starport named locations, matching retail behaviour - No dangling exit targets (all 94 exit references resolve) - _source metadata on every room: 'solaris_map', 'screenshot', 'solaris_map+screenshot', or 'stub' - _screenshot field records which PNG screenshot the room data came from Also adds 'park' and 'stub' to RoomType union in maps.ts to match the two new type values used in the reconstructed file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/data/maps.ts | 2 +- world-map.json | 4055 ++++++++++++++++++++-------------------------- 2 files changed, 1727 insertions(+), 2330 deletions(-) diff --git a/src/data/maps.ts b/src/data/maps.ts index 1242d19..bf2e305 100644 --- a/src/data/maps.ts +++ b/src/data/maps.ts @@ -240,7 +240,7 @@ export function loadSolarisRooms(filePath?: string): WorldRoom[] | null { * Room type tags used in world-map.json. * bar | arena | hub | terminal | bank | street | sector | path */ -export type RoomType = 'bar' | 'arena' | 'hub' | 'terminal' | 'bank' | 'street' | 'sector' | 'path' | 'tram'; +export type RoomType = 'bar' | 'arena' | 'hub' | 'terminal' | 'bank' | 'street' | 'sector' | 'path' | 'tram' | 'park' | 'stub'; /** One entry from world-map.json, representing navigation data for a single room. */ export interface WorldMapRoom { diff --git a/world-map.json b/world-map.json index 8503e74..ca32807 100644 --- a/world-map.json +++ b/world-map.json @@ -1,2815 +1,2212 @@ { - "rooms": [ - { - "roomId": 161, - "_name": "Wasteland", - "description": "You are in a sad little shop called Froglover's Pizza, which appears to have a dearth of customers. To the south you see a public data terminal. To the west you see the Via Dolorosa.", - "type": "street", - "sector": "montenegro", - "icon": 53, - "exits": { - "north": null, - "south": 2132, - "west": null, - "east": null - }, - "_gridCol": 24, - "_gridRow": 5, - "clientMapDescription": true - }, - { - "roomId": 151, - "_name": "Kobe Slums", - "description": "You are relieved of your weapons at the door by a big guy in a fuzzy suit. To the east you see the intersection of Yamato and Proserpina St. To the west you see a data terminal.", - "type": "street", - "sector": "kobe", - "icon": 57, - "exits": { - "north": null, - "south": 2011, - "west": null, - "east": null - }, - "_gridCol": 7, - "_gridRow": 6, - "clientMapDescription": true - }, - { - "roomId": 157, - "_name": "Factory", - "description": "You are in a bar called The Reiver. To the east you see a public data terminal. To the west you see Montenegro Street.", - "type": "arena", - "sector": "montenegro", - "icon": 12, - "exits": { - "north": null, - "south": 2049, - "west": 2129, - "east": null - }, - "_gridCol": 20, - "_gridRow": 6, - "clientMapDescription": true - }, - { - "roomId": 2129, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": 2130, - "west": null, - "east": 157 - }, - "_gridCol": 21, - "_gridRow": 6, - "icon": 51, - "_name": "Montenegro Street" - }, - { - "roomId": 2130, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 2129, - "south": null, - "west": null, - "east": 2131 - }, - "_gridCol": 22, - "_gridRow": 6, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2131, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": 2130, - "east": 2132 - }, - "_gridCol": 23, - "_gridRow": 6, - "icon": 54, - "_name": "Road" - }, - { - "roomId": 2132, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 161, - "south": null, - "west": 2131, - "east": null - }, - "_gridCol": 24, - "_gridRow": 6, - "icon": 53, - "_name": "Via Dolorosa" - }, - { - "roomId": 149, - "_name": "White Lotus", - "description": "You are in the Snowbird. To the north you see Buckminster Circle. To the east you see a public data terminal.", - "type": "bar", - "sector": "kobe", - "icon": 26, - "exits": { - "north": 2094, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 3, - "_gridRow": 7, - "clientMapDescription": true - }, - { - "roomId": 2094, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": 149, - "west": null, - "east": null - }, - "_gridCol": 4, - "_gridRow": 7, - "icon": 50, - "_name": "Buckminster Circle" - }, - { - "roomId": 2011, - "type": "street", - "sector": "kobe", - "exits": { - "north": 151, - "south": 2, - "west": null, - "east": null - }, - "_gridCol": 7, - "_gridRow": 7, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 148, - "_name": "Government House", - "description": "You are in the Marauder Bar. To the north you see a public data terminal. To the south you see Yamato St.", - "type": "terminal", - "sector": "kobe", - "icon": 45, - "exits": { - "north": null, - "south": 2005, - "west": null, - "east": 2088 - }, - "_gridCol": 10, - "_gridRow": 7, - "clientMapDescription": true - }, - { - "roomId": 2088, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": 2046, - "west": 148, - "east": 2089 - }, - "_gridCol": 11, - "_gridRow": 7, - "icon": 50, - "_name": "Road" - }, - { - "roomId": 2089, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": 2045, - "west": 2088, - "east": 2090 - }, - "_gridCol": 12, - "_gridRow": 7, - "icon": 56, - "_name": "Road" - }, - { - "roomId": 2090, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": 2044, - "west": 2089, - "east": 2091 - }, - "_gridCol": 13, - "_gridRow": 7, - "icon": 50, - "_name": "Road" - }, - { - "roomId": 2091, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 2090, - "east": null - }, - "_gridCol": 14, - "_gridRow": 7, - "icon": 55, - "_name": "Road" - }, - { - "roomId": 2092, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": 2042, - "west": null, - "east": 2093 - }, - "_gridCol": 15, - "_gridRow": 7, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 2093, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": 2041, - "west": 2092, - "east": null - }, - "_gridCol": 16, - "_gridRow": 7, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 2049, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 157, - "south": 2048, - "west": null, - "east": null - }, - "_gridCol": 20, - "_gridRow": 7, - "icon": 54, - "_name": "Road" - }, - { - "roomId": 2009, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": 2010, - "west": null, - "east": 2008 - }, - "_gridCol": 4, - "_gridRow": 8, - "icon": 57, - "_name": "Road" - }, - { - "roomId": 2008, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 2009, - "east": 2007 - }, - "_gridCol": 5, - "_gridRow": 8, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2007, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 2008, - "east": 2 - }, - "_gridCol": 6, - "_gridRow": 8, - "icon": 57, - "_name": "Road" - }, - { - "roomId": 2, - "_name": "Kobe Sector", - "description": "The Kurita sector is beautiful as well as safe. The tourist trade is detoured around those areas with a higher crime rate, and the corruption within Kobe authority is not obvious to the outsider. The fixed arenas in Kobe use desert terrain.", - "type": "sector", - "sector": "kobe", - "icon": 6, - "exits": { - "north": 2011, - "south": null, - "west": 2007, - "east": 2003 - }, - "_gridCol": 7, - "_gridRow": 8, - "clientMapDescription": true - }, - { - "roomId": 2003, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 2, - "east": 2004 - }, - "_gridCol": 8, - "_gridRow": 8, - "icon": 54, - "_name": "Road" - }, - { - "roomId": 2004, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 2003, - "east": null - }, - "_gridCol": 9, - "_gridRow": 8, - "icon": 50, - "_name": "Road" - }, - { - "roomId": 2005, - "type": "street", - "sector": "kobe", - "exits": { - "north": 148, - "south": 2006, - "west": null, - "east": 2046 - }, - "_gridCol": 10, - "_gridRow": 8, - "icon": 50, - "_name": "Yamato St" - }, - { - "roomId": 2046, - "type": "street", - "sector": "kobe", - "exits": { - "north": 2088, - "south": null, - "west": 2005, - "east": 2045 - }, - "_gridCol": 11, - "_gridRow": 8, - "icon": 56, - "_name": "Proserpina St" - }, - { - "roomId": 2045, - "type": "street", - "sector": "kobe", - "exits": { - "north": 2089, - "south": null, - "west": 2046, - "east": 2044 - }, - "_gridCol": 12, - "_gridRow": 8, - "icon": 56, - "_name": "Road" - }, - { - "roomId": 2044, - "type": "street", - "sector": "kobe", - "exits": { - "north": 2090, - "south": null, - "west": 2045, - "east": null - }, - "_gridCol": 13, - "_gridRow": 8, - "icon": 57, - "_name": "Road" - }, - { - "roomId": 2043, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": null, - "east": 2042 - }, - "_gridCol": 14, - "_gridRow": 8, - "icon": 54, - "_name": "Road" - }, - { - "roomId": 2042, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 2092, - "south": null, - "west": 2043, - "east": 2041 - }, - "_gridCol": 15, - "_gridRow": 8, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2041, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 2093, - "south": 2050, - "west": 2042, - "east": 2040 - }, - "_gridCol": 16, - "_gridRow": 8, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2040, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": 2041, - "east": 4 - }, - "_gridCol": 17, - "_gridRow": 8, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 4, - "_name": "Montenegro Sector", - "description": "Montenegro is an ugly place, with its abandoned industrial complexes and fierce commercialization. Authorities are far more concerned about maintaining the ties between member-states than improving living and working conditions in the sector. The fixed arenas in Montenegro use tropical terrain.", - "type": "sector", - "sector": "montenegro", - "icon": 35, - "exits": { - "north": null, - "south": 2051, - "west": 2040, - "east": 2047 - }, - "_gridCol": 18, - "_gridRow": 8, - "clientMapDescription": true - }, - { - "roomId": 2047, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": 4, - "east": 2048 - }, - "_gridCol": 19, - "_gridRow": 8, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 2048, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 2049, - "south": 159, - "west": 2047, - "east": null - }, - "_gridCol": 20, - "_gridRow": 8, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2010, - "type": "street", - "sector": "kobe", - "exits": { - "north": 2009, - "south": 150, - "west": null, - "east": null - }, - "_gridCol": 4, - "_gridRow": 9, - "icon": 54, - "_name": "Robert Kurita St" - }, - { - "roomId": 2006, - "type": "street", - "sector": "kobe", - "exits": { - "north": 2005, - "south": 147, - "west": null, - "east": null - }, - "_gridCol": 10, - "_gridRow": 9, - "icon": 50, - "_name": "Road" - }, - { - "roomId": 2050, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 2041, - "south": 158, - "west": null, - "east": null - }, - "_gridCol": 16, - "_gridRow": 9, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 2051, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 4, - "south": 2052, - "west": null, - "east": null - }, - "_gridCol": 18, - "_gridRow": 9, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 159, - "_name": "Allman", - "description": "You are in Brit's. To the south you see the Via Dolorosa. To the east you see a public data terminal.", - "type": "street", - "sector": "montenegro", - "icon": 54, - "exits": { - "north": 2048, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 20, - "_gridRow": 9, - "clientMapDescription": true - }, - { - "roomId": 150, - "_name": "Waterfront", - "description": "You are in The Silver Claw. To the north you see Robert Kurita St. To the west you see a public data terminal.", - "type": "bar", - "sector": "kobe", - "icon": 46, - "exits": { - "north": 2010, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 4, - "_gridRow": 10, - "clientMapDescription": true - }, - { - "roomId": 2086, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": 2087, - "west": null, - "east": 2085 - }, - "_gridCol": 8, - "_gridRow": 10, - "icon": 54, - "_name": "Road" - }, - { - "roomId": 2085, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 2086, - "east": 147 - }, - "_gridCol": 9, - "_gridRow": 10, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 147, - "_name": "Ishiyama Arena", - "description": "You are in a bar called The Paradise. To the east you see Theodore Kurita St. To the west you see a public data terminal.", - "type": "arena", - "sector": "kobe", - "icon": 28, - "exits": { - "north": 2006, - "south": 2077, - "west": 2085, - "east": 2072 - }, - "_gridCol": 10, - "_gridRow": 10, - "clientMapDescription": true - }, - { - "roomId": 2072, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 147, - "east": 2073 - }, - "_gridCol": 11, - "_gridRow": 10, - "icon": 57, - "_name": "Theodore Kurita St" - }, - { - "roomId": 2073, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 2072, - "east": 2074 - }, - "_gridCol": 12, - "_gridRow": 10, - "icon": 56, - "_name": "North Theodore Kurita St" - }, - { - "roomId": 2074, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": 2073, - "east": null - }, - "_gridCol": 13, - "_gridRow": 10, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2075, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 14, - "_gridRow": 10, - "icon": 53, - "_name": "West Bolivar St" - }, - { - "roomId": 2076, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": 158, - "east": null - }, - "_gridCol": 15, - "_gridRow": 10, - "icon": 54, - "_name": "Bolivar St" - }, - { - "roomId": 158, - "_name": "Marik Tower", - "description": "You are in The Home Guard Club. To the east you see Bolivar St. To the west you see a public data terminal.", - "type": "terminal", - "sector": "montenegro", - "icon": 17, - "exits": { - "north": 2050, - "south": 2133, - "west": null, - "east": 2076 - }, - "_gridCol": 16, - "_gridRow": 10, - "clientMapDescription": true - }, - { - "roomId": 2052, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 2051, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 18, - "_gridRow": 10, - "icon": 54, - "_name": "Road" - }, - { - "roomId": 2087, - "type": "street", - "sector": "kobe", - "exits": { - "north": 2086, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 8, - "_gridRow": 11, - "icon": 54, - "_name": "Frances Ave" - }, - { - "roomId": 2077, - "type": "street", - "sector": "kobe", - "exits": { - "north": 147, - "south": 2078, - "west": null, - "east": null - }, - "_gridCol": 10, - "_gridRow": 11, - "icon": 55, - "_name": "Road" - }, - { - "roomId": 2133, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 158, - "south": 2134, - "west": null, - "east": null - }, - "_gridCol": 16, - "_gridRow": 11, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2053, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, - "south": 2054, - "west": null, - "east": null - }, - "_gridCol": 18, - "_gridRow": 11, - "icon": 57, - "_name": "Road" - }, - { - "roomId": 160, - "_name": "Riverfront", - "description": "You are in Hangar 66. To the north you see Reisman St. To the south you see a public data terminal.", - "type": "street", - "sector": "montenegro", - "icon": 53, - "exits": { - "north": 2112, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 20, - "_gridRow": 11, - "clientMapDescription": true - }, - { - "roomId": 2147, - "type": "street", - "sector": "blackhills", - "exits": { - "north": null, - "south": 2148, - "west": null, - "east": null - }, - "_gridCol": 6, - "_gridRow": 12, - "icon": 54, - "_name": "Road" - }, - { - "roomId": 2146, - "type": "street", - "sector": "kobe", - "exits": { - "north": null, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 7, - "_gridRow": 12, - "icon": 57, - "_name": "Road" - }, - { - "roomId": 170, - "_name": "Marina", - "description": "You are in the Pelican. To the north you see Frances Ave. To the south you see a public data terminal.", - "type": "bar", - "sector": "blackhills", - "icon": 1, - "exits": { - "north": null, - "south": 2141, - "west": null, - "east": null - }, - "_gridCol": 8, - "_gridRow": 12, - "clientMapDescription": true - }, - { - "roomId": 2078, - "type": "street", - "sector": "kobe", - "exits": { - "north": 2077, - "south": 2079, - "west": null, - "east": null - }, - "_gridCol": 10, - "_gridRow": 12, - "icon": 57, - "_name": "Road" - }, - { - "roomId": 2134, - "type": "street", - "sector": "montenegro", - "exits": { - "north": 2133, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 16, - "_gridRow": 12, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 2054, - "type": "street", - "sector": "silesia", - "exits": { - "north": 2053, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 18, - "_gridRow": 12, - "icon": 47, - "_name": "Road" - }, - { - "roomId": 2112, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": 160, - "west": null, - "east": null - }, - "_gridCol": 20, - "_gridRow": 12, - "icon": 52, - "_name": "Reisman St" - }, - { - "roomId": 2148, - "type": "street", - "sector": "blackhills", - "exits": { - "north": 2147, - "south": 2149, - "west": null, - "east": null - }, - "_gridCol": 6, - "_gridRow": 13, - "icon": 52, - "_name": "East Frances Ave" - }, - { - "roomId": 2079, - "type": "street", - "sector": "kobe", - "exits": { - "north": 2078, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 10, - "_gridRow": 13, - "icon": 56, - "_name": "Road" - }, - { - "roomId": 2080, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, - "south": 2123, - "west": null, - "east": 2081 - }, - "_gridCol": 11, - "_gridRow": 13, - "icon": 48, - "_name": "Road" - }, - { - "roomId": 2081, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, - "south": null, - "west": 2080, - "east": null - }, - "_gridCol": 12, - "_gridRow": 13, - "icon": 47, - "_name": "Road" - }, - { - "roomId": 2082, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": null, - "east": 2083 - }, - "_gridCol": 13, - "_gridRow": 13, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2083, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": 2120, - "west": 2082, - "east": 2084 - }, - "_gridCol": 14, - "_gridRow": 13, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 2084, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": 2083, - "east": null - }, - "_gridCol": 15, - "_gridRow": 13, - "icon": 53, - "_name": "Capellan St" - }, - { - "roomId": 165, - "_name": "Rivertown", - "description": "You are in the Cobalt Coil. To the east you see Capellan St. To the west you see a public data terminal.", - "type": "street", - "sector": "cathay", - "icon": 51, - "exits": { - "north": null, - "south": 2116, - "west": null, - "east": null - }, - "_gridCol": 16, - "_gridRow": 13, - "clientMapDescription": true - }, - { - "roomId": 2055, - "type": "street", - "sector": "montenegro", - "exits": { - "north": null, - "south": null, - "west": 2056, - "east": null - }, - "_gridCol": 18, - "_gridRow": 13, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 2111, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, - "south": 2110, - "west": null, - "east": 2105 - }, - "_gridCol": 20, - "_gridRow": 13, - "icon": 48, - "_name": "Road" - }, - { - "roomId": 2105, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, - "south": 155, - "west": 2111, - "east": 2104 - }, - "_gridCol": 21, - "_gridRow": 13, - "icon": 48, - "_name": "Fraunstalh St" - }, + "_comment": "Reconstructed from SOLARIS.MAP binary and retail in-room screenshots. Synthetic IDs 10001-10027 = sub-rooms; 10028-10062 = stub/terminal nodes. Do not edit manually.", + "_sources": [ + "SOLARIS.MAP", + "Solaris_Bitmap screenshots" + ], + "rooms": [ { - "roomId": 2104, - "type": "street", - "sector": "silesia", + "roomId": 1, + "name": "International Sector", + "type": "sector", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": 42, + "y": 388, + "labelW": 126, + "labelH": 46, + "isSector": true, + "mapDescription": "This area, composed primarily of public-works installations and tourist areas, is under the direct control of the Solaris central authority. There are no arenas in this sector.", + "description": null, "exits": { "north": null, "south": null, - "west": 2105, - "east": 2103 + "east": null, + "west": null }, - "_gridCol": 22, - "_gridRow": 13, - "icon": 45, - "_name": "Road" + "_source": "solaris_map", + "_screenshot": null }, { - "roomId": 2103, - "type": "street", - "sector": "silesia", + "roomId": 2, + "name": "Kobe Sector", + "type": "sector", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": 86, + "y": 106, + "labelW": 46, + "labelH": 24, + "isSector": true, + "mapDescription": "The Kurita sector is beautiful as well as safe. The tourist trade is detoured around those areas with a higher crime rate, and the corruption within Kobe authority is not obvious to the outsider. The fixed arenas in Kobe use desert terrain.", + "description": null, "exits": { "north": null, "south": null, - "west": 2104, - "east": 153 + "east": null, + "west": null }, - "_gridCol": 23, - "_gridRow": 13, - "icon": 51, - "_name": "Road" + "_source": "solaris_map", + "_screenshot": null }, { - "roomId": 153, - "_name": "Lyran Building", - "description": "You are in the Vindictive Philanthropist, writing bad checks to charity. To the north you see a public data terminal. To the south you see Arnulf St.", - "type": "terminal", + "roomId": 3, + "name": "Silesia Sector", + "type": "sector", "sector": "silesia", - "icon": 17, + "districtIndex": 2, + "districtName": "Silesia", + "x": 322, + "y": 306, + "labelW": 64, + "labelH": 24, + "isSector": true, + "mapDescription": "Despite official attempts to maintain Silesia as a showpiece of Lyran culture, it suffers the same problems as the rest of the City. The beautiful private estates of the Sector are frequently financed by the very crime that devastates its poorer areas. The fixed arenas in Silesia use temperate terrain.", + "description": null, "exits": { "north": null, "south": null, - "west": 2103, - "east": null - }, - "_gridCol": 24, - "_gridRow": 13, - "clientMapDescription": true - }, - { - "roomId": 2149, - "type": "street", - "sector": "blackhills", - "exits": { - "north": 2148, - "south": 171, - "west": null, - "east": null - }, - "_gridCol": 6, - "_gridRow": 14, - "icon": 54, - "_name": "Laurel Ave" - }, - { - "roomId": 2123, - "type": "street", - "sector": "silesia", - "exits": { - "north": 2080, - "south": null, - "west": null, - "east": 2122 + "east": null, + "west": null }, - "_gridCol": 11, - "_gridRow": 14, - "icon": 47, - "_name": "Road" + "_source": "solaris_map", + "_screenshot": null }, { - "roomId": 2120, - "type": "street", + "roomId": 4, + "name": "Montenegro Sector", + "type": "sector", "sector": "montenegro", - "exits": { - "north": 2083, - "south": null, - "west": null, - "east": 2119 - }, - "_gridCol": 14, - "_gridRow": 14, - "icon": 51, - "_name": "Road" - }, - { - "roomId": 2116, - "type": "street", - "sector": "cathay", - "exits": { - "north": 165, - "south": 2117, - "west": null, - "east": 2115 - }, - "_gridCol": 16, - "_gridRow": 14, - "icon": 53, - "_name": "Road" - }, - { - "roomId": 2115, - "type": "street", - "sector": "cathay", - "exits": { - "north": null, - "south": 166, - "west": 2116, - "east": 2114 - }, - "_gridCol": 17, - "_gridRow": 14, - "icon": 48, - "_name": "Road" - }, - { - "roomId": 2114, - "type": "street", - "sector": "cathay", + "districtIndex": 3, + "districtName": "Montenegro", + "x": 274, + "y": 92, + "labelW": 110, + "labelH": 24, + "isSector": true, + "mapDescription": "Montenegro is an ugly place, with its abandoned industrial complexes and fierce commercialization. Authorities are far more concerned about maintaining the ties between member-states than improving living and working conditions in the sector. The fixed arenas in Montenegro use tropical terrain.", + "description": null, "exits": { "north": null, "south": null, - "west": 2115, - "east": 2113 + "east": null, + "west": null }, - "_gridCol": 18, - "_gridRow": 14, - "icon": 57, - "_name": "Road" + "_source": "solaris_map", + "_screenshot": null }, { - "roomId": 2113, - "type": "street", + "roomId": 5, + "name": "Cathay Sector", + "type": "sector", "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": 206, + "y": 326, + "labelW": 68, + "labelH": 24, + "isSector": true, + "mapDescription": "Cathay displays the sharpest contrasts between the classes. A large body of what are essentially working-class serfs maintain the luxurious splendor of the small upper class. Crime and gang activity is rampant, despite the ruthless efficiency of Cathay Security. The fixed arenas in Cathay use temperate and tropical terrain.", + "description": null, "exits": { "north": null, "south": null, - "west": 2114, - "east": null - }, - "_gridCol": 19, - "_gridRow": 14, - "icon": 51, - "_name": "Alley off Maximilian Ave" - }, - { - "roomId": 2110, - "type": "street", - "sector": "silesia", - "exits": { - "north": 2111, - "south": null, - "west": null, - "east": 155 - }, - "_gridCol": 20, - "_gridRow": 14, - "icon": 52, - "_name": "Road" - }, - { - "roomId": 155, - "_name": "Riverside", - "description": "You are in the Swooping Crane. To the north you see Fraunstalh St. To the south you see a public data terminal.", - "type": "bar", - "sector": "silesia", - "icon": 46, - "exits": { - "north": 2105, - "south": 2028, - "west": 2110, - "east": null - }, - "_gridCol": 21, - "_gridRow": 14, - "clientMapDescription": true - }, - { - "roomId": 171, - "_name": "Viewpoint", - "description": "You are in the Cryptic Skeptic, though probably not for too much longer...considering. To the north you see a public data terminal. To the west you see a busy pedestrian interchange.", - "type": "street", - "sector": "blackhills", - "icon": 53, - "exits": { - "north": 2149, - "south": null, - "west": 2145, - "east": null + "east": null, + "west": null }, - "_gridCol": 6, - "_gridRow": 15, - "clientMapDescription": true + "_source": "solaris_map", + "_screenshot": null }, { - "roomId": 2141, - "type": "terminal", + "roomId": 6, + "name": "Black Hills Sector", + "type": "sector", "sector": "blackhills", - "exits": { - "north": 170, - "south": 2142, - "west": null, - "east": 167 - }, - "_gridCol": 8, - "_gridRow": 15, - "icon": 17, - "_name": "Public Data Terminal" - }, - { - "roomId": 2140, - "type": "street", - "sector": "silesia", + "districtIndex": 5, + "districtName": "Black Hills", + "x": 104, + "y": 310, + "labelW": 56, + "labelH": 46, + "isSector": true, + "mapDescription": "The Davion sector presents itself as a high-tech bastion of civilization and virtue, but squalor and crime are just as prevalent here as anywhere in the city. The fixed arenas in Black Hills use arctic terrain.", + "description": null, "exits": { "north": null, "south": null, - "west": null, - "east": 2139 + "east": null, + "west": null }, - "_gridCol": 9, - "_gridRow": 15, - "icon": 48, - "_name": "Road" + "_source": "solaris_map", + "_screenshot": null }, { - "roomId": 2139, - "type": "street", - "sector": "silesia", + "roomId": 146, + "name": "Solaris Starport", + "type": "hub", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": 30, + "y": 311, + "labelW": 50, + "labelH": 70, + "isSector": false, + "mapDescription": "This area, composed primarily of public-works installations and tourist areas, is under the direct control of the Solaris central authority. There are no arenas in this sector.", + "description": "This area, composed primarily of public-works installations and tourist areas, is under the direct control of the Solaris central authority. There are no arenas in this sector.", "exits": { "north": null, "south": null, - "west": 2140, - "east": null + "east": null, + "west": null }, - "_gridCol": 10, - "_gridRow": 15, - "icon": 52, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "International Zone/1.png", + "_notes": "Screenshot 1.png is travel map view; no icon grid visible, exits not determinable." }, { - "roomId": 167, - "_name": "Davion Arena", - "description": "You are in The Sun and Sword, one of the older bars in the city. To the east you see Halloran St. To the west you see a public data terminal.", + "roomId": 147, + "name": "Ishiyama Arena", "type": "arena", - "sector": "blackhills", - "icon": 32, - "exits": { - "north": null, - "south": null, - "west": 2141, - "east": 2138 - }, - "_gridCol": 11, - "_gridRow": 15, - "clientMapDescription": true - }, - { - "roomId": 2138, - "type": "street", - "sector": "blackhills", - "exits": { - "north": null, - "south": null, - "west": 167, - "east": null - }, - "_gridCol": 12, - "_gridRow": 15, - "icon": 53, - "_name": "Halloran St" - }, - { - "roomId": 2137, - "type": "street", - "sector": "silesia", - "exits": { - "north": 2121, - "south": null, - "west": null, - "east": null - }, - "_gridCol": 13, - "_gridRow": 15, - "icon": 45, - "_name": "Road" - }, - { - "roomId": 2136, - "type": "street", - "sector": "cathay", - "exits": { - "north": null, - "south": null, - "west": null, - "east": 2135 - }, - "_gridCol": 14, - "_gridRow": 15, - "icon": 46, - "_name": "Road" - }, - { - "roomId": 2135, - "type": "street", - "sector": "cathay", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": 132, + "y": 131, + "labelW": 51, + "labelH": 29, + "isSector": false, + "mapDescription": "Iron Mountain, the centerpiece of Kobe and the destination of thousands of game fans each week, stands here. Ishiyama is perhaps the most feared arena by Mechwarriors, perhaps accounting for its consistently high ratings and huge waiting list. The street layout is very simple.", + "description": "You are in a bar called The Paradise. To the east you see Theodore Kurita St. To the west you see a public data terminal.", "exits": { "north": null, "south": null, - "west": 2136, - "east": 2117 - }, - "_gridCol": 15, - "_gridRow": 15, - "icon": 48, - "_name": "Road" - }, - { - "roomId": 2117, - "type": "street", - "sector": "cathay", - "exits": { - "north": 2116, - "south": 2118, - "west": 2135, - "east": 166 + "east": 10029, + "west": 10028 }, - "_gridCol": 16, - "_gridRow": 15, - "icon": 48, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Kobe/Ishiyama Arena/1.png" }, { - "roomId": 166, - "_name": "Maze", - "description": "You are in Belthasar's Hideaway, where you had better keep looking over your shoulder and keep your back to the wall. To the north you see a public data terminal. To the south you see Confederation Ave. running east and west.", - "type": "street", - "sector": "cathay", - "icon": 54, + "roomId": 148, + "name": "Government House", + "type": "terminal", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": 142, + "y": 85, + "labelW": 66, + "labelH": 29, + "isSector": false, + "mapDescription": "With a sweeping view of War Memorial Park, this district is the center of civil administration for the sector. The stately white Government House is the official home of the Kobe governor. The street layout is simple.", + "description": "You are in the Marauder Bar. To the north you see a public data terminal. To the south you see Yamato St.", "exits": { - "north": 2115, - "south": null, - "west": 2117, - "east": null + "north": 10028, + "south": 10030, + "east": null, + "west": null }, - "_gridCol": 17, - "_gridRow": 15, - "clientMapDescription": true + "_source": "solaris_map+screenshot", + "_screenshot": "Kobe/Government House/1.png" }, { - "roomId": 2028, - "type": "street", - "sector": "silesia", + "roomId": 149, + "name": "White Lotus", + "type": "bar", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": 25, + "y": 84, + "labelW": 68, + "labelH": 15, + "isSector": false, + "mapDescription": "The standard of living in White Lotus is second only to the Chancellor's Quarter in Cathay, and security in the district is the best on the planet. The street layout is of average complexity.", + "description": "You are in the Snowbird. To the north you see Buckminster Circle. To the east you see a public data terminal.", "exits": { - "north": 155, - "south": 2027, - "west": null, - "east": null + "north": 10031, + "south": null, + "east": 10028, + "west": null }, - "_gridCol": 21, - "_gridRow": 15, - "icon": 48, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Kobe/White Lotus/1.png", + "_notes": "Iron Throne (10012) south exit references 'The Snowbird' (this room), but this room's north exit is 10031 (Buckminster Circle) per its own description — reverse exit conflict, not overridden." }, { - "roomId": 2145, - "type": "street", - "sector": "blackhills", + "roomId": 150, + "name": "Waterfront", + "type": "bar", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": 35, + "y": 140, + "labelW": 67, + "labelH": 15, + "isSector": false, + "mapDescription": "Shrines, parks and statuary line the immaculate waterfront of Kobe. The high security is used to advantage by the crime bosses for their meetings, much to the exasperation of the Kobe upper-class. The street layout is of average complexity.", + "description": "You are in The Silver Claw. To the north you see Robert Kurita St. To the west you see a public data terminal.", "exits": { - "north": null, - "south": 2144, - "west": null, - "east": 171 + "north": 10032, + "south": null, + "east": null, + "west": 10028 }, - "_gridCol": 6, - "_gridRow": 16, - "icon": 54, - "_name": "Hanse Davion Dr" + "_source": "solaris_map+screenshot", + "_screenshot": "Kobe/Waterfront/1.png" }, { - "roomId": 2142, + "roomId": 151, + "name": "Kobe Slums", "type": "street", - "sector": "blackhills", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": 83, + "y": 60, + "labelW": 62, + "labelH": 15, + "isSector": false, + "mapDescription": "Despite Kobe's general high standard of living, some still live in poverty, pursued by the demons of gambling addiction and crime. The street layout is complex.", + "description": "You are relieved of your weapons at the door by a big guy in a fuzzy suit. To the east you see the intersection of Yamato and Proserpina St. To the west you see a data terminal.", "exits": { - "north": 2141, - "south": 169, - "west": null, - "east": null + "north": null, + "south": null, + "east": 10033, + "west": 10028 }, - "_gridCol": 8, - "_gridRow": 16, - "icon": 52, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Kobe/Kobe Slums/1.png" }, { - "roomId": 2039, - "type": "street", + "roomId": 152, + "name": "Steiner Stadium", + "type": "arena", "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": 323, + "y": 249, + "labelW": 45, + "labelH": 29, + "isSector": false, + "mapDescription": "Looking like a tremendous Roman coliseum, Steiner Stadium is the only arena to offer seating within the actual arena for spectators, and utilizes lostech to protect them from weapons fire and explosions from the fierce battles taking place within this neoclassical edifice. The street layout is very simple.", + "description": "You are in a bar called the Copper Coin. To the east you see Dusseldorf Street. To the west you see a public data terminal.", "exits": { "north": null, - "south": 2038, - "west": null, - "east": null + "south": null, + "east": 10035, + "west": 10028 }, - "_gridCol": 11, - "_gridRow": 16, - "icon": 46, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Silesia/Steiner Stadium/1.png" }, { - "roomId": 2118, - "type": "street", - "sector": "cathay", + "roomId": 153, + "name": "Lyran Building", + "type": "terminal", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": 366, + "y": 182, + "labelW": 76, + "labelH": 18, + "isSector": false, + "mapDescription": "The armored fist of House Steiner graces the entrance to this grim, gray structure, where Lyran officials administer their sector of Solaris. The street layout is of average complexity.", + "description": "You are in the Vindictive Philanthropist, writing bad checks to charity. To the north you see a public data terminal. To the south you see Arnulf St.", "exits": { - "north": 2117, - "south": 162, - "west": null, - "east": null + "north": 10028, + "south": 10036, + "east": null, + "west": null }, - "_gridCol": 16, - "_gridRow": 16, - "icon": 57, - "_name": "Confederation Ave" + "_source": "solaris_map+screenshot", + "_screenshot": "Silesia/Lyran Building/1.png" }, { - "roomId": 2027, - "type": "street", + "roomId": 154, + "name": "Chahar Park", + "type": "park", "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": 387, + "y": 331, + "labelW": 40, + "labelH": 29, + "isSector": false, + "mapDescription": "The man-made lake at the center of the park is well-loved by boaters and picnickers, but night brings muggers and drug dealers, who have thus far resisted the stepped-up patrols instituted by Steiner security forces. The street layout is simple.", + "description": "You are in Thor's Shieldhall. To the south you see a public data terminal. To the west you see Vialla Circle.", "exits": { - "north": 2028, - "south": null, - "west": 152, - "east": null + "north": null, + "south": 10028, + "east": null, + "west": 10037 }, - "_gridCol": 21, - "_gridRow": 16, - "icon": 47, - "_name": "Dusseldorf Street" + "_source": "solaris_map+screenshot", + "_screenshot": "Silesia/Chahar Park/1.png" }, { - "roomId": 2144, - "type": "street", - "sector": "blackhills", - "exits": { - "north": 2145, - "south": 2143, - "west": null, - "east": null + "roomId": 155, + "name": "Riverside", + "type": "bar", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": 321, + "y": 199, + "labelW": 52, + "labelH": 15, + "isSector": false, + "mapDescription": "Within sight of Cathay River Town, the Silesian waterfront is the location of numerous high-class hotels and condominiums. The street layout is of average complexity.", + "description": "You are in the Swooping Crane. To the north you see Fraunstah St. To the south you see a public data terminal.", + "exits": { + "north": 10038, + "south": 10028, + "east": null, + "west": null }, - "_gridCol": 6, - "_gridRow": 17, - "icon": 52, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Silesia/Riverside/1.png" }, { - "roomId": 169, - "_name": "Guzman Park", - "description": "You are in Skippy's Fern Bar. A classy joint where warriors meet to drink and tell lies about their battles. Skippy's is known for serving the best. To the south you see Swift Shore Drive. To the west you see a public data terminal.", + "roomId": 156, + "name": "Black Thorne", "type": "street", - "sector": "blackhills", - "icon": 53, + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": 303, + "y": 369, + "labelW": 71, + "labelH": 15, + "isSector": false, + "mapDescription": "Parts of this district live in relative comfort, but other areas still experience a high crime rate, and many citizens arm themselves for protection against the organized crime triads. The street layout is complex.", + "description": "You are just in time. The mysterious \"Deadhead\" cult is having a reading. Plus, some woman named \"Sunshine\" keeps telling you you're \"beautiful.\" To the north you see a public data terminal. To the south you see the intersection of Arnulf St. and Marburg St.", "exits": { - "north": 2142, - "south": 2069, - "west": null, - "east": null + "north": 10028, + "south": 10039, + "east": null, + "west": null }, - "_gridCol": 8, - "_gridRow": 17, - "clientMapDescription": true + "_source": "solaris_map+screenshot", + "_screenshot": "Silesia/Black Thorne/1.png" }, { - "roomId": 2038, - "type": "street", - "sector": "silesia", + "roomId": 157, + "name": "Factory", + "type": "arena", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": 296, + "y": 61, + "labelW": 45, + "labelH": 15, + "isSector": false, + "mapDescription": "A refitted shuttle production facility, The Factory has enjoyed remarkable success despite its small size and lack of sophistication. Situated within the industrial sector, it draws consistent crowds, who suffer the cramped conditions gladly. The street layout is very simple.", + "description": "You are in a bar called The Reiver. To the east you see a public data terminal. To the west you see Montenegro Street.", "exits": { - "north": 2039, - "south": 2037, - "west": null, - "east": 2102 + "north": null, + "south": null, + "east": 10028, + "west": 10044 }, - "_gridCol": 11, - "_gridRow": 17, - "icon": 46, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Montenegro/Factory/1.png" }, { - "roomId": 2102, - "type": "street", - "sector": "silesia", + "roomId": 158, + "name": "Marik Tower", + "type": "terminal", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": 240, + "y": 126, + "labelW": 37, + "labelH": 28, + "isSector": false, + "mapDescription": "This brand-new mountain of glass and steel bears the Marik eagle proudly over its main entrance, and is the center of Montenegro civic authority, as well as the diplomatic and governmental quarter of the sector. The street layout is simple.", + "description": "You are In The Home Guard Club. To the east you see Bolivar St. To the west you see a public data terminal.", "exits": { "north": null, "south": null, - "west": 2038, - "east": null + "east": 10045, + "west": 10028 }, - "_gridCol": 12, - "_gridRow": 17, - "icon": 45, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Montenegro/Marik Tower/1.png" }, { - "roomId": 2101, + "roomId": 159, + "name": "Allman", "type": "street", - "sector": "cathay", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": 311, + "y": 124, + "labelW": 38, + "labelH": 14, + "isSector": false, + "mapDescription": "The common people's residential area is every bit as bleak and charmless as the rest of Montenegro, with anonymous high-rises and row houses as the rule. The street layout is of average complexity.", + "description": "You are in Brit's. To the south you see the Via Dolorosa. To the east you see a public data terminal.", "exits": { "north": null, - "south": null, - "west": null, - "east": 2100 + "south": 10046, + "east": 10028, + "west": null }, - "_gridCol": 13, - "_gridRow": 17, - "icon": 49, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Montenegro/Allman/1.png" }, { - "roomId": 2100, + "roomId": 160, + "name": "Riverfront", "type": "street", - "sector": "cathay", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": 305, + "y": 155, + "labelW": 59, + "labelH": 15, + "isSector": false, + "mapDescription": "A place of burned-out warehouses and the twisted remains of dock cranes and loaders, the Riverfront, like the Wasteland, is home to numerous gangs, squatters and other criminals. The street layout is of average complexity.", + "description": "You are in Hangar 66. To the north you see Reisman St. To the south you see a public data terminal.", "exits": { - "north": null, - "south": null, - "west": 2101, - "east": 2099 + "north": 10047, + "south": 10028, + "east": null, + "west": null }, - "_gridCol": 14, - "_gridRow": 17, - "icon": 48, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Montenegro/Riverfront/1.png" }, { - "roomId": 2099, + "roomId": 161, + "name": "Wasteland", "type": "street", - "sector": "cathay", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": 365, + "y": 50, + "labelW": 60, + "labelH": 15, + "isSector": false, + "mapDescription": "This grim, deadly jungle of abandoned manufacturing plants, warehouses, foundries and offices holds only the desperate, luckless and ruthless, who accept the brutal living conditions as the price of a great hiding place. The street layout is complex.", + "description": "You are in a sad little shop called Froglover's Pizza, which appears to have a dearth of customers. To the south you see a public data terminal. To the west you see the Via Dolorosa.", "exits": { "north": null, - "south": null, - "west": 2100, - "east": 162 + "south": 10028, + "east": null, + "west": 10046 }, - "_gridCol": 15, - "_gridRow": 17, - "icon": 48, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Montenegro/Wasteland/1.png" }, { "roomId": 162, - "_name": "Jungle", - "description": "You are in a bar. To the north you see Confederation Ave. To the south you see a public data terminal.", + "name": "Jungle", "type": "arena", "sector": "cathay", - "icon": 31, + "districtIndex": 4, + "districtName": "Cathay", + "x": 240, + "y": 246, + "labelW": 37, + "labelH": 18, + "isSector": false, + "mapDescription": "The Jungle rises above the Cathay slums, a tribute to the greatness of House Liao amid the pathetic tenements of the teeming masses. Its lush interior is in stark contrast to the intimidating bulk of this massive ferrocrete pyramidal stadium.. The street layout is very simple.", + "description": "You are in a bar. To the north you see Confederation Ave. To the south you see a public data terminal.", "exits": { - "north": 2118, - "south": 2032, - "west": 2099, - "east": 2098 + "north": 10050, + "south": 10028, + "east": null, + "west": null }, - "_gridCol": 16, - "_gridRow": 17, - "clientMapDescription": true + "_source": "solaris_map+screenshot", + "_screenshot": "Cathay/Jungle/1.png" }, { - "roomId": 2098, - "type": "street", + "roomId": 163, + "name": "Chancellor's Quarter", + "type": "terminal", "sector": "cathay", - "exits": { - "north": null, - "south": null, - "west": 162, - "east": 2097 + "districtIndex": 4, + "districtName": "Cathay", + "x": 196, + "y": 366, + "labelW": 68, + "labelH": 29, + "isSector": false, + "mapDescription": "A stark contrast to the slums below, the Chancellor's Quarter is home to Cathay's elite. Boasting the highest standard of living in Solaris City, gleaming towers and rambling estates are the rule here. The street layout is simple.", + "description": "You are in Warrior's Hall. To the south you see Maximillian Ave. To the east you see a public data terminal.", + "exits": { + "north": null, + "south": 10052, + "east": 10028, + "west": null }, - "_gridCol": 17, - "_gridRow": 17, - "icon": 57, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Cathay/Chancellor's Quarter/1.png" }, { - "roomId": 2097, + "roomId": 164, + "name": "Middletown", "type": "street", "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": 228, + "y": 289, + "labelW": 62, + "labelH": 15, + "isSector": false, + "mapDescription": "The middle class of Cathay enjoy a relatively comfortable existence here, positioned between the crime and poverty of the slums and the wealth and influence of the upper-class Liaoists. The street layout is of average complexity.", + "description": "You are in The Bitter Pool, where bitterness is not just a state of mind, but a way of life. Whenever you get bitter, come down to The Bitter Pool. To the east you see Solaris Highway One. To the west you see a public data terminal.", "exits": { "north": null, "south": null, - "west": 2098, - "east": null + "east": 10051, + "west": 10028 }, - "_gridCol": 18, - "_gridRow": 17, - "icon": 57, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Cathay/Middletown/1.png" }, { - "roomId": 2096, + "roomId": 165, + "name": "Rivertown", "type": "street", - "sector": "silesia", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": 236, + "y": 187, + "labelW": 61, + "labelH": 15, + "isSector": false, + "mapDescription": "The lowest level of the Cathay slums lies along this filthy stretch of water, and the houseboats and shacks on rickety piers frequently crowd as many as ten adults into a single cramped room. The street layout is of average complexity.", + "description": "You are in the Cobalt Coil. To the east you see Capellan St. To the west you see a public data terminal.", "exits": { "north": null, "south": null, - "west": null, - "east": 2095 + "east": 10053, + "west": 10028 }, - "_gridCol": 19, - "_gridRow": 17, - "icon": 46, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Cathay/Rivertown/1.png" }, { - "roomId": 2095, + "roomId": 166, + "name": "Maze", "type": "street", - "sector": "silesia", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": 251, + "y": 213, + "labelW": 30, + "labelH": 15, + "isSector": false, + "mapDescription": "The absolute center of Cathay's crime and poverty, this twisting labyrinth of streets, burned-out storefronts, decayed shacks, tottering tenements, bars and opium dens is home to many of the area's major gangs and crime families. The street layout is complex.", + "description": "You are in Belthasar's Hideaway, where you had better keep looking over your shoulder and keep your back to the wall. To the north you see a public data terminal. To the south you see Confederation Ave. running east and west.", "exits": { - "north": null, - "south": null, - "west": 2096, - "east": 152 + "north": 10028, + "south": 10050, + "east": null, + "west": null }, - "_gridCol": 20, - "_gridRow": 17, - "icon": 52, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Cathay/Maze/1.png" }, { - "roomId": 152, - "_name": "Steiner Stadium", - "description": "You are in a bar called the Copper Coin. To the east you see Dusseldorf Street. To the west you see a public data terminal.", + "roomId": 167, + "name": "Davion Arena", "type": "arena", - "sector": "silesia", - "icon": 29, + "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": 159, + "y": 224, + "labelW": 37, + "labelH": 29, + "isSector": false, + "mapDescription": "The Davion Arena is the largest and most sophisticated Arena on Solaris, seating half again as many as its nearest rival. This ultra-modern edifice towers over the defunct industrial park in which it was built. The street layout is very simple.", + "description": "You are in The Sun and Sword, one of the older bars in the city. To the east you see Halloran St. To the west you see a public data terminal.", "exits": { "north": null, - "south": 2021, - "west": 2095, - "east": 2027 + "south": null, + "east": 10055, + "west": 10028 }, - "_gridCol": 21, - "_gridRow": 17, - "clientMapDescription": true + "_source": "solaris_map+screenshot", + "_screenshot": "Black Hills/Davion Arena/1.png" }, { "roomId": 168, - "_name": "Sortek Building", - "description": "You are an exclusive establishment called Seventh Heaven. To the south you see Hanse Davion Ave. To the east you see a public data terminal.", + "name": "Sortek Building", "type": "terminal", "sector": "blackhills", - "icon": 17, + "districtIndex": 5, + "districtName": "Black Hills", + "x": 46, + "y": 266, + "labelW": 43, + "labelH": 32, + "isSector": false, + "mapDescription": "The ugly brick Sortek Building characterizes much of this district, heavy on administrative muscle and rather short on style. The street layout is simple.", + "description": "You are an exclusive establishment called Seventh Heaven. To the south you see Hanse Davion Ave. To the east you see a public data terminal.", "exits": { "north": null, - "south": 2071, - "west": null, - "east": 2143 + "south": 10056, + "east": 10028, + "west": null }, - "_gridCol": 5, - "_gridRow": 18, - "clientMapDescription": true + "_source": "solaris_map+screenshot", + "_screenshot": "Black Hills/Sortek Building/1.png" }, { - "roomId": 2143, - "type": "street", + "roomId": 169, + "name": "Guzman Park", + "type": "park", "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": 109, + "y": 252, + "labelW": 43, + "labelH": 29, + "isSector": false, + "mapDescription": "The centerpiece of the Black Hills residential district, the winding trails and sports fields of this heavily forested park become virtual battlegrounds after dark as slum gangs fight their turf wars. The street layout is of average complexity.", + "description": "You are in Skippy's Fern Bar. A classy joint where warriors meet to drink and tell lies about their battles. Skippy's is known for serving the best. To the south you see Swift Shore Drive. To the west you see a public data terminal.", "exits": { - "north": 2144, - "south": null, - "west": 168, - "east": 2069 + "north": null, + "south": 10057, + "east": null, + "west": 10028 }, - "_gridCol": 6, - "_gridRow": 18, - "icon": 52, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Black Hills/Guzman Park/1.png" }, { - "roomId": 2069, - "type": "street", + "roomId": 170, + "name": "Marina", + "type": "bar", "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": 99, + "y": 170, + "labelW": 39, + "labelH": 15, + "isSector": false, + "mapDescription": "This opulent district is the resort of many wealthy Davions, who keep their yachts (or floating palaces) here for parties and getaways. The street layout is of average complexity.", + "description": "You are in the Pelican. To the north you see Frances Ave. To the south you see a public data terminal.", "exits": { - "north": 169, - "south": 2068, - "west": 2143, - "east": null - }, - "_gridCol": 8, - "_gridRow": 18, - "icon": 53, - "_name": "Swift Shore Drive" - }, - { - "roomId": 2037, - "type": "street", - "sector": "silesia", - "exits": { - "north": 2038, - "south": 2036, - "west": null, - "east": null - }, - "_gridCol": 11, - "_gridRow": 18, - "icon": 46, - "_name": "Road" - }, - { - "roomId": 2032, - "type": "street", - "sector": "cathay", - "exits": { - "north": 162, - "south": 164, - "west": null, - "east": null - }, - "_gridCol": 16, - "_gridRow": 18, - "icon": 57, - "_name": "Road" - }, - { - "roomId": 2021, - "type": "street", - "sector": "silesia", - "exits": { - "north": 152, - "south": 2020, - "west": null, - "east": null + "north": 10058, + "south": 10028, + "east": null, + "west": null }, - "_gridCol": 21, - "_gridRow": 18, - "icon": 53, - "_name": "Road" + "_source": "solaris_map+screenshot", + "_screenshot": "Black Hills/Marina/1.png" }, { - "roomId": 2071, + "roomId": 171, + "name": "Viewpoint", "type": "street", "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": 67, + "y": 214, + "labelW": 55, + "labelH": 16, + "isSector": false, + "mapDescription": "This magnificent vantage point looks out over the whole of Solaris City, and plaques and memorials point out the sights to the masses of tourists. The street layout is complex.", + "description": "You are in the Cryptic Skeptic, though probably not for too much longer...considering. To the north you see a public data terminal. To the west you see a busy pedestrian interchange.", "exits": { - "north": 168, - "south": null, - "west": null, - "east": null + "north": 10028, + "south": 10025, + "east": null, + "west": 10059 }, - "_gridCol": 5, - "_gridRow": 19, - "icon": 53, - "_name": "Hanse Davion Ave" + "_source": "solaris_map+screenshot", + "_screenshot": "Black Hills/Viewpoint/1.png", + "_notes": "South exit 10025 set by reverse-exit rule: Keith's House of Mummery (10025) north exit references 'Viewpoint, just off of Laurel Ave.' (roomId 171)." }, { - "roomId": 2068, - "type": "street", - "sector": "blackhills", - "exits": { - "north": 2069, - "south": 2067, - "west": null, - "east": null + "roomId": 10001, + "name": "Solaris Starport Arrivals Area", + "type": "terminal", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "Click on the stone arch icon to the east for a tutorial if you are new to Solaris City and need help. Click on the tram icon to the south if you know your way around and you're ready to go find a fight. Remember, players with [MPBT] in front of their names are the game staff. They will help to teach you the game. Other players that want to train you or that want you to join their group may not have the best intentions.", + "exits": { + "north": null, + "south": 10010, + "east": 10002, + "west": null }, - "_gridCol": 8, - "_gridRow": 19, - "icon": 52, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "International Zone/2.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 2036, + "roomId": 10002, + "name": "Solaris Tutorial", "type": "street", - "sector": "silesia", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "Using this tutorial, you will move from one location to another and learn about the game. Each location explains a different feature. At the end of the tutorial you can leave this sector to go find a battle. +++ The five icons in the lower left corner of the screen represent locations. The center icon shows your current location. The other four icons show locations nearby to the north, south, east and west. +++ Click on the street icon to the east to continue.", "exits": { - "north": 2037, + "north": null, "south": null, - "west": null, - "east": null + "east": 10003, + "west": 10001 }, - "_gridCol": 11, - "_gridRow": 19, - "icon": 51, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "International Zone/3.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 164, - "_name": "Middletown", - "description": "You are in The Bitter Pool, where bitterness is not just a state of mind, but a way of life. Whenever you get bitter, come down to The Bitter Pool. To the east you see Solaris Highway One. To the west you see a public data terminal.", + "roomId": 10003, + "name": "West Training Street", "type": "street", - "sector": "cathay", - "icon": 57, + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "You are standing in a street location. Players use the streets to move from one location to another within a sector. You can chat with other players where you are in any location by entering text in the chat buffer just above the help button. Click on the street to the east to continue.", "exits": { - "north": 2032, - "south": 2016, - "west": null, - "east": null + "north": null, + "south": null, + "east": 10004, + "west": 10002 }, - "_gridCol": 16, - "_gridRow": 19, - "clientMapDescription": true + "_source": "screenshot", + "_screenshot": "International Zone/4.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 2020, + "roomId": 10004, + "name": "East Training Street", "type": "street", - "sector": "silesia", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "Click on the center street icon, your current location, for a list of all players at this location. From there, click on \"All\" to see a list of all players; you can see a player's score or send a message by clicking on that player in the list. Click on the street to the east to continue.", "exits": { - "north": 2021, - "south": 3, - "west": null, - "east": null + "north": null, + "south": null, + "east": 10005, + "west": 10003 }, - "_gridCol": 21, - "_gridRow": 19, - "icon": 47, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "International Zone/5.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 2070, + "roomId": 10005, + "name": "Training Street", "type": "street", "sector": "international", - "exits": { - "north": null, - "south": 2066, - "west": null, - "east": null + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "Click on the Bar icon to the north to enter the ComStar Headquarters bar. Click on the Davion Arena icon to the south to enter the training arena. After looking at these areas, return here to continue the tutorial. Then, click on the street icon to the east to learn about traveling between sectors.", + "exits": { + "north": 10006, + "south": 10007, + "east": 10010, + "west": 10004 + }, + "_source": "screenshot", + "_screenshot": "International Zone/6.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." + }, + { + "roomId": 10006, + "name": "ComStar Headquarters", + "type": "bar", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "The place for game staff to meet and help new players. The place to find answers to all of your questions. ComStar Headquarters has a little something for everyone. ComStar members come here to relax. MechWarriors come here to hang out with the coolest group on Solaris.", + "exits": { + "north": null, + "south": 10005, + "east": null, + "west": null }, - "_gridCol": 5, - "_gridRow": 20, - "icon": 51, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "International Zone/7.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 2067, - "type": "street", - "sector": "blackhills", - "exits": { - "north": 2068, - "south": 6, - "west": null, - "east": null + "roomId": 10007, + "name": "Davion Training Arena: Reception Area", + "type": "arena", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "This is the outer room of a Mech dueling arena. Each district on Solaris has five arenas; find them by traveling to a district, then walking around its streets. In each district, arenas 1 and 2 are sanctioned scoring arenas. +++ Click on the metal doors icon to the east to enter the Ready Room.", + "exits": { + "north": 10005, + "south": null, + "east": 10008, + "west": null }, - "_gridCol": 8, - "_gridRow": 20, - "icon": 53, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "International Zone/8.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 2035, - "type": "street", - "sector": "blackhills", + "roomId": 10008, + "name": "Davion Training Arena: Ready Room", + "type": "arena", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "This is a simulation of a working Ready Room. An actual Ready Room has additional buttons to choose your Mech and Side; a button to see the current Status of all participants; and a description of the arena's climate. You would click on the mech icon to the east when ready to drop into battle. If alone, you will face droid opponents; if other players are present, you will battle them when all are ready. +++ Click east to continue.", "exits": { "north": null, "south": null, - "west": null, - "east": null + "east": 10009, + "west": 10007 }, - "_gridCol": 11, - "_gridRow": 20, - "icon": 53, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "International Zone/9.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 2034, - "type": "street", - "sector": "cathay", + "roomId": 10009, + "name": "Dropping Into Battle", + "type": "arena", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "When everyone has clicked on the drop icon to show they're ready, a DropShip will take you to the battlefield. Once all Mechs are in place, the battle begins. When in your Mech, press and hold F1 for a list of all keyboard commands. +++ You can't drop into a real combat here; you must take the tram to another district on Solaris. Return to the street outside the arena, then head east for instructions for the tram.", "exits": { "north": null, "south": null, - "west": null, - "east": 2033 + "east": null, + "west": 10008 }, - "_gridCol": 12, - "_gridRow": 20, - "icon": 48, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "International Zone/10.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 2033, - "type": "street", - "sector": "cathay", + "roomId": 10010, + "name": "Tram Station", + "type": "terminal", + "sector": "international", + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 146, + "mapDescription": null, + "description": "Click on the Tram icon to the east for a subway map of Solaris City. The map shows sectors in white and districts in color. From the map, click on a district name and then on \"Travel\" to travel to that district. (If you do this, you will exit this tutorial; take the tram back to the \"Solaris Starport\" district to return to the tutorial.) +++ The Jungle, The Factory and Steiner Stadium have easier street layouts, and are recommended for new warriors.", "exits": { "north": null, "south": null, - "west": 2034, - "east": 2018 + "east": 10062, + "west": 10005 }, - "_gridCol": 13, - "_gridRow": 20, - "icon": 48, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "International Zone/11.png", + "_notes": "IZ tutorial room; exits inferred from icon grid and sequential flow (non-standard 'click icon' format, not 'To the X you see Y')." }, { - "roomId": 2018, - "type": "street", - "sector": "cathay", - "exits": { - "north": null, - "south": 2019, - "west": 2033, - "east": 2017 + "roomId": 10011, + "name": "Hodie's Hideaway", + "type": "bar", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 151, + "mapDescription": null, + "description": "You are in Hodie's Hideaway, where Lisa, the chef, will rock your culinary world. To the south you see a data terminal. To the east you see North Theodore Kurita Street.", + "exits": { + "north": null, + "south": 10028, + "east": 10034, + "west": null }, - "_gridCol": 14, - "_gridRow": 20, - "icon": 49, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Kobe/Kobe Slums/2.png" }, { - "roomId": 2017, - "type": "street", - "sector": "cathay", - "exits": { - "north": null, - "south": null, - "west": 2018, - "east": 2016 + "roomId": 10012, + "name": "The Iron Throne", + "type": "bar", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 151, + "mapDescription": null, + "description": "You are in The Iron Throne, where equanimous patrons are sipping drinks and watching fight telecasts. To the north you see a data terminal. To the south you see a set of marble steps sweeping up towards the Snowbird.", + "exits": { + "north": 10028, + "south": 149, + "east": null, + "west": null }, - "_gridCol": 15, - "_gridRow": 20, - "icon": 57, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Kobe/Kobe Slums/3.png", + "_notes": "South exit: non-standard phrase. 'The Snowbird' resolved to roomId 149 (White Lotus). Reverse exit conflict: 149 north = 10031, not this room." }, { - "roomId": 2016, - "type": "street", - "sector": "cathay", - "exits": { - "north": 164, - "south": 2063, - "west": 2017, - "east": 2015 + "roomId": 10013, + "name": "Solrao's House of Death", + "type": "bar", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 156, + "mapDescription": null, + "description": "You are in Solrao's House of Death. Graphic memorials to the great fallen MechWarriors cover the walls. To the north you see a public data terminal. To the south you see the end of Arnulf St., south of Solrao's House of Death.", + "exits": { + "north": 10028, + "south": 10040, + "east": null, + "west": null }, - "_gridCol": 16, - "_gridRow": 20, - "icon": 49, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Silesia/Black Thorne/2.png" }, { - "roomId": 2015, - "type": "street", - "sector": "cathay", - "exits": { - "north": null, + "roomId": 10014, + "name": "Y.B.Y.A.", + "type": "bar", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 156, + "mapDescription": null, + "description": "You are in Y.B.Y.A., amongst about 30 drunken MechWarriors raucously betting on the latest fights. Shades, the owner, is watching you. To the north you see a public data terminal. To the west you see the administrative center of Silesia Sector.", + "exits": { + "north": 10028, "south": null, - "west": 2016, - "east": null + "east": null, + "west": 10041 }, - "_gridCol": 17, - "_gridRow": 20, - "icon": 48, - "_name": "Solaris Highway One" + "_source": "screenshot", + "_screenshot": "Silesia/Black Thorne/3.png" }, { - "roomId": 2014, - "type": "street", + "roomId": 10015, + "name": "Uncle Skippy's House of Madness", + "type": "bar", "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 156, + "mapDescription": null, + "description": "You are endangering your sanity! Leave while you can! To the east you see North Dusseldorf St. To the west you see a public data terminal.", "exits": { "north": null, "south": null, - "west": null, - "east": 2013 + "east": 10042, + "west": 10028 }, - "_gridCol": 18, - "_gridRow": 20, - "icon": 46, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Silesia/Black Thorne/4.png" }, { - "roomId": 2013, - "type": "street", + "roomId": 10016, + "name": "Stimpy's Bathhouse", + "type": "bar", "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 156, + "mapDescription": null, + "description": "You are in Stimpy's Bathhouse, and the rumors about the gang wars in the hot tubs are totally false. Really. To the east you see a public data terminal. To the west you see a dark and forbidding alley.", "exits": { "north": null, "south": null, - "west": 2014, - "east": 2012 + "east": 10028, + "west": 10043 }, - "_gridCol": 19, - "_gridRow": 20, - "icon": 53, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Silesia/Black Thorne/5.png" }, { - "roomId": 2012, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, - "south": 2029, - "west": 2013, - "east": 3 + "roomId": 10017, + "name": "Shizuka's House of Pain", + "type": "bar", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 161, + "mapDescription": null, + "description": "You are sure you understand the bar's name after having your first drink. To the north you see Bolivar St., a small promenade closed to motor traffic. To the west you see a public data terminal.", + "exits": { + "north": 10045, + "south": null, + "east": null, + "west": 10028 }, - "_gridCol": 20, - "_gridRow": 20, - "icon": 48, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Montenegro/Wasteland/2.png" }, { - "roomId": 3, - "_name": "Silesia Sector", - "description": "Despite official attempts to maintain Silesia as a showpiece of Lyran culture, it suffers the same problems as the rest of the City. The beautiful private estates of the Sector are frequently financed by the very crime that devastates its poorer areas. The fixed arenas in Silesia use temperate terrain.", - "type": "sector", - "sector": "silesia", - "icon": 34, + "roomId": 10018, + "name": "The Bubbling Kettel", + "type": "bar", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 161, + "mapDescription": null, + "description": "You are in The Bubbling Kettel, where Greg, the jolly proprieter, is constantly and mercilessly cheerful. To the east you see West Bolivar St. To the west you see a public data terminal.", "exits": { - "north": 2020, + "north": null, "south": null, - "west": 2012, - "east": 2022 + "east": 10048, + "west": 10028 }, - "_gridCol": 21, - "_gridRow": 20, - "clientMapDescription": true + "_source": "screenshot", + "_screenshot": "Montenegro/Wasteland/3.png" }, { - "roomId": 2022, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, + "roomId": 10019, + "name": "Boot's House of Boot", + "type": "bar", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 161, + "mapDescription": null, + "description": "You are in Boot's House of Boot, where 'boot' is the word, Man. Can you dig? Yeah, have another beer. To the north you see the thick of Montenegro's nightlife. To the west you see a public data terminal.", + "exits": { + "north": 10049, "south": null, - "west": 3, - "east": 2023 + "east": null, + "west": 10028 }, - "_gridCol": 22, - "_gridRow": 20, - "icon": 57, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Montenegro/Wasteland/4.png" }, { - "roomId": 2023, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, - "south": null, - "west": 2022, - "east": 2024 + "roomId": 10020, + "name": "The Green Sausage", + "type": "bar", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 166, + "mapDescription": null, + "description": "You are in The Green Sausage, surrounded by camaraderie and good cheer (and full-scale bar brawls, but mostly camaraderie and good cheer). To the south you see a public data terminal. To the east you see more of Solaris Highway One.", + "exits": { + "north": null, + "south": 10028, + "east": 10051, + "west": null }, - "_gridCol": 23, - "_gridRow": 20, - "icon": 57, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Cathay/Maze/2.png" }, { - "roomId": 2024, - "type": "street", - "sector": "silesia", + "roomId": 10021, + "name": "Brass Lily", + "type": "bar", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 166, + "mapDescription": null, + "description": "You are in the Brass Lily. Roaches check in, but they don't check out. To the east you see a public data terminal. To the west you see more of Solaris Highway One.", "exits": { "north": null, "south": null, - "west": 2023, - "east": 2025 + "east": 10028, + "west": 10051 }, - "_gridCol": 24, - "_gridRow": 20, - "icon": 57, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Cathay/Maze/3.png" }, { - "roomId": 2025, - "type": "street", - "sector": "silesia", - "exits": { - "north": null, - "south": 2026, - "west": 2024, - "east": null + "roomId": 10022, + "name": "The Suspenseful Turkey", + "type": "bar", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 166, + "mapDescription": null, + "description": "You are The Suspenseful Turkey, surrounded by sullen and intoxicated local residents. You don't feel cherished. To the north you see a public data terminal. To the west you see Solaris Highway One continuing.", + "exits": { + "north": 10028, + "south": null, + "east": null, + "west": 10051 }, - "_gridCol": 25, - "_gridRow": 20, - "icon": 45, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Cathay/Maze/4.png" }, { - "roomId": 146, - "_name": "Solaris Starport", - "description": "Click on the stone arch icon to the east for a tutorial if you are new to Solaris City and need help. Click on the tram icon to the south if you know your way around and you're ready to go find a fight. Remember players with [MPBT] in front of their names are the game staff. They will help to teach you the game. Other players that want to train you or that want you to join their group may not have the best intentions.", - "type": "hub", - "sector": "international", - "icon": 4, + "roomId": 10023, + "name": "The Rampaging Squidherd", + "type": "bar", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 166, + "mapDescription": null, + "description": "You are in a filthy little hole called The Rampaging Squidherd, which caters to the basest of tastes. To the east you see Solaris Highway One, receding into the distance. To the west you see a public data terminal.", "exits": { "north": null, - "south": 2002, - "west": null, - "east": 2066 + "south": null, + "east": 10051, + "west": 10028 }, - "_gridCol": 4, - "_gridRow": 21, - "clientMapDescription": true + "_source": "screenshot", + "_screenshot": "Cathay/Maze/5.png" }, { - "roomId": 2066, - "type": "street", - "sector": "international", - "exits": { - "north": 2070, - "south": null, - "west": 146, - "east": null + "roomId": 10024, + "name": "Rule #6", + "type": "bar", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 166, + "mapDescription": null, + "description": "You are sure Rule #6 was here, but you can't seem to find it. Too bad, Bruce. To the north you see a public data terminal. To the south you see an alley off of Maximillian Ave.", + "exits": { + "north": 10028, + "south": 10054, + "east": null, + "west": null }, - "_gridCol": 5, - "_gridRow": 21, - "icon": 52, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Cathay/Maze/6.png" }, { - "roomId": 2065, - "type": "street", + "roomId": 10025, + "name": "Keith's House of Mummery", + "type": "bar", "sector": "blackhills", - "exits": { - "north": null, + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 171, + "mapDescription": null, + "description": "You are in Keith's House of Mummery. The tranquil atmosphere could almost lull you to sleep. To the north you see Viewpoint, just off of Laurel Ave. To the west you see a public data terminal.", + "exits": { + "north": 171, "south": null, - "west": null, - "east": 2064 + "east": null, + "west": 10028 }, - "_gridCol": 6, - "_gridRow": 21, - "icon": 53, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Black Hills/Viewpoint/2.png" }, { - "roomId": 2064, - "type": "street", + "roomId": 10026, + "name": "The Mystery Meat House", + "type": "bar", "sector": "blackhills", - "exits": { - "north": null, - "south": null, - "west": 2065, - "east": 6 + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 171, + "mapDescription": null, + "description": "You are a fool, but a starving one, apparently. You are served a moderately quiescent slab of unidentifiable meat in an uncertain sauce. To the north you see East Frances Ave. To the south you see a public data terminal.", + "exits": { + "north": 10060, + "south": 10028, + "east": null, + "west": null }, - "_gridCol": 7, - "_gridRow": 21, - "icon": 52, - "_name": "Road" + "_source": "screenshot", + "_screenshot": "Black Hills/Viewpoint/3.png" }, { - "roomId": 6, - "_name": "Black Hills Sector", - "description": "The Davion sector presents itself as a high-tech bastion of civilization and virtue, but squalor and crime are just as prevalent here as anywhere in the city. The fixed arenas in Black Hills use arctic terrain.", - "type": "sector", + "roomId": 10027, + "name": "Yarmoor's House of Doom", + "type": "bar", "sector": "blackhills", - "icon": 37, + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "parentRoomId": 171, + "mapDescription": null, + "description": "You are at the bar with a tough looking crowd. Yarmoor cleans glasses and makes faces at the patrons. To the south you see a public data terminal. To the west you see the south end of Hanse Davion Dr.", + "exits": { + "north": null, + "south": 10028, + "east": null, + "west": 10061 + }, + "_source": "screenshot", + "_screenshot": "Black Hills/Viewpoint/4.png" + }, + { + "roomId": 10028, + "name": "Public Data Terminal", + "type": "terminal", + "sector": null, + "districtIndex": null, + "districtName": null, + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2067, + "north": null, "south": null, - "west": 2064, - "east": null + "east": null, + "west": null }, - "_gridCol": 8, - "_gridRow": 21, - "clientMapDescription": true + "_source": "stub", + "_screenshot": null }, { - "roomId": 2019, - "type": "street", - "sector": "cathay", + "roomId": 10029, + "name": "Theodore Kurita St.", + "type": "stub", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2018, - "south": 5, - "west": null, - "east": null + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 14, - "_gridRow": 21, - "icon": 49, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2063, - "type": "street", - "sector": "cathay", + "roomId": 10030, + "name": "Yamato St.", + "type": "stub", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2016, - "south": 2058, - "west": null, - "east": null + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 16, - "_gridRow": 21, - "icon": 48, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2029, - "type": "street", - "sector": "silesia", + "roomId": 10031, + "name": "Buckminster Circle", + "type": "stub", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2012, + "north": null, "south": null, - "west": null, - "east": null + "east": null, + "west": null }, - "_gridCol": 20, - "_gridRow": 21, - "icon": 46, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2026, - "type": "street", - "sector": "silesia", + "roomId": 10032, + "name": "Robert Kurita St.", + "type": "stub", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2025, - "south": 154, - "west": null, - "east": null + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 25, - "_gridRow": 21, - "icon": 47, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2002, - "type": "street", - "sector": "international", + "roomId": 10033, + "name": "Yamato and Proserpina St.", + "type": "stub", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 146, - "south": 2001, - "west": null, - "east": null + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 4, - "_gridRow": 22, - "icon": 47, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 5, - "_name": "Cathay Sector", - "description": "Cathay displays the sharpest contrasts between the classes. A large body of what are essentially working-class serfs maintain the luxurious splendor of the small upper class. Crime and gang activity is rampant, despite the ruthless efficiency of Cathay Security. The fixed arenas in Cathay use temperate and tropical terrain.", - "type": "sector", - "sector": "cathay", - "icon": 36, + "roomId": 10034, + "name": "North Theodore Kurita Street", + "type": "stub", + "sector": "kobe", + "districtIndex": 1, + "districtName": "Kobe", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2019, + "north": null, "south": null, - "west": null, - "east": 2057 + "east": null, + "west": null }, - "_gridCol": 14, - "_gridRow": 22, - "clientMapDescription": true + "_source": "stub", + "_screenshot": null }, { - "roomId": 2057, - "type": "street", - "sector": "cathay", + "roomId": 10035, + "name": "Dusseldorf Street", + "type": "stub", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 5, - "east": 2058 + "east": null, + "west": null }, - "_gridCol": 15, - "_gridRow": 22, - "icon": 49, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2058, - "type": "street", - "sector": "cathay", + "roomId": 10036, + "name": "Arnulf St.", + "type": "stub", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2063, - "south": 2128, - "west": 2057, - "east": 2059 + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 16, - "_gridRow": 22, - "icon": 49, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2059, - "type": "street", - "sector": "cathay", + "roomId": 10037, + "name": "Vialla Circle", + "type": "stub", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 2058, - "east": 2060 + "east": null, + "west": null }, - "_gridCol": 17, - "_gridRow": 22, - "icon": 48, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2060, - "type": "street", - "sector": "cathay", + "roomId": 10038, + "name": "Fraunstah St.", + "type": "stub", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 2059, - "east": 2061 + "east": null, + "west": null }, - "_gridCol": 18, - "_gridRow": 22, - "icon": 57, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2061, - "type": "street", - "sector": "cathay", + "roomId": 10039, + "name": "Arnulf St. and Marburg St.", + "type": "stub", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 2060, - "east": 2030 + "east": null, + "west": null }, - "_gridCol": 19, - "_gridRow": 22, - "icon": 48, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2030, - "type": "street", - "sector": "cathay", + "roomId": 10040, + "name": "Arnulf St. (South End)", + "type": "stub", + "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 2061, - "east": null + "east": null, + "west": null }, - "_gridCol": 20, - "_gridRow": 22, - "icon": 48, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2109, - "type": "street", + "roomId": 10041, + "name": "Administrative Center of Silesia Sector", + "type": "stub", "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": null, - "east": 2108 + "east": null, + "west": null }, - "_gridCol": 21, - "_gridRow": 22, - "icon": 45, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2108, - "type": "street", + "roomId": 10042, + "name": "North Dusseldorf St.", + "type": "stub", "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 2109, - "east": 2107 + "east": null, + "west": null }, - "_gridCol": 22, - "_gridRow": 22, - "icon": 46, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2107, - "type": "street", + "roomId": 10043, + "name": "Dark and Forbidding Alley", + "type": "stub", "sector": "silesia", + "districtIndex": 2, + "districtName": "Silesia", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 2108, - "east": 2106 + "east": null, + "west": null }, - "_gridCol": 23, - "_gridRow": 22, - "icon": 46, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2106, - "type": "street", - "sector": "silesia", + "roomId": 10044, + "name": "Montenegro Street", + "type": "stub", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 2107, - "east": 154 + "east": null, + "west": null }, - "_gridCol": 24, - "_gridRow": 22, - "icon": 52, - "_name": "Vialla Circle" + "_source": "stub", + "_screenshot": null }, { - "roomId": 154, - "_name": "Chahar Park", - "description": "You are in Thor's Shieldhall. To the south you see a public data terminal. To the west you see Vialla Circle.", - "type": "street", - "sector": "silesia", - "icon": 45, + "roomId": 10045, + "name": "Bolivar St.", + "type": "stub", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2026, + "north": null, "south": null, - "west": 2106, - "east": null + "east": null, + "west": null }, - "_gridCol": 25, - "_gridRow": 22, - "clientMapDescription": true + "_source": "stub", + "_screenshot": null }, { - "roomId": 2001, - "type": "street", - "sector": "international", + "roomId": 10046, + "name": "Via Dolorosa", + "type": "stub", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2002, - "south": 2000, - "west": null, - "east": null + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 4, - "_gridRow": 23, - "icon": 57, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2062, - "type": "street", - "sector": "cathay", + "roomId": 10047, + "name": "Reisman St.", + "type": "stub", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 163, + "north": null, "south": null, - "west": null, - "east": null + "east": null, + "west": null }, - "_gridCol": 14, - "_gridRow": 23, - "icon": 57, - "_name": "Maximilian Ave" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2128, - "type": "street", - "sector": "cathay", + "roomId": 10048, + "name": "West Bolivar St.", + "type": "stub", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2058, - "south": 2127, - "west": null, - "east": null + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 16, - "_gridRow": 23, - "icon": 48, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2031, - "type": "street", - "sector": "silesia", + "roomId": 10049, + "name": "Montenegro's Nightlife", + "type": "stub", + "sector": "montenegro", + "districtIndex": 3, + "districtName": "Montenegro", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, - "south": 156, - "west": null, - "east": null + "south": null, + "east": null, + "west": null }, - "_gridCol": 20, - "_gridRow": 23, - "icon": 48, - "_name": "Arnulf St" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2000, - "type": "street", - "sector": "international", + "roomId": 10050, + "name": "Confederation Ave.", + "type": "stub", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2001, - "south": 1, - "west": null, - "east": null + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 4, - "_gridRow": 24, - "icon": 55, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 163, - "_name": "Chancellor's Quarter", - "description": "You are in Warrior's Hall. To the south you see Maximilian Ave. To the east you see a public data terminal.", - "type": "terminal", + "roomId": 10051, + "name": "Solaris Highway One", + "type": "stub", "sector": "cathay", - "icon": 45, + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, - "south": 2062, - "west": null, - "east": null + "south": null, + "east": null, + "west": null }, - "_gridCol": 14, - "_gridRow": 24, - "clientMapDescription": true + "_source": "stub", + "_screenshot": null }, { - "roomId": 2127, - "type": "street", + "roomId": 10052, + "name": "Maximillian Ave.", + "type": "stub", "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2128, + "north": null, "south": null, - "west": null, - "east": null + "east": null, + "west": null }, - "_gridCol": 16, - "_gridRow": 24, - "icon": 48, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2126, - "type": "street", - "sector": "silesia", + "roomId": 10053, + "name": "Capellan St.", + "type": "stub", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": null, - "east": 2125 + "east": null, + "west": null }, - "_gridCol": 17, - "_gridRow": 24, - "icon": 48, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2125, - "type": "street", - "sector": "silesia", + "roomId": 10054, + "name": "Alley off Maximillian Ave.", + "type": "stub", + "sector": "cathay", + "districtIndex": 4, + "districtName": "Cathay", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": 2126, - "east": 2124 + "east": null, + "west": null }, - "_gridCol": 18, - "_gridRow": 24, - "icon": 48, - "_name": "North Dusseldorf St" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2124, - "type": "street", - "sector": "silesia", + "roomId": 10055, + "name": "Halloran St.", + "type": "stub", + "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 156, + "north": null, "south": null, - "west": 2125, - "east": null + "east": null, + "west": null }, - "_gridCol": 19, - "_gridRow": 24, - "icon": 49, - "_name": "Marburg St" + "_source": "stub", + "_screenshot": null }, { - "roomId": 156, - "_name": "Black Thorne", - "description": "You are just in time. The mysterious \"Deadhead\" cult is having a reading. Plus, some woman named \"Sunshine\" keeps telling you you're \"beautiful.\" To the north you see a public data terminal. To the south you see the intersection of Arnulf St. and Marburg St.", - "type": "street", - "sector": "silesia", - "icon": 48, + "roomId": 10056, + "name": "Hanse Davion Ave.", + "type": "stub", + "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2031, - "south": 2124, - "west": null, - "east": null + "north": null, + "south": null, + "east": null, + "west": null }, - "_gridCol": 20, - "_gridRow": 24, - "clientMapDescription": true + "_source": "stub", + "_screenshot": null }, { - "roomId": 1, - "_name": "International Sector", - "description": "This area, composed primarily of public-works installations and tourist areas, is under the direct control of the Solaris central authority. There are no arenas in this sector.", - "type": "sector", - "sector": "international", - "icon": 6, + "roomId": 10057, + "name": "Swift Shore Drive", + "type": "stub", + "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { - "north": 2000, + "north": null, "south": null, - "west": null, - "east": null + "east": null, + "west": null }, - "_gridCol": 4, - "_gridRow": 25, - "clientMapDescription": true + "_source": "stub", + "_screenshot": null }, { - "roomId": 2056, - "type": "street", - "sector": "montenegro", + "roomId": 10058, + "name": "Frances Ave.", + "type": "stub", + "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "east": 2055, + "east": null, "west": null }, - "_gridCol": 17, - "_gridRow": 13, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2119, - "type": "street", - "sector": "montenegro", + "roomId": 10059, + "name": "Busy Pedestrian Interchange", + "type": "stub", + "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, "east": null, - "west": 2120 + "west": null }, - "_gridCol": 15, - "_gridRow": 14, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2121, - "type": "street", - "sector": "silesia", + "roomId": 10060, + "name": "East Frances Ave.", + "type": "stub", + "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, - "south": 2137, + "south": null, "east": null, - "west": 2122 + "west": null }, - "_gridCol": 13, - "_gridRow": 14, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 2122, - "type": "street", - "sector": "silesia", + "roomId": 10061, + "name": "Hanse Davion Dr.", + "type": "stub", + "sector": "blackhills", + "districtIndex": 5, + "districtName": "Black Hills", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "east": 2121, - "west": 2123 + "east": null, + "west": null }, - "_gridCol": 12, - "_gridRow": 14, - "_name": "Road" + "_source": "stub", + "_screenshot": null }, { - "roomId": 9000, - "_name": "Tram Station", - "description": "Click on the Tram icon for a subway map of Solaris City. The map shows sectors in white and districts in color. From the map, click on a district name and then on \"Travel\" to travel to that district.", - "type": "tram", + "roomId": 10062, + "name": "Tram", + "type": "terminal", "sector": "international", - "icon": 0, + "districtIndex": 0, + "districtName": "International Zone", + "x": null, + "y": null, + "labelW": null, + "labelH": null, + "isSector": false, + "mapDescription": null, + "description": null, "exits": { "north": null, "south": null, - "west": null, - "east": null + "east": null, + "west": null }, - "_gridCol": 4, - "_gridRow": 22 + "_source": "stub", + "_screenshot": null } - ], - "_schema": { - "type": "bar | arena | hub | terminal | bank | street | sector | path | tram", - "description": "Optional lower scene-header text, mirrored from SOLARIS.MAP for stock rooms.", - "clientMapDescription": "Optional boolean. When true, the client should use the room's description in the map/scene header context for stock rooms." - } -} + ] +} \ No newline at end of file From 0dea24a5b0391a446286bff28bb7d282bcd5eb62 Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 15:40:50 -0400 Subject: [PATCH 04/14] feat: add GET /world/rooms to REST API Adds CORS support and a new endpoint: GET /world/rooms -> { ok, rooms: WorldRoom[], source_available } Loads rooms from SOLARIS.MAP via loadSolarisRooms(). Returns an empty array (source_available: false) when the proprietary file is absent so the server still starts cleanly without retail assets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/api.ts b/src/api.ts index 96e28ab..d2f0402 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,34 +1,60 @@ /** * MPBT REST API server — modern client adapter. * - * Provides a lightweight HTTP server on API_PORT (default 3000) for the + * Provides a lightweight HTTP server on API_PORT (default 3002) for the * Godot 4 client. The ARIES TCP protocol (ports 2000/2001) is unaffected. * * Endpoints: - * GET /health → { ok: true, version, name } + * GET /health → { ok: true, version, name } + * GET /world/rooms → { ok: true, rooms: WorldRoom[], source_available: boolean } */ import * as http from 'http'; import { readFileSync } from 'fs'; import { Logger } from './util/logger.js'; +import { loadSolarisRooms } from './data/maps.js'; const _pkg = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf8'), ) as { version: string }; +function setCors(res: http.ServerResponse): void { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); +} + +function jsonOk(res: http.ServerResponse, body: object): void { + const payload = JSON.stringify(body); + setCors(res); + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload), + }); + res.end(payload); +} + export function startApiServer(log: Logger, host: string, port: number): http.Server { const apiLog = log.child('api'); const server = http.createServer((req, res) => { const pathname = req.url?.split('?')[0] ?? '/'; + if (req.method === 'OPTIONS') { + setCors(res); + res.writeHead(204); + res.end(); + return; + } + if (req.method === 'GET' && pathname === '/health') { - const body = JSON.stringify({ ok: true, version: _pkg.version, name: 'mpbt-server' }); - res.writeHead(200, { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(body), - }); - res.end(body); + jsonOk(res, { ok: true, version: _pkg.version, name: 'mpbt-server' }); + return; + } + + if (req.method === 'GET' && pathname === '/world/rooms') { + const rooms = loadSolarisRooms() ?? []; + jsonOk(res, { ok: true, rooms, source_available: rooms.length > 0 }); return; } From 855f1e8f00be8ca369602e3930137d09258f781c Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 16:32:17 -0400 Subject: [PATCH 05/14] M4: add POST /world/travel + GET /world/presence endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/world/presence.ts: PresenceStore — in-memory room occupancy map (travel, remove, getAll, getRoomOccupants); not persisted to DB - src/api.ts: POST /world/travel — validates X-Username header and {roomId} body, calls presenceStore.travel(), returns matching WorldRoom - src/api.ts: GET /world/presence — returns snapshot of all occupied rooms - CORS extended to allow POST method and X-Username request header - http.createServer callback made async to support await readBody() - Added jsonError() helper and readBody() body-reading promise Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 78 ++++++++++++++++++++++++++++++++++++++++--- src/world/presence.ts | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 src/world/presence.ts diff --git a/src/api.ts b/src/api.ts index d2f0402..0d84a3d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -5,14 +5,19 @@ * Godot 4 client. The ARIES TCP protocol (ports 2000/2001) is unaffected. * * Endpoints: - * GET /health → { ok: true, version, name } - * GET /world/rooms → { ok: true, rooms: WorldRoom[], source_available: boolean } + * GET /health → { ok: true, version, name } + * GET /world/rooms → { ok: true, rooms: WorldRoom[], source_available: boolean } + * POST /world/travel → { ok: true, room: WorldRoom | null } + * Body: { roomId: number } + * Header: X-Username (authenticated display name) + * GET /world/presence → { ok: true, rooms: Array<{ roomId, occupants: string[] }> } */ import * as http from 'http'; import { readFileSync } from 'fs'; import { Logger } from './util/logger.js'; import { loadSolarisRooms } from './data/maps.js'; +import { presenceStore } from './world/presence.js'; const _pkg = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf8'), @@ -20,8 +25,8 @@ const _pkg = JSON.parse( function setCors(res: http.ServerResponse): void { res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Username'); } function jsonOk(res: http.ServerResponse, body: object): void { @@ -34,10 +39,29 @@ function jsonOk(res: http.ServerResponse, body: object): void { res.end(payload); } +function jsonError(res: http.ServerResponse, status: number, message: string): void { + const payload = JSON.stringify({ ok: false, error: message }); + setCors(res); + res.writeHead(status, { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload), + }); + res.end(payload); +} + +function readBody(req: http.IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + req.on('data', (chunk: Buffer) => chunks.push(chunk)); + req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + req.on('error', reject); + }); +} + export function startApiServer(log: Logger, host: string, port: number): http.Server { const apiLog = log.child('api'); - const server = http.createServer((req, res) => { + const server = http.createServer(async (req, res) => { const pathname = req.url?.split('?')[0] ?? '/'; if (req.method === 'OPTIONS') { @@ -58,6 +82,50 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se return; } + if (req.method === 'POST' && pathname === '/world/travel') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { + jsonError(res, 400, 'X-Username header required'); + return; + } + let body: string; + try { + body = await readBody(req); + } catch { + jsonError(res, 400, 'failed to read request body'); + return; + } + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch { + jsonError(res, 400, 'invalid JSON body'); + return; + } + const roomId = + parsed !== null && + typeof parsed === 'object' && + 'roomId' in parsed && + typeof (parsed as Record).roomId === 'number' + ? ((parsed as Record).roomId as number) + : NaN; + if (!Number.isFinite(roomId)) { + jsonError(res, 400, 'roomId must be a number'); + return; + } + presenceStore.travel(username, roomId); + apiLog.info('%s traveled to room %d', username, roomId); + const rooms = loadSolarisRooms() ?? []; + const room = rooms.find((r) => r.roomId === roomId) ?? null; + jsonOk(res, { ok: true, room }); + return; + } + + if (req.method === 'GET' && pathname === '/world/presence') { + jsonOk(res, { ok: true, rooms: presenceStore.getAll() }); + return; + } + res.writeHead(404); res.end(); }); diff --git a/src/world/presence.ts b/src/world/presence.ts new file mode 100644 index 0000000..05faeeb --- /dev/null +++ b/src/world/presence.ts @@ -0,0 +1,61 @@ +/** + * In-memory presence store — tracks which room each player is currently in. + * + * Not persisted to DB; cleared on server restart. Sufficient for M4 local + * play; WebSocket push updates replace polling in a later milestone. + */ + +export class PresenceStore { + /** roomId → set of display-name strings currently in that room. */ + private readonly _map = new Map>(); + + /** + * Move `username` to `roomId`. Removes the player from their previous + * room automatically. + */ + travel(username: string, roomId: number): void { + this._removeUser(username); + let occupants = this._map.get(roomId); + if (!occupants) { + occupants = new Set(); + this._map.set(roomId, occupants); + } + occupants.add(username); + } + + /** Ordered list of usernames currently in a specific room. */ + getRoomOccupants(roomId: number): string[] { + return [...(this._map.get(roomId) ?? [])].sort(); + } + + /** + * Snapshot of all rooms that have at least one occupant. + * Rooms with no occupants are omitted. + */ + getAll(): Array<{ roomId: number; occupants: string[] }> { + const out: Array<{ roomId: number; occupants: string[] }> = []; + for (const [roomId, occupants] of this._map) { + if (occupants.size > 0) { + out.push({ roomId, occupants: [...occupants].sort() }); + } + } + return out; + } + + /** Remove a player from whichever room they are in (e.g. on disconnect). */ + remove(username: string): void { + this._removeUser(username); + } + + private _removeUser(username: string): void { + for (const [roomId, occupants] of this._map) { + if (occupants.has(username)) { + occupants.delete(username); + if (occupants.size === 0) this._map.delete(roomId); + return; + } + } + } +} + +export const presenceStore = new PresenceStore(); From 79cd54b13d0c1d40d2a2ae8317a475e94cbf4429 Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 16:56:11 -0400 Subject: [PATCH 06/14] M4: add WebSocket /ws endpoint for real-time presence push - Install ws + @types/ws - src/world/ws_broadcaster.ts: WsBroadcaster class; attaches to the existing HTTP server via handleUpgrade at path /ws; sends current presence snapshot to each newly-connected client; broadcasts presence_update JSON events to all OPEN clients - src/api.ts: import wsBroadcaster; call attach(server) on startup; broadcast presence_update after every successful POST /world/travel Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- package-lock.json | 36 ++++++++++++++++++++++--- package.json | 4 ++- src/api.ts | 5 ++++ src/world/ws_broadcaster.ts | 52 +++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/world/ws_broadcaster.ts diff --git a/package-lock.json b/package-lock.json index b88fe5e..91f156c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,10 @@ "version": "0.1.59", "license": "AGPL-3.0-only", "dependencies": { + "@types/ws": "^8.18.1", "bcryptjs": "^3.0.3", - "pg": "^8.20.0" + "pg": "^8.20.0", + "ws": "^8.20.0" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", @@ -103,7 +105,6 @@ "version": "20.19.39", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -121,6 +122,15 @@ "pg-types": "^2.2.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -386,7 +396,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/v8-compile-cache-lib": { @@ -396,6 +405,27 @@ "dev": true, "license": "MIT" }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "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 + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 0018e3f..304917c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,9 @@ "typescript": "^5.4.0" }, "dependencies": { + "@types/ws": "^8.18.1", "bcryptjs": "^3.0.3", - "pg": "^8.20.0" + "pg": "^8.20.0", + "ws": "^8.20.0" } } diff --git a/src/api.ts b/src/api.ts index 0d84a3d..45488f3 100644 --- a/src/api.ts +++ b/src/api.ts @@ -11,6 +11,7 @@ * Body: { roomId: number } * Header: X-Username (authenticated display name) * GET /world/presence → { ok: true, rooms: Array<{ roomId, occupants: string[] }> } + * WS /ws → real-time presence push (presence_update events) */ import * as http from 'http'; @@ -18,6 +19,7 @@ import { readFileSync } from 'fs'; import { Logger } from './util/logger.js'; import { loadSolarisRooms } from './data/maps.js'; import { presenceStore } from './world/presence.js'; +import { wsBroadcaster } from './world/ws_broadcaster.js'; const _pkg = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf8'), @@ -115,6 +117,7 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se } presenceStore.travel(username, roomId); apiLog.info('%s traveled to room %d', username, roomId); + wsBroadcaster.broadcast('presence_update', { rooms: presenceStore.getAll() }); const rooms = loadSolarisRooms() ?? []; const room = rooms.find((r) => r.roomId === roomId) ?? null; jsonOk(res, { ok: true, room }); @@ -130,6 +133,8 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se res.end(); }); + wsBroadcaster.attach(server); + server.on('error', (err: Error) => { apiLog.error('HTTP server error: %s', err.message); }); diff --git a/src/world/ws_broadcaster.ts b/src/world/ws_broadcaster.ts new file mode 100644 index 0000000..9471f99 --- /dev/null +++ b/src/world/ws_broadcaster.ts @@ -0,0 +1,52 @@ +/** + * WsBroadcaster — attaches a WebSocket server to the existing HTTP server on + * port 3002. Handles the upgrade handshake at path `/ws` and broadcasts + * JSON events to all connected clients. + * + * On every new connection an initial `presence_update` snapshot is sent so + * late-joiners and re-connectors get current state without waiting for the + * next travel event. + */ + +import { WebSocketServer, WebSocket } from 'ws'; +import * as http from 'http'; +import { presenceStore } from './presence.js'; + +export class WsBroadcaster { + private readonly _wss = new WebSocketServer({ noServer: true }); + private _attached = false; + + attach(server: http.Server): void { + if (this._attached) return; + this._attached = true; + + server.on('upgrade', (req, socket, head) => { + const pathname = (req.url ?? '').split('?')[0]; + if (pathname === '/ws') { + this._wss.handleUpgrade(req, socket, head, (ws) => { + this._wss.emit('connection', ws, req); + }); + } else { + socket.destroy(); + } + }); + + this._wss.on('connection', (ws) => { + // Send immediate presence snapshot so the client has current state. + ws.send( + JSON.stringify({ type: 'presence_update', rooms: presenceStore.getAll() }), + ); + }); + } + + broadcast(type: string, data: object): void { + const msg = JSON.stringify({ type, ...data }); + for (const client of this._wss.clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(msg); + } + } + } +} + +export const wsBroadcaster = new WsBroadcaster(); From f6f6a8cb9d79c3dc4629aed97fddc5516dfe8108 Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 17:09:13 -0400 Subject: [PATCH 07/14] =?UTF-8?q?M4:=20add=20POST=20/world/chat=20?= =?UTF-8?q?=E2=80=94=20room=20chat=20via=20WebSocket=20broadcast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate X-Username header + {roomId, text} body (text max 200 chars) - Broadcast room_chat event {type, roomId, username, text} to all connected WebSocket clients via wsBroadcaster - Updated endpoint doc comment to include new route and WS events Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index 45488f3..8d9c8e2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -11,7 +11,11 @@ * Body: { roomId: number } * Header: X-Username (authenticated display name) * GET /world/presence → { ok: true, rooms: Array<{ roomId, occupants: string[] }> } - * WS /ws → real-time presence push (presence_update events) + * POST /world/chat → { ok: true } + * Body: { roomId: number, text: string (max 200 chars) } + * Header: X-Username + * Broadcasts room_chat WebSocket event to all clients + * WS /ws → real-time push: presence_update, room_chat events */ import * as http from 'http'; @@ -129,6 +133,53 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se return; } + if (req.method === 'POST' && pathname === '/world/chat') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { + jsonError(res, 400, 'X-Username header required'); + return; + } + let body: string; + try { + body = await readBody(req); + } catch { + jsonError(res, 400, 'failed to read request body'); + return; + } + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch { + jsonError(res, 400, 'invalid JSON body'); + return; + } + const p = parsed as Record; + const roomId = + parsed !== null && typeof parsed === 'object' && typeof p.roomId === 'number' + ? (p.roomId as number) + : NaN; + const rawText = + parsed !== null && typeof parsed === 'object' && typeof p.text === 'string' + ? (p.text as string).trim() + : ''; + if (!Number.isFinite(roomId)) { + jsonError(res, 400, 'roomId must be a number'); + return; + } + if (!rawText) { + jsonError(res, 400, 'text must be a non-empty string'); + return; + } + if (rawText.length > 200) { + jsonError(res, 400, 'text must be 200 characters or fewer'); + return; + } + apiLog.info('chat room %d [%s]: %s', roomId, username, rawText.slice(0, 40)); + wsBroadcaster.broadcast('room_chat', { roomId, username, text: rawText }); + jsonOk(res, { ok: true }); + return; + } + res.writeHead(404); res.end(); }); From abda3c8739894c14f60b1782cab30afa9bdcbe0b Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 17:24:52 -0400 Subject: [PATCH 08/14] feat(api): GET /mechs and PATCH /world/mech/select endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /mechs: returns full mech roster merged with MECH_STATS display data (id, slot, typeString, name, weightClass, tonnage, maxSpeedKph, armor, jumpMeters, armament, effectiveRange, disabled) — uses WORLD_MECHS cache - PATCH /world/mech/select: saves mech_id + slot to character DB validates mechId against live roster; looks up character by X-Username header - findCharacterByDisplayName() added to src/db/characters.ts - CORS updated to include PATCH method Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ROADMAP.md | 395 +++--- mpbt-server.err.log | 2 + mpbt-server.out.log | 1728 ++++++++++++++++++++++++ src/api.ts | 99 +- src/db/characters.ts | 12 + src/protocol/combat.ts | 8 +- src/server-world.ts | 11 + src/state/players.ts | 63 +- src/state/world-resume.ts | 13 + src/world/combat-config.ts | 78 ++ src/world/world-handlers.ts | 2494 +++++++++++++++++++++++------------ src/world/world-scene.ts | 16 +- 12 files changed, 3847 insertions(+), 1072 deletions(-) create mode 100644 mpbt-server.err.log create mode 100644 mpbt-server.out.log diff --git a/ROADMAP.md b/ROADMAP.md index 712d189..a75a7a9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,318 +2,247 @@ ## Vision -Two players load `MPBTWIN.EXE`, connect to this server, pick their mechs, drop into a Solaris arena, and fight a real-time BattleMech duel — served entirely from this open-source emulator with no Kesmai infrastructure. +Build a server emulator that lets the original retail **Multiplayer BattleTech: Solaris v1.29 client** (`MPBTWIN.EXE`, released June 24, 1999) connect, create or resume a character, move through Solaris, select mechs, enter sanctioned and unsanctioned arena fights, complete real-time multiplayer BattleMech combat, and persist SCentEx-style results without any Kesmai infrastructure. -This is a long-haul reverse-engineering project. Every milestone below is blocked by RE work or builds directly on a prior milestone. Progress is honest: where the protocol is unknown, we say so. +This roadmap is intentionally scoped to the **retail v1.29 client**. Earlier v1.23 work remains valuable as reverse-engineering history, but completion is measured against the 1999 retail client behavior. ---- - -## How to Read This +## Roadmap Rules | Icon | Meaning | -|------|---------| -| ✅ | Complete and tested against the real client | -| 🔬 | Blocked on Ghidra RE — protocol unknown | -| 🔧 | Engineering work — protocol understood, implementation needed | +|---|---| +| ✅ | Complete and tested against a real retail client | +| 🟡 | Implemented or mostly understood, but needs v1.29 hardening or broader coverage | +| 🔬 | Reverse-engineering blocker | | ❌ | Not started | ---- +## Commit-History Read -## Current State +The project moved from first lobby compatibility to playable arena prototypes quickly: -| Feature | Status | -|---|---| -| ARIES 12-byte transport framing | ✅ | -| TCP stream reassembly (fragmentation) | ✅ | -| `LOGIN_REQUEST` → `LOGIN` handshake | ✅ | -| `SYNC` ack + welcome escape sequence | ✅ | -| Inner game frame (seq + cmd + CRC) | ✅ | -| Base-85 encode/decode | ✅ | -| 19-bit LFSR CRC (lobby init `0x0A5C25`) | ✅ | -| Cmd 26 — mech list window | ✅ | -| Cmd 7 — menu dialog (select + confirm) | ✅ | -| Cmd 0x1D — cancel/ESC re-sends mech list | ✅ | -| Cmd 20 — examine mech text-dialog response | ✅ | -| `REDIRECT` packet (type `0x03`) | ✅ | -| Post-redirect game world (world login, navigation, mech bay, combat handoff) | ✅ | -| Two-human sanctioned duel playtest | ✅ | - -The emulator now goes well beyond the old redirect-only frontier: the client reaches the world, travels, selects mechs in-world, enters Solaris combat, and can complete supervised two-human sanctioned duel playtests. The remaining work is broader fidelity, richer multi-client arena behavior, and fuller late-1990s-faithful world/combat coverage. - ---- - -## Reference Materials - -These files are gitignored — place them in `research/` for local use. - -| File | Contents | Project use | -|---|---|---| -| `BT-MAN.decrypted.txt` | Full game manual: world navigation, chat channels, combat controls, mech stat tables | Design reference for M4–M9; source for `src/data/mech-stats.ts` | -| `SOLARIS.MAP` | Solaris city venue locations, 189 KB. Leading room table is now reproducibly parsed: u16 record count, then room ID / flags / coordinates / aux fields / NUL-included name+description strings. Local file count is 32 records: Solaris rooms 146–171 plus sector rows 1–6; trailing non-room sections remain undecoded | M5 world map reconstruction | -| `IS.MAP` | Inner Sphere / global location table, 40 KB. Same leading room-table format; local file count is 271 records, covering room IDs 1–271, including Solaris entries duplicated in the global namespace | M5 world map; full-sector navigation | -| `Gnum*.txt / Gnum*.md` | Firsthand gameplay observations: 4v4 lances, fixed spawns, travel times, team/all-chat | Sanity-check for RE findings | +- 2026-04-05 to 2026-04-07: lobby, `.MEC` roster, command 20, character/world entry, v1.23 protocol RE. +- 2026-04-08 to 2026-04-11: M4/M5 world travel, map parsing, combat entry, movement/control frames. +- 2026-04-10 to 2026-04-17: concentrated M6/M7 combat work: jump jets, firing gates, match-end scenes, ready rooms, sanctioned duel flow. +- 2026-04-18 to 2026-04-22: retail combat fidelity work: fall/recovery, gait, ammo, radar distance, bot behavior, arena regressions. +- 2026-04-23 to 2026-04-27: v1.29 migration: world command repurposing, Solaris live testing, travel fallback, bot team bootstrap fidelity. ---- +Practical implication: the remaining work is not a wholesale protocol rewrite. It is v1.29-specific completion: safer Solaris UI routing, accurate arena staging, multi-client combat fidelity, sanctioned-battle lifecycle, SCentEx persistence, and long-running operational hardening. Client-visible mech-management screens exist in v1.29, but firsthand GameStorm behavior indicates buy-ammo/repair/name-mech style flows were not part of live 1999 gameplay, so they are post-completion research rather than launch-blocking server scope. -## Milestones +## Current Baseline ---- +| Area | Status | Notes | +|---|---|---| +| ARIES transport, login, keepalive, redirect | ✅ | `COMMEG32.DLL` and `INITAR.DLL` are byte-identical between the local v1.23 and v1.29 installs; no new transport contract is expected just because of v1.29. | +| Lobby and world login | ✅ | Returning players can skip character creation and enter world; first-login character creation is implemented. | +| Room presence, room chat, ComStar DM | ✅ | Room roster, arrivals/departures, booths, online/offline messages, reply flow, and personnel-record basics exist. | +| Solaris travel | 🟡 | v1.29 `Cmd40`/`Cmd43` browser family and `Cmd49` overlays are understood enough for current travel; authentic topology and all facility flows need completion. | +| In-world mech picker | ✅ | Class/chassis/variant picker feeds combat bootstrap. Retail completion requires selection/loadout fidelity, not dormant repair or ammo-purchase economics. | +| Arena ready rooms | 🟡 | `MECH` / `SIDE` / `STATUS`, same-room staging, readiness, and 2..8 pilot launch are partly implemented. Full sanctioned lifecycle is not complete. | +| Single-client combat | 🟡 | Movement, jump jets, selected mech bootstrap, bot opponent, firing, damage feedback, and result scenes exist. Retail heat/damage/fall fidelity still needs closure. | +| Multi-client combat | 🟡 | Two-human sanctioned duel playtests are possible. Broader 4v4/team behavior, synchronized damage/state, and settlement hardening remain. | +| v1.29 world UI migration | 🟡 | Major repurposed commands are known (`Cmd39`, `Cmd44`, `Cmd46`, `Cmd57`, `Cmd45`/`Cmd58`), but some server surfaces still use conservative compatibility routes. | +| Rankings / SCentEx | 🟡 | Emulator ranking pages and duel persistence exist. Exact retail formula, result routing, and Team Sanctioned Battle reporting are not complete. | + +## Completion Definition + +The emulator is complete enough for a first public retail-v1.29 server when all of the following are true: + +- A clean retail v1.29 install can connect through the normal launcher flow with no binary patches other than optional local windowing helpers. +- A new account can create a character, select allegiance, enter Solaris, travel, use ComStar, and persist state. +- Returning accounts restore world room, selected mech, messages, ranking state, and pending settlement notices. +- Players can use Solaris facilities needed for routine 1999 play: travel, ComStar, mech selection, arena ready rooms, rankings/results, and personnel records. +- At least one sanctioned 1v1 duel path is retail-stable end to end: staging, entry, combat, result scene, disconnect/restore, C-bill transfer, ranking update. +- Multi-player arena fights support the retail eight-side model and at least 4v4 lance-scale play without state desync. +- Combat uses real `.MEC` data for movement, weapons, ammo, armor/internal state, heat, jump capability, and critical effects as far as the retail client exposes them. +- Long-running sessions survive keepalives, disconnect/reconnect, duplicate-login replacement, combat result timing, and common client focus/window quirks. +- All remaining deviations from known retail behavior are documented as intentional compatibility choices. + +## Milestones -### M1 — Lobby Completeness +### M0 — Preserve the Retail v1.29 Contract -**Goal:** The lobby experience is fully correct before moving to the game world. +**Goal:** Keep the project aligned to the actual June 1999 client rather than drifting around older v1.23 assumptions. | Task | Status | Notes | |---|---|---| -| Parse real `.MEC` files → `src/data/mechs.ts` | ✅ | `loadMechs()` scans `mechdata/*.MEC`, assigns correct `mech_id` from MPBT.MSG variant table; `mechType` field hardcoded to 0 pending M2 binary RE; `variant`/`name` empty → client falls back to its own MPBT.MSG lookup | -| Cmd 20 — mech examine/stats response | ✅ | Single mode=2 packet with direct stats text built by `buildMechExamineText()` from `MECH_STATS`; `0x5C` (`\`) is the line separator (`FUN_00433310` NULs it before rendering); `#NNN` shortcode is NOT used — our MPBT.MSG has incomplete/stale stats data | -| Cmd 20 — hierarchical class/mech/variant examine (issue #21) | ⏸ | **Deferred to M8** — the single-variant examine (`mode 2`) is complete. The 3-level hierarchy (class → mech list → variant details) is enhancement scope; will be revisited alongside `.MEC` file integration in M8. | -| Cmd `0x1D` — cancel/ESC in menu dialogs | ✅ | Resolved — server re-sends mech list; sending nothing freezes client | -| ACK reply for seq > 42 | 🔬 | Trigger condition documented in RESEARCH.md §9; v1.23 RE confirms `FUN_0040eb40` is a no-op stub — no ACK is sent by the combat client in v1.23. Server must not require combat ACKs. | +| Document v1.29 binary baseline | ✅ | `RESEARCH.md` records v1.29 file version, size, and SHA-256 plus byte-identical `COMMEG32.DLL` / `INITAR.DLL`. | +| Keep ARIES transport unchanged | ✅ | v1.29 does not require a new login, redirect, keepalive, or launcher contract. | +| Track repurposed v1.29 world opcodes | 🟡 | Known: old v1.23 meanings for `Cmd39`, `Cmd44`, and `Cmd46` are unsafe for v1.29. | +| Maintain v1.29 regression tests/smokes | 🟡 | Existing socket and GUI-driven probes should be kept; add explicit v1.29 fixtures for every fixed compatibility issue. | -**Verification:** Connect real `MPBTWIN.EXE`; press `X` on a mech (stats appear), press `ESC` in dialog (no disconnect), browse the first 20 mechs without crash. +**Exit criteria:** Every new server-visible feature states whether it targets v1.29 directly or is a v1.23-era compatibility fallback. ---- +### M1 — Stable Retail Login, Account, and Character Flow -### M2 — RE: Game World Protocol +**Goal:** A retail v1.29 player can reliably get from launcher to world. -**Goal:** Understand the protocol spoken on the second TCP connection (post-`REDIRECT`). +| Task | Status | Notes | +|---|---|---| +| ARIES login and redirect | ✅ | Transport, `LOGIN_REQUEST` / `LOGIN`, `SYNC`, welcome, and `REDIRECT` are implemented. | +| Account auth and first-login character creation | ✅ | PostgreSQL accounts/characters and House allegiance selection exist. | +| Returning-player direct world entry | ✅ | Existing character skips creation and redirects to world. | +| Duplicate-login/session replacement | 🟡 | Replacement behavior exists; broaden coverage for lobby, world, ready room, combat, and post-result windows. | +| Launcher/test tooling | 🟡 | `play.pcgi` generation exists; improve reproducibility for fresh per-launch retail-client validation. | -This milestone is pure Ghidra work. No code is written here — findings go into `RESEARCH.md`. +**Exit criteria:** Ten clean v1.29 launcher runs in a row can create or resume characters without manual recovery. -| RE Target | Binary | Status | Notes | -|---|---|---|---| -| `Aries_RecvHandler` case 0 & REDIRECT | `COMMEG32.DLL` | ✅ | §17: REDIRECT handler confirmed; case 0 sends WM_0x7f0 to game window | -| World command dispatch table | `MPBTWIN.EXE` | ✅ | §18: two tables — RPS (0x00470198, cmd 0–76) and Combat (0x00470408, cmd 0–79); full address table | -| Initial world handshake | `COMMEG32.DLL` + `MPBTWIN.EXE` | ✅ | §18: LOGIN_REQUEST→LOGIN→`"\x1b?MMW Copyright Kesmai Corp. 1991"`→cmd-3; same sequence as lobby | -| `g_aries_GameWorldConn` (`DAT_1001a080`) | `COMMEG32.DLL` | ✅ | §17: created by `Aries_Connect`; secondary connection object | -| Combat CRC crossover point | `MPBTWIN.EXE` | ✅ | §18: `Frame_VerifyCRC` uses `g_combatMode` to select seed; RPS=`0x0a5c25`, Combat=`0x0a5c45` | -| First 10+ world commands | `MPBTWIN.EXE` | ✅ | §18: first 13 RPS cmds decompiled — ping/ack (1–2), text broadcast (3), scene init (4), cursor (5–6), menu (7), session data (8), room list (9), text feed (10), player events (11–13) | -| World frame format | `MPBTWIN.EXE` | ✅ | §18: identical to lobby — ESC-delimited, 19-bit LFSR CRC, same base-85 encoding | +### M2 — Solaris World Core -**Deliverable:** RESEARCH.md §17 (COMMEG32.DLL RE) and §18 (world protocol RE) — COMPLETE. +**Goal:** The world layer is stable enough for ordinary social play. ---- +| Task | Status | Notes | +|---|---|---| +| Room presence and chat | ✅ | Same-room roster, arrival/departure, booth privacy, and room-local text relay work. | +| ComStar direct messages | ✅ | Online and offline message delivery works. | +| All-roster inquiry and personnel record | 🟡 | `Cmd48` / `Cmd14` path works, but personnel-record page semantics and target header limitations need cleanup. | +| Facility entry model | 🟡 | Travel and arena entry exist. Bar terminal, global ComStar access, and facility-specific action menus need retail-safe coverage. | +| World reconnect restore | 🟡 | Room/mech/deferred settlement restore exists; expand to all world UI states that matter. | -### M3 — Game World Connection +**Exit criteria:** Two retail clients can spend 30 minutes in world using travel, room chat, ComStar, roster/personnel lookup, and reconnects without stale presence or broken UI state. -**Goal:** The client successfully connects to the game world server and enters a stable state without crashing. +### M3 — v1.29 Solaris Browser and Menu Surfaces -*Depends on M2.* +**Goal:** Replace unsafe v1.23-shaped UI assumptions with v1.29-correct command families. | Task | Status | Notes | |---|---|---| -| `src/server-world.ts` — second TCP listener | ✅ | Port 2001; same `PacketParser` (ARIES); RPS CRC seed 0x0A5C25 | -| `src/protocol/world.ts` — world command builders | ✅ | Cmd3 TextBroadcast, Cmd4 SceneInit, Cmd5/6 cursor, Cmd9 character-creation prompt notes | -| `src/state/launch.ts` — mech launch registry | ✅ | Bridges lobby→world: records selected mech before REDIRECT, consumed on world LOGIN | -| `ClientSession` — add `'world'` phase | ✅ | Extended `src/state/players.ts`; `selectedMechId?` / `selectedMechSlot?` added | -| Initial world handshake | ✅ | LOGIN_REQUEST → LOGIN → SYNC ack → MMW welcome → cmd-3 → Cmd6+Cmd4+Cmd10+Cmd3+Cmd5 | -| Fix REDIRECT target to WORLD_PORT | ✅ | Lobby now redirects to port 2001; launch record stored before REDIRECT sends | -| `gen-pcgi.ts` — separate lobby/world ports | N/A | `play.pcgi` always points to lobby (2000); REDIRECT carries the world address. Combat server is a separate dynamic spin-up (M6/M7). | - -**M3 additions — Persistence, Character Creation, Direct World Entry (#25 / #26 / #27):** +| `Cmd40` / `Cmd43` location browser | 🟡 | v1.29 browser family is mapped; server should consistently use the v1.29-safe path for travel and grouped Solaris browsing. | +| `Cmd49` map connector overlay | 🟡 | Handler is identified; use it where retail map links are known. | +| `Cmd45` / `Cmd58` scroll-list shell | 🟡 | Accepted body syntax for rankings/results is emulator-proven against v1.29; keep it as the safe paged-list surface. | +| `Cmd57` hotkey selection menu | 🔬 | Strong v1.29 chooser candidate, but preset/control-strip details are still risky. Do not replace working compatibility menus until a safe builder is proven. | +| Remove unsafe old `Cmd44`/`Cmd46` usage | 🟡 | v1.29 repurposes them; keep explicit builders only for the new meanings. | -| Task | Status | Notes | -|---|---|---| -| PostgreSQL persistence layer | ✅ | `pg` + `bcryptjs`; `src/db/{client,schema.sql,accounts,characters,migrate}.ts`; `docker-compose.yml` | -| `accounts` table + bcrypt password auth | ✅ | Auto-register on first login; verify password on subsequent logins; rejects wrong passwords | -| `characters` table + allegiance enum | ✅ | One character per account; `display_name UNIQUE`; allegiance CHECK constraint `Davion\|Steiner\|Liao\|Marik\|Kurita` | -| `npm run db:migrate` — idempotent schema apply | ✅ | Reads `src/db/schema.sql`; safe to re-run | -| `ClientSession` — add `accountId`, `displayName`, `allegiance` | ✅ | Set from DB after login; `'char-creation'` phase added | -| Character creation flow (first login) | ✅ | cmd-3 → no character in DB → send `Cmd9` callsign + House prompt → persist typed display name and allegiance → seed launch context → REDIRECT | -| Post-login direct world entry (returning player) | ✅ | cmd-3 → character found → REDIRECT to port 2001 immediately; no mech-select shown | -| World server uses `displayName` as Cmd4 callsign | ✅ | Falls back to `username` if character data unavailable (e.g. test direct-connect) | -| Display name entry (name selection dialog) | ✅ | Implemented with server `Cmd9`, the likely authentic first-login prompt: it opens `MPBT.MSG[5]` (`"Enter your character's name"`), then a numbered selector titled `MPBT.MSG[6]` (`"Choose your allegiance:"`), and submits outbound `cmd 9, subcmd 1, , `. This supersedes the earlier `Cmd36`/`Cmd37` hypothesis; `Cmd36` is the read/reply viewer, `Cmd37` opens the ComStar compose editor, and the live `Cmd37(0)` probe is only a compatibility bridge. Live GUI probe confirmed the wire path; socket smoke now confirms persistence, launch-context seeding, and returning-account world entry with the typed callsign in `Cmd4`. | +**Exit criteria:** Travel, ranking choosers, mech-selection surfaces, and result pages use v1.29-safe surfaces with no accidental v1.23 opcode semantics. -**Known M3 limitations / M4 work:** -- Initial room-sync uses `Cmd10`; the earlier `Cmd9(count=0)` placeholder was removed, and `Cmd9` is now tied to the first-login name + allegiance prompt rather than room presence. -- `Cmd8` (session binary data / mech loadout) not yet sent; client mech stats display may be absent. -- Arena navigation and movement not yet implemented (M5). -- World server does not yet bounce a second REDIRECT to a combat server (M6/M7). +### M4 — Map, Room, and Facility Model -**Verification:** -- *New player:* connect, select House allegiance, enter world — Cmd4 callsign shows username; allegiance persisted to DB. -- *Returning player:* connect, skip character creation, enter world directly — no mech-select screen shown. -- *Wrong password:* second login with wrong credentials → connection closed. -- *Mech select (M6 path):* cmd-26 visible only when explicitly triggered; pre-combat flow unaffected. -- *First-login `Cmd9` implementation:* socket smoke confirmed `Cmd9` prompt → typed callsign + House reply → persisted character → REDIRECT → world init `6,4,10,3,5`, with `Cmd4` containing the typed callsign on both first-login and returning-account paths. The older `Cmd37(0)` probe remains a compatibility bridge, not the authentic original name-entry UI. +**Goal:** Solaris feels like a coherent world, not a hardcoded room stub. ---- +| Task | Status | Notes | +|---|---|---| +| Parse `SOLARIS.MAP` and `IS.MAP` leading room tables | ✅ | Leading room records are decoded and loaded. | +| Use real room names/descriptions/icons | 🟡 | Room descriptions are wired; complete icon/facility metadata and fallback behavior. | +| Authentic travel topology | 🔬 | Leading map tables do not contain exits. Need either RE of server-side topology clues, manual reconstruction, or documented approximation. | +| Room/facility classification | 🟡 | Arena vs. non-arena is enough for current flow; bar/terminal/bank/hub/street semantics need completion. | +| Tram/T.O.F.S. behavior | ✅ | Same travel flow as ordinary Solaris map travel; no separate command needed. | -### M4 — Chat and Presence +**Exit criteria:** A player can navigate all known Solaris sectors/facilities exposed by the v1.29 client, with documented topology choices. -**Goal:** Players see each other and communicate across the full world — not just within a room. +### M5 — Asset-Backed Mech Selection and Combat State -*Depends on M3.* +**Goal:** Server-owned mech selection and combat state match the retail data the client actually used during 1999 play. | Task | Status | Notes | |---|---|---| -| ComStar DM — store and deliver | ✅ | `Cmd36` delivers to online recipients immediately. Offline messages are persisted to a `messages` DB table (`src/db/messages.ts`: `storeMessage` / `claimUndeliveredMessages`). The message body is stored pre-formatted and delivered atomically on the recipient's next world login (cmd-3 trigger). Offline detection: `10_000_000 + accountId` range→`recipientAccountId` derived from ComStar ID. `Reply` flow: `Cmd37(targetId)` opens the compose editor pre-addressed to the target. | -| Room broadcast | ✅ | Same-room presence seeds the roster with `Cmd10`, then uses `Cmd13` arrival and `Cmd11(status=0)` departure for incremental updates. World `cmd-4` free-text relay is implemented as room-local chat fan-out via `Cmd3`. Booth privacy implemented: booth occupants only hear each other; standing players only hear other standing players. Validated with two-GUI sandbox (2026-04-07): real Client B receives `Cmd10 RoomPresenceSync (2 entries)`, both clients exchange chat, and arrival/departure notifications fire correctly. | -| Player join / leave events | ✅ | Same-room `Cmd10` / `Cmd13` / `Cmd11(status=0)` path fully operational. Social-room status: `Cmd7(listId=3)` `selection=0` grabs a booth, `selection=2` stands, `selection>=3` joins booth `selection-2`, with `Cmd11(status=5..12)` updating the roster table. Two-GUI sandbox confirmed Client B world-init, `RoomPresenceSync`, and arrival/departure events with Client A connected. | -| F7 — team / lance channel | ❌ | Arena-only; requires `Cmd8` team assignment — moved to M7. v1.23 RE (§19.4) confirms F7 does NOT emit a network packet — it only toggles the local chat-channel UI indicator. Channel selection is implicit via the mode command (`FUN_0043d920`). | -| F8 — all-comm / chat-window toggle | ❌ | Arena-only; v1.23 RE (§19.4) confirms F8 does NOT emit a network packet — purely local UI state toggle (same `FUN_0042dc30` visual handler as F7). Moved to M7. | -| All-roster query + inquiry submenu | ✅ | KP5 → `Cmd7(listId=3, selection=1)` sends `Cmd48_KeyedTripleStringList` (`0x51`) with live sessions. Row-picks open the inquiry submenu (`INQUIRY_MENU_ID=0x3F3`; 0x3E8 is client-reserved and must not be used). `selection=1` sends `Cmd37(targetId)` to open ComStar compose; `selection=2` sends `Cmd14` personnel record. Both selections work even if the target disconnects after the submenu opens. Personnel record: `Handle` header fixed by sending a single-entry `Cmd10` (target only) before `Cmd14` — seeds `entry[0]` of the all-roster table; `ID` header fixed by shifting `comstarId` into the `FUN_00405ea0` valid range (`10_000_000 + accountId` → 5-char base-36). | +| Load all `.MEC` variants | ✅ | Roster and mech IDs come from real assets and `MPBT.MSG`. | +| Use `.MEC` movement fields | ✅ | Walk/run split, speedMag, and jump-jet presence are integrated. | +| Use `.MEC` weapons/ammo/heat/armor/internal fields | 🟡 | Weapon ranges, ammo bootstrap, heat sinks, and key armor/internal values are partly integrated; complete all combat-critical fields. | +| Selected-mech persistence | 🟡 | Preserve the selected mech across world, ready-room, combat, reconnect, and result-restore flows. | +| Deliberately defer inactive mech-management economics | ✅ | `Cmd30`, `Cmd31`, and repurposed `Cmd39` are useful RE findings, but GameStorm-era play did not expose functional buy-ammo/repair/name-mech loops. Keep them out of the completion path unless new retail evidence proves otherwise. | -**Verification:** Two-GUI sandbox: KP5 → select target → "Send ComStar message" opens compose editor pre-addressed to correct player; "Access personnel data" shows correct Handle, ID, Rank, House, Sector, Location, Status. Offline stub shown when target disconnects between menu open and pick. +**Exit criteria:** A player can select a retail `.MEC` variant, enter world/ready-room/combat with that selection intact, fight with asset-backed movement/weapons/ammo/heat/armor data, and return to world without loadout or result-state corruption. ---- +### M6 — Single-Client Combat Fidelity -### M5 — World Navigation - -**Goal:** A single player can move around the game world (Solaris sectors / arenas) from the server's perspective. - -*Depends on M4.* - -The world uses two distinct room types: **bar** (social spaces, Tier Ranking terminals, ComStar facilities) and **arena** (combat venues). Source topology: `SOLARIS.MAP` (rooms 146+, partly decoded) and `IS.MAP` (rooms 1–145) — both gitignored; see Reference Materials above. +**Goal:** One retail v1.29 client can fight a server bot with retail-shaped movement, weapons, damage, and result flow. | Task | Status | Notes | |---|---|---| -| `SOLARIS.MAP` / `IS.MAP` binary format RE | ✅ | **DECODED** (RESEARCH.md §19.7): 2-byte LE record_count header; each record = 18-byte fixed prefix (room_id, faction, raw_x, raw_y, 4×flags) + uint8 name_len + name chars + uint8 desc_len + desc chars. IS.MAP display: `x/3+380`, `y/−3+248`; SOLARIS.MAP: identity. Parser via `npm run map:dump -- --rooms`. Ghidra confirms `Map_LoadFile` passes trailing bytes to the picture/resource loader; exits not stored in trailing blob. Needs implementation in room-loader. | -| RE movement protocol | 🔧 | **DECODED** (RESEARCH.md §19.2): client→server timer-based (100 ms). Cmd 8 (coasting): X(3w)+Y(3w)+heading(2w)+adj_vel(1w)+rotation(1w). Cmd 9 (moving): X(3w)+Y(3w)+heading(2w)+turn(1w)+0xe1c(1w)+throttle(1w)+leg(1w)+rotation(1w). Bias constant=0xe1c (3612), divisor=0xb6 (182). Travel-reply: server cmd 40/43 opens IS/Solaris map UI; client replies `cmd 10` (`type1 contextId` + `type4 selectedRoomId+1`). Real GUI validated `Travel → Cmd43 → cmd 10(selection=148) → Ishiyama Arena`. Server→client position packets (Cmd65) still 🔬. | -| Tram / monorail RE | ✅ | **RESOLVED** (RESEARCH.md §19.10): T.O.F.S. (The Tram) uses the **identical** `cmd5 actionType 4 → Cmd43 (context 0xc6) → cmd10` travel flow as regular Solaris map travel. `World_HandleMapOpenSolarisPacket_v123` has no tram-specific context branch; no separate tram command exists in the v1.23 dispatch table. No new server implementation needed. Closes issue #70. | -| Room model from map files | 🔧 | `parseMapFile()` implemented in `src/data/maps.ts`; `SOLARIS_SCENE_ROOMS` (32 rooms: 146–171 Solaris + sectors 1–6) is a hardcoded stub with provisional linear exits in `getSolarisRoomExits()`. `Cmd23` location-icon clicks handled via `handleLocationAction`; `Cmd43`→`cmd10` travel reply handled via `handleMapTravelReply`. Next: load rooms, types (bar / arena), and exits from `IS.MAP` / `SOLARIS.MAP` parsed data; replace hardcoded stub; authentic exit graph still 🔬. | -| RE world scene-action family | ✅ | **RESOLVED** (RESEARCH.md §19.6.0a): subtype `1/2` in-game noun confirmed as **Agreement** (C-bill contract between Successor State parties — `MSG[0x19e]` = `"Details of Agreement between"`). Full field-label MSG string table (`MSG[0x19e]`–`MSG[0x1b4]`) now documented. Subtype model: `1/2` = Agreement offer/review, `3` = duel, `4` = membership bid, `5/6/7` = subcontract offer/review/terms. Live capture for `cmd5 actionId → subtype` mapping deferred (not required for M5 verification). | -| Server-side position tracking | ✅ | `worldX/Y/Z` + `worldMapRoomId` on `ClientSession`; populated atomically via `setSessionRoomPosition()` in `world-data.ts` from SOLARIS.MAP `centreX/centreY` at every room transition. | -| Position sync to client | ✅ | World-mode scene position conveyed via Cmd4 `playerScoreSlot` (= room sceneIndex) — already working. Room type communicated via arena-only "Fight" button (`actionType 5`) in `buildSceneInitForSession`; Cmd65-equivalent server→client coord push in travel-world mode remains 🔬. | - -**Verification:** Single client can navigate between areas; room type (bar vs. arena) is correctly identified by the server. - ---- +| Combat bootstrap (`Cmd72`, `Cmd64`, `Cmd65`, `Cmd62`) | ✅ | Local and remote actor setup works. | +| Movement and radar distance | ✅ | Recent history aligned movement/speed and radar distance with the retail client. | +| Jump jets including low-jet v1.27+ behavior | 🟡 | v1.29 confirms non-zero jump capability, not a four-jet minimum. Finish fuel, altitude, landing, and damage edge cases. | +| Weapon fire and projectile/effect updates | 🟡 | `cmd10` shot geometry and `Cmd68`/`Cmd69` effects are usable; finish ordinary vs. TIC volley semantics. | +| Damage, heat, criticals, ammo | 🟡 | Current model is playable but not complete retail fidelity. Heat/system degradation and all critical sections need closure. | +| Fall/recovery | 🟡 | v1.29 confirms `Cmd70` remains the main fall/landing/collapse driver; 60 FPS cap solved the visible slow-fall symptom. Finish recovery/stand-up and damaged-leg edge cases. | +| Bot opponent | 🟡 | Bot AI is increasingly retail-like; keep it deterministic enough for tests and configurable enough for play. | -### M6 — Single-Client Combat Loop +**Exit criteria:** A single player can complete repeated bot fights with no stuck combat gate, no bogus speed/altitude state, correct result scene, and documented remaining combat deviations. -**Goal:** One player in an arena can engage with the combat system (even against a scripted dummy opponent). +### M7 — Multi-Client Arena Combat -*Depends on M5.* +**Goal:** Multiple retail v1.29 clients can fight each other in real time with coherent state. | Task | Status | Notes | |---|---|---| -| RE server→client combat bootstrap / position sync | ✅ | Full bootstrap sequence documented in RESEARCH.md §19.9: Cmd72 (local mech init) → Cmd64 (remote actors) → Cmd65 (initial positions) → Cmd62 (combat-start, clears SPACEBAR block). SpeedMag Cmd65 echo implemented and confirmed on HUD gauge (§19.10). | -| Combat movement + speedMag physics | ✅ | Cmd8 (coasting) and Cmd9 (moving) client→server are parsed and handled with the corrected `.MEC` speed split from RESEARCH.md §24: `walkSpeedMag = mec_speed × 300`, `maxSpeedMag = round(mec_speed × 1.5) × 300`, and full-forward Cmd9 scaling now uses `THROTTLE_RUN_SCALE = 20` instead of the old `45`. TAP-mode Cmd8 echo suppression remains required so local throttle ownership is not overwritten, and current live-combat server policy also walk-caps reverse drift / echoed reverse `speedMag` for range pressure parity. HUD speed gauge confirmed working. | -| In-world 3-step mech picker (Mech / Mech Bay) | ✅ | Class → chassis → variant flow implemented. Arena scenes now label action type `6` as `Mech`; non-arena rooms still show `Mech Bay`. Safe listIds: `0x20` (class/variant), `0x3e` (chassis). Cursor-freeze fix: `Cmd5 CURSOR_NORMAL` sent after every `Cmd26` and after post-selection `Cmd3`. Selected mech slot stored in `session.selectedMechSlot`. See RESEARCH.md §23. | -| RE weapon fire packets | 🔧 | v1.23 client → server fire request decoding in RESEARCH.md §19.3 is now tighter: `Combat_SendCmd12Action_v123` emits `cmd 12`, but fresh 2026-04-20 caller audit shows its `action 0` caller is the downed recovery branch in `Combat_InputActionDispatch_v123`, while the other two live callers are jump start (`4`) and landing (`6`). That weakens the old "action0 is ordinary fire" interpretation further; current server combat still relies on `cmd10` shot geometry for actual weapon fire acceptance, with TIC volleys already proven to arrive as direct bundled `cmd10`. Server `Cmd68` is projectile/effect spawn; `Cmd66`/`Cmd67` now carry damage code/value updates. | -| RE TIC system | ✅ N/A | Three Targeting Interlock Circuits (A/B/C): v1.23 RE **confirms TIC is entirely client-local**. Toggle membership stored in local arrays (`DAT_004f2128`, `DAT_004f2150`, `DAT_004f2178`); TIC group fire calls a local effect path only. No separate network sender exists. No server-side player TIC protocol implementation is needed. Dynamic capture still needed to clarify whether `cmd 12/action 0` targets the selected weapon, selected TIC group, or all queued weapons. Current server note: bot AI may still derive TIC-style volley presets locally from weapon/heat data without changing this protocol conclusion. | -| RE damage model | 🔧 | v1.23 damage-result path is partially decoded in RESEARCH.md §19.6.1: `Cmd66` applies actor damage code/value pairs, `Cmd67` applies local-actor pairs, and the shared classifier partitions codes into critical/system, armor-like, internal-like, weapon, and ammo-bin ranges. `.MEC` offset correction: `0x3c` is a signed critical/equipment range bound and weapon ids start at `0x3e`. Exact section labels, kill semantics, and heat/system-degradation mapping still need live capture. | -| RE non-death fall / recovery fidelity | 🔬 | The server can now prove multiple left-leg loss probes on the wire (`Cmd70/8`, `1->8`, `4->8->6`, `1->4->8->6`, and local recovery `1->8->0`), and all meaningful variants tried so far are GUI-validated as visually insufficient: the retail client stays upright. Ghidra now narrows the recovery side: local slot `0` ignores inbound `Cmd70/4` and `Cmd70/6`, inbound `Cmd70/0` is the strongest current local recovery-ack candidate, and F12 stand-up should emit wire `cmd12/action0` only when the client is truly down. The latest live callback proof closes that local gate (`+0xdc bit 0x10` must clear before F12 sends), and fresh caller audit shows `cmd12/action0` comes from the recovery branch rather than ordinary weapon fire. Based on that, the server-side stateful `action0 -> Cmd70/0` path has now been promoted beyond the old env-gated experiment; the remaining blocker is full retail-visible fall/recovery fidelity, not the old timer heuristic. | -| RE jump jets | 🔧 | Fire command **decoded** (§19.3): client sends ESC+'!'+0x2D+0x25+CRC (cmd=12, action=4) via `Combat_SendCmd12Action_v123('\x04')`; landing/touchdown sends `cmd 12/action 6`. The server now matches several confirmed client guards instead of the older loose prototype: jump fuel uses the client's `0x78`/`120` cap, start requires fuel `> 0x32`/`50`, duplicate airborne start is rejected, and grounded recharge follows a single timer path closer to the client's main-loop regen instead of the old per-frame + passive combo. Remaining 🔬: exact airborne drain breakdown by thrust/turn/velocity flags, authoritative altitude/landing semantics for `action 6`, and no-jump chassis validation against broader `.MEC` data. | -| Implement `src/protocol/combat.ts` | ✅ | All combat packet builders: Cmd64–Cmd73 implemented; combat entry wired in server-world.ts via `/fight` text command; MMC welcome + Cmd72 bootstrap sent on trigger | -| Selected mech → combat bootstrap propagation | ✅ | World mech selection now feeds live combat bootstrap state. `tools\\duel-selected-mech-smoke.mjs` proves the shared duel path sends each pilot's chosen mech ID through `Cmd72` (local) and `Cmd64` (remote) on both clients. | -| RE torso/leg independence | 🔬 | Legs = heading (KP4/6/2/8); torso = facing (WASD); server must track both; compass shows both simultaneously | -| RE turn timer / match end | ✅ | **RE complete (issue #79, §23):** No server-to-client match-end packet exists. Win = client local sim kills enemy → results loop → exit key → TCP close. Loss = Cmd67 IS damage → actor-0 IS=0 → disconnect timer → TCP close. Server stops Cmd67 when `playerHealth ≤ 0`. | -| RE physical combat | 🔬 | Death-from-above (DFA) and alpha strike — dedicated commands or derived from positional data? | -| RE v1.23 RPS→combat state handoff | 🔬 | `MMW` welcome enters `"Solaris RPS"`; later `MMC` welcome enters `"Solaris COMBAT"` only after RPS is established. `"Transition to combat - even"` is an internal music state, not a server payload. | -| Scripted dummy opponent | 🔧 | The old static dummy has been replaced by a much more retail-like combat bot. It now maintains persistent remote position/facing, maneuvers to matchup-aware preferred range, uses jump jets tactically, strafes/jinks under threat, applies movement-aware to-hit rolls in both directions, derives TIC-style volley presets from heat/weapon data, and now keeps its retreat / reverse behavior inside the same walk-capped reverse envelope the live player path uses. Bot range holding and jump-fit planning are also ammo-aware, so spent long-range bins no longer keep it kiting for bands it cannot actually threaten. Remaining work is live tuning and any deeper retail fidelity gaps that only show up in manual duels. | +| Arena ready-room identity | 🟡 | Ready rooms, `MECH`, `SIDE`, `STATUS`, and room capacity exist; harden for all room counts and reconnects. | +| Team/side bootstrap | 🟡 | Recent upstream work improved bot team bootstrap fidelity; generalize to all human/bot combinations. | +| Remote movement/gait sync | 🟡 | Remote gait and position have received fixes; build stronger long-running multi-client tests. | +| Synchronized damage and death state | 🟡 | Ensure all clients see the same armor/internal, fall, destruction, and result state. | +| Arena chat channels | 🔬 | F7/F8 are local UI toggles; server-side fan-out depends on team/all-comm mode inference and team assignment. | +| Match orchestration | 🟡 | Shared combat can start from ready rooms; finish timers, disconnect policy, result settlement, and cleanup. | -**Verification:** Player can enter an arena, fire weapons, receive damage feedback, and reach a win/lose screen. +**Exit criteria:** Four or more clients can enter the same arena, split into sides, fight, see consistent remote state, and return to world cleanly. ---- +### M8 — Sanctioned Battles and SCentEx -### M7 — Multi-Client Combat - -**Goal:** Two human players can fight each other in real time. - -*Depends on M6.* - -Manual-backed arena staging model: the ready room exposes `MECH`, `SIDE`, and `STATUS`; `SIDE` offers eight sides, and players on the same side are teammates. Current implementation assumption: cap an arena ready room at **8 participants**, matching the eight-side model, unless stronger contrary evidence appears. **Sanctioned matches** use only arenas #1 and #2 per sector — results feed SCentEx (M9). The primary full-match use case is a **4v4 lance (8 total players)**. +**Goal:** Sanctioned play affects persistent rankings and player history like the retail service. | Task | Status | Notes | |---|---|---| -| Room broadcast | ❌ | Sync combat state to all clients in the same arena | -| Player enter / leave events | ✅ | Generic same-room `Cmd13` arrival / `Cmd11(status=0)` departure already work for arena rooms, `tools\\arena-room-smoke.mjs` live-validates arena ready-room arrival/departure visibility, and the lone-pilot combat fallback now uses the same departure/restore announcement path. | -| Side assignment enforcement | 🔧 | Arena scenes now expose `SIDE`, and same-side duel staging is rejected once both pilots explicitly pick the same side. Broader multi-party side-cap enforcement is still open. | -| Arena ready-room roster / listing model | 🔧 | Manual proves `MECH` / `SIDE` / `STATUS` and eight sides; current server assumption is max 8 participants. Arena entry now opens a live ready-room chooser, ready rooms are tracked as `Ready Room N` under each arena, `STATUS` titles/presence text/reconnect restore preserve that room identity, and a full room is rejected on selection instead of silently overfilling. Custom room naming, an explicit room-size selector, and explicit FFA/team-play labels remain unproven. | -| Synchronized position | 🔬 | Each client sees other mechs move in real time. Current local Ghidra lead: combat cmd `65` / wire `0x66` (`FUN_00401820`) parses player id, X/Y/Z, rotation-ish bytes, and speed/throttle-ish byte; constants differ from RazorWing/solaris. | -| Synchronized damage | ❌ | Damage dealt by one client is reflected in all clients' views | -| Match orchestration | 🔧 | Arena fights can now start as shared combat directly from `READY` when 2..8 pilots are in the same arena ready room. Remaining gaps are the full 15-minute match timer, end-of-match orchestration/settlement, and the sanctioned-match flag. | -| F7 — team / lance channel | 🔬 | Scoped broadcast to your lance teammates; v1.23 RE confirms F7 is local-only (no network packet). The server-side team-channel fan-out mechanism (identifying which clients are on the same lance) remains 🔬; wire format unknown. Requires `Cmd8` team assignment to be established. | -| F8 — all-comm channel | 🔬 | Broadcast to all players in the current arena match; v1.23 RE confirms F8 is local-only (no network packet). The all-comm delivery mechanism and any associated server→client command remain 🔬. | - -Live robustness coverage now also includes `tools\\duel-reconnect-restore-smoke.mjs`, which reconnects a participant during the post-duel restore window and confirms deferred settlement delivery plus selected-mech persistence on the replacement session. +| Sanctioned duel lifecycle | 🟡 | First duel progression and C-bill settlement exist; finish all arena/session edge cases. | +| Team Sanctioned Battles | 🔬 | v1.27 introduced support; v1.29 still needs explicit server-visible lifecycle RE and implementation. | +| SCentEx formula | 🔬 | Current ranking model is emulator-owned. Retail damage-inflicted/sustained and fame/rank math need RE or documented approximation. | +| Ranking/result display | 🟡 | `Cmd45`/`Cmd58` pages are accepted by v1.29; `Cmd41` score matrix is a likely results surface. Finish safe result/ranking routing. | +| Personnel and public history | 🟡 | Basic personnel records exist; finish battles-to-date, fame, rank, house, mech, and result history pages. | +| Durable result settlement | 🟡 | Duel results persist; make settlement idempotent across disconnect/reconnect/server restart. | -**Verification:** Two `MPBTWIN.EXE` instances connect, enter the same arena, see each other, and fight to completion. +**Exit criteria:** Two or more players complete sanctioned matches and can immediately see correct C-bill, fame/rank, personnel, and ranking/result updates from retail UI surfaces. ---- +### M9 — Operational Hardening -### M8 — Playable Game - -**Goal:** The emulator is complete enough for a real play session. - -*Depends on M7.* +**Goal:** The emulator can run unattended for real users. | Task | Status | Notes | |---|---|---| -| All 161 mechs loaded from real `.MEC` files | 🔧 | `loadMechs()` scans/parses `mechdata/*.MEC` in M1. Mech examine/status surfaces and the world mech picker now expose `.MEC`-derived tonnage, walk/run speed, and jump-jet presence across all variants; remaining work is actual gameplay integration (armor, weapons, heat, internal state). | -| Real Solaris arena layouts | ❌ | From M5 RE work | -| Correct mech stat handling (armor, weapons, heat) | 🔧 | `.MEC`-driven weapon/heat fidelity is no longer purely placeholder: weapon families now carry direct damage, cooldown, per-weapon S/M/L range caps, and mech heat-sink counts; combat uses the real long-range table from `screenshots/weapon-ranges.png` (including `AC/10 = 360m`) and bot range-band logic now uses explicit per-weapon S/M/L caps instead of generic `90/270` heuristics. Bot TIC/range planning also now follows the **usable** loadout instead of stale mounted range alone, so expected-damage and jump-fit decisions ignore ammo-depleted weapons. Remaining work is broader gameplay integration: armor/internal fidelity, heat/system-degradation behavior, and any additional retail-only stat surfaces still missing from RE. | -| Client launcher — `play.pcgi` generator | ✅ | `npm run gen-pcgi` already works | -| Basic observability (logs, session captures) | ✅ | Already implemented | -| Graceful disconnect / reconnect handling | 🔧 | ARIES type-`0x05` keepalive is now sent periodically by the server and echoed by the client, matching COMMEG32.DLL `FUN_100014e0` case `5`. `ARIES_KEEPALIVE_INTERVAL_MS` and `SOCKET_IDLE_TIMEOUT_MS` are configurable so long GUI validation sessions are not cut off by the old hardcoded 120-second idle timeout. Real two-GUI validation on 2026-04-07 confirmed both `MPBTWIN.EXE` sessions remained connected beyond 120 seconds and replied to repeated world keepalive pings. Lobby→world reconnect now restores the previous room, selected mech, and deferred duel-settlement notice; replacement-session settlement sync also covers disconnect/reconnect timing races. Longer mid-match recovery and broader world-session restoration are still incomplete. | - -**Verification:** Full play session — two humans, real mechs, real arena, fight to conclusion — with no manual intervention. - ---- +| Database migrations and backups | 🟡 | Schema/migrate tooling exists; add operational backup/restore guidance and migration checks. | +| Observability | 🟡 | Logs and captures exist; add structured event IDs for session, combat, settlement, and packet-failure analysis. | +| Long-running soak tests | ❌ | Add multi-client soak covering world idle, travel, ComStar, arena staging, combat, result restore, and reconnect. | +| Config and deployment | 🟡 | Docker and env config exist; harden for public server deployment. | +| Abuse/duplicate/session controls | 🟡 | Duplicate session handling exists; complete rate limits, stale locks, and admin recovery tools. | -### M9 — SCentEx / Persistence +**Exit criteria:** A public test weekend can run without manual database surgery or server restarts for routine failures. -**Goal:** Sanctioned matches produce persistent ranking results, matching original game behaviour. +### M10 — Completion Polish -*Depends on M8. Not optional — SCentEx existed in the original game.* +**Goal:** Close the difference between “playable” and “credible retail service.” | Task | Status | Notes | |---|---|---| -| SCentEx ranking model | ❌ | Damage inflicted vs. damage sustained determines rank change after each sanctioned match | -| Player fame stat | ❌ | Per-character fame tracked (BT-MAN p. 9) | -| Tier Ranking display | 🔧 | Tier/class ranking pages, match results, and v1.29-safe Cmd14 ranking detail are now served by the world layer. The submit path is already retail-shaped enough: inbound selection accepts both cmd-7 and world cmd-29 subtype 2, and later chooser surfaces still appear to submit through ordinary cmd-7. Remaining gap is the exact late-client outbound chooser preset/paging semantics: the counted Cmd57 row body is now largely understood, but the direct row callback only handles six visible row controls, so the 8-row tier chooser still needs the correct preset-strip state before it can safely replace the current Cmd7 compatibility menu. Live 2026-04-25 GUI + `cdb.exe` verification now also proves the current emulator still emits only the compatibility Cmd7 ranking choosers/results menus in real runs — no actual outbound `Cmd57` ranking packet exists in the running server yet. Preserved repo artifacts close the obvious archive fallback too: current captures/logs contain no ranking-side `Cmd57`, only later `CMD58_SCROLL_LIST_ID` + `CMD45_*RANKINGS` result-shell traffic. The active shared shell path also now correlates special control `0x131` with the literal stock `OPTN` glyph and surrounding `CSTR` / `EXIT` chrome, and the dynamic strip-slot table now resolves to stock `MW_MPICS.DAT` world/location art (`I101..I184`, including `ACADEMY`, `STEINER STADIUM`, `DAVION ARENA`, and HQ-style emblems), which makes guessing a ranking-safe preset even riskier. | -| Personnel record | 🔬 | First page is now identified: `Cmd7(0x3f2, target_id + 1)` triggers world `Cmd14_PersonnelRecord` (`0x2f`), which displays the selected handle, ComStar ID, battles-to-date, and six server-formatted text lines. Follow-up trace on the built-in `Cmd7(0x95, 2)` `More` request did not reveal a distinct second-page command handler; strongest current inference is that later pages are delivered as additional `Cmd14` payloads. A minimal two-page server prototype is now implemented on the branch and passes a direct world-session socket smoke (`Cmd48 -> Cmd14 page 1 -> Cmd7(0x95, 2) -> Cmd14 page 2`). New client disassembly confirms the remaining header limitation too: `Cmd14` takes header `ID` from packet payload, but header `Handle` still comes from the client-local room-roster cursor, so the server cannot currently make both header values target-correct at once. Remaining unknowns: exact mapping of the six text lines and the meaning of two legacy/unused `type4` payload slots. | -| SCentEx result reporting protocol | 🔬 | Duel results are now persisted and feed the emulator ranking pages, but the exact retail reporting/routing path into the later ranking UI is still unresolved | +| Retail comparison matrix | ❌ | For every known retail surface, list implemented / approximate / intentionally absent. | +| Manual-backed gameplay audit | ❌ | Re-read BT manual against implemented world, mech, combat, and SCentEx behavior. | +| Compatibility docs | ❌ | Document exact required client version, assets, launcher flow, optional windowing shim, and known client quirks. | +| Admin/player docs | ❌ | Account creation, server setup, backup, troubleshooting, and play guide. | +| Release candidate test plan | ❌ | Freeze feature surface and run full regression checklist against clean v1.29 installs. | -**Verification:** Two players complete a sanctioned match; both observe updated rankings at a bar terminal. - ---- +**Exit criteria:** A new operator can deploy the server and a new player can connect with a retail v1.29 install using only documented steps. ## RE Priority Queue -Work these in order when sitting down with Ghidra: - -1. ~~**`FUN_100014e0` case 0** (`COMMEG32.DLL`) — secondary connection handler.~~ ✅ Resolved (RESEARCH.md §17) -2. ~~**World command dispatch table** (`MPBTWIN.EXE`).~~ ✅ Resolved (RESEARCH.md §18) -3. ~~**Initial world handshake**.~~ ✅ Resolved (RESEARCH.md §18) -4. ~~**Cmd 20 server response** (`FUN_00401c90`).~~ ✅ Resolved (M1 complete) -5. ~~**Combat CRC crossover**.~~ ✅ Resolved — `g_combatMode` flag selects seed; RPS=`0x0a5c25`, Combat=`0x0a5c45` (RESEARCH.md §18) -6. ~~**`SOLARIS.MAP` / `IS.MAP` exit graph** — decode room-to-room connections.~~ ✅ Leading room tables fully decoded (RESEARCH.md §19.7); provisional exit tree implemented in server-world.ts; authentic exit graph from trailing section still needs RE. -7. **Non-death fall / recovery local state** — why does the retail client remain upright even after proven local `Cmd70` fall/collapse/recover probes, and what additional local state unlocks wire `cmd12/action0` stand-up? -8. **F7 / F8 chat channel wire format** — are team and all-comm differentiated by command code or a flag in the packet? (M7 prerequisite; both channels require `Cmd8` team assignment and are arena-phase only). -9. **Movement packets** — ✅ DECODED (RESEARCH.md §19.2): Cmd8 (coasting) and Cmd9 (moving) client→server formats fully mapped; bias/divisor constants confirmed. Server→client position (Cmd65) implemented and confirmed in live combat test. -10. **Weapon fire / damage packets** — ✅ DECODED (RESEARCH.md §19.3 / §19.9): weapon fire acceptance is now grounded primarily on `cmd10` shot geometry, while recovery-side RE shows `cmd12/action 0` is reused for stand-up when already down rather than being a clean dedicated ordinary-fire opcode. Server `Cmd62` (wire `0x5F`) unblocks the fire gate by clearing `DAT_0047ef60` bit `0x20`. Damage model (Cmd66/Cmd67) is partially decoded; round-trip hit confirmation is strong enough for current combat play, but deeper retail damage/heat fidelity still needs capture. | -11. **TIC circuit wire format** (M6 prerequisite). -12. **Jump jet / altitude state packets** (M6 prerequisite) — fire command decoded; fuel/regen/Z-altitude still 🔬. -13. **Turn timer / sanctioned match lifecycle** (M6/M7 prerequisite) — local win/loss disconnect path is understood, but the shared 15-minute arena timer and broader sanctioned settlement/orchestration are still open. -14. **SCentEx result reporting** (M9 prerequisite). - ---- - -## Known Unknowns +Work these in order when doing Ghidra / live-client sessions: -These are gaps we know exist. They are not bugs — they are the RE frontier. +1. **Team Sanctioned Battles**: server-visible lifecycle, side/team assignment details, result aggregation, and whether any v1.27+ opcode family is involved. +2. **SCentEx formula and result routing**: rank/fame/C-bill math, `Cmd41` score matrix role, and persistent public history. +3. **Combat heat/damage/critical fidelity**: heat buildup/dissipation, shutdown, ammo explosions, weapon disablement, section labels, and death/cripple boundaries. +4. **Jump/altitude edge cases**: fuel drain, recharge, landing, jump-jet damage, DFA/physical attacks if present. +5. **Arena chat channels**: F7/F8 local UI state and the server-visible distinction for team/all-comm delivery. +6. **World topology and facility menus**: authentic room graph, bar terminals, ComStar global entry, bank/terminal/hub actions. +7. **`Cmd57` safe builder**: prove the chooser preset/control-strip contract before using it for production ranking or menu flows. +8. **v1.28+ anti-hack/guard surfaces**: confirm whether any runtime guard affects server-visible behavior in v1.29. +9. **Long-run retail-client timing**: keep validating 60 FPS cap, result timers, fall/recovery timing, and focus/window repaint quirks. +10. **Post-completion mech-management surfaces**: exact `Cmd26` → `Cmd30` → `Cmd31` / `Cmd39` row order and submit behavior for repair/reload/buy-extra-ammo/name-mech, kept as optional future work unless new evidence shows GameStorm used it. -- **`SOLARIS.MAP` / `IS.MAP` exit graph** — leading room tables are fully decoded (RESEARCH.md §19.7); the trailing binary section (picture/resource data) still needs a separate movement/topology RE pass to extract authentic room-to-room exit connections and room-type classifications. -- **F7 / F8 chat channel differentiation** — two distinct broadcast channels exist (team/lance and all-comm); both are arena-phase constructs gated on `Cmd8` team assignment; wire-format difference is unknown. Tracked in M7. -- **Bar booth terminal commands** — `KP5` → `Cmd48` all-roster query and `Cmd7(0x3f2)` personnel record are implemented; Tier Ranking terminal activation format is still unknown. -- **Arena ready-room creation / listing UI** — manual evidence proves `MECH`, `SIDE`, and `STATUS` in the ready room plus an eight-side team model; current server work assumes up to 8 participants, but custom room naming, an explicit room-size selector, and explicit FFA/team-play labels remain unproven. -- **Tram / monorail command** — ✅ **RESOLVED** (RESEARCH.md §19.10): T.O.F.S. uses the same `cmd5 actionType 4 → Cmd43 → cmd10` path as regular Solaris travel; no separate tram command. Closes issue #70. -- **SCentEx result-reporting protocol** — how does the server communicate sanctioned match results? -- **Non-death fall / recovery local state** — retail still stays upright after every meaningful server-side `Cmd70` probe tried so far, including local `1->8->0`. Ghidra now says F12 stand-up should become wire `cmd12/action0` only when the client is truly down, but the latest live `legrecover` validation still produced no `cmd12/action0`, no `cmd10`, and no posture change. The missing state transition that makes the client consider itself recoverable is still unknown. -- **Server→client combat position sync (`Cmd65`)** — implemented and live-confirmed for bootstrap/movement echo, but fuller field semantics for remote multi-client sync still need stronger capture confirmation. -- **TIC group fire** — whether `cmd 12/action 0` means selected weapon, selected TIC group, or all queued fire needs dynamic capture to confirm. -- **Jump jet fuel / Z-altitude state** — fire (`cmd 12/action 4`) and landing (`cmd 12/action 6`) decoded; fuel depletion, regeneration rate, and server→client altitude feedback still unknown. -- **Turn timer / sanctioned match lifecycle** — the local win/loss disconnect path is now understood, but the shared 15-minute arena timer, mech-kill broadcast/settlement behavior, and sanctioned-match orchestration are still unconfirmed. +## Known Unknowns ---- +- Exact retail SCentEx ranking formula and Team Sanctioned Battle settlement. +- Authentic Solaris room topology beyond decoded map room records. +- Full mech bay repair/reload/extra-ammo/name-mech contracts in v1.29, treated as post-completion client-capability research rather than required 1999 GameStorm behavior. +- Whether `Cmd57` can be safely used for production choosers without drawing unrelated stock controls. +- Full heat, shutdown, critical, ammo explosion, and physical-combat behavior. +- Complete arena team/all-comm delivery semantics. +- Whether v1.28+ anti-hack strings have any server-visible consequence. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow. -See [RESEARCH.md](RESEARCH.md) for all confirmed protocol details and RE methodology. +See [RESEARCH.md](RESEARCH.md) for confirmed protocol details, binary notes, and RE methodology. -If you have access to Ghidra and want to help, the RE Priority Queue above is where to start. Open a **Research Finding** issue with your findings before opening a PR. +For new RE findings, update `RESEARCH.md` first, then adjust this roadmap only after the finding changes implementation scope or completion criteria. diff --git a/mpbt-server.err.log b/mpbt-server.err.log new file mode 100644 index 0000000..a6435bb --- /dev/null +++ b/mpbt-server.err.log @@ -0,0 +1,2 @@ +[world] loaded 32 rooms from SOLARIS.MAP +[world] loaded world-map.json (183 rooms) diff --git a/mpbt-server.out.log b/mpbt-server.out.log new file mode 100644 index 0000000..700a74c --- /dev/null +++ b/mpbt-server.out.log @@ -0,0 +1,1728 @@ +2026-04-27T09:46:00.836Z INFO [server] Loaded 161 mechs from mechdata/ +2026-04-27T09:46:00.840Z INFO [server] [world] ══════════════════════════════════════════════ +2026-04-27T09:46:00.841Z INFO [server] [world] Game World Server (M3) +2026-04-27T09:46:00.841Z INFO [server] [world] Listening on 0.0.0.0:2001 +2026-04-27T09:46:00.841Z INFO [server] [world] CRC seed: 0x0A5C25 (RPS / MMW path) +2026-04-27T09:46:00.842Z INFO [server] [world] ══════════════════════════════════════════════ +2026-04-27T09:46:00.842Z INFO [server] ═══════════════════════════════════════════════════════ +2026-04-27T09:46:00.842Z INFO [server] MPBT ARIES Server Emulator +2026-04-27T09:46:00.842Z INFO [server] Listening on 0.0.0.0:2000 +2026-04-27T09:46:00.844Z INFO [server] Hostname: Moozeo +2026-04-27T09:46:00.845Z INFO [server] Protocol: ARIES binary (12-byte header, confirmed by RE) +2026-04-27T09:46:00.845Z INFO [server] play.pcgi server=127.0.0.1:2000 +2026-04-27T09:46:00.845Z INFO [server] Log level: DEBUG → logs/server.log +2026-04-27T09:46:00.845Z INFO [server] Packet captures: enabled → captures/ +2026-04-27T09:46:00.846Z INFO [server] ═══════════════════════════════════════════════════════ +2026-04-27T09:46:20.218Z INFO [server:fdbf716d] Client connected from 192.168.1.109:62916 (session fdbf716d-c4e4-4f0a-8e73-242c1907ee6c) +2026-04-27T09:46:20.219Z INFO [server:fdbf716d] Sending LOGIN_REQUEST (0x16) — 12 bytes +2026-04-27T09:46:20.222Z DEBUG [server:fdbf716d] recv 346 bytes (total=346, phase=auth) +2026-04-27T09:46:20.222Z DEBUG [server:fdbf716d] pkt type=0x15 tag=0x0 payloadLen=334 +2026-04-27T09:46:20.223Z DEBUG [server:fdbf716d] [rx] +00000000 4d 6f 6f 73 65 00 00 00 00 00 00 00 00 00 00 00 |Moose...........| +00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 45 6e 67 69 6e |Kesmai CommEngin| +00000080 65 20 33 2e 32 39 00 00 00 00 00 00 00 00 00 00 |e 3.29..........| +00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000e0 00 00 00 00 00 00 00 00 67 00 00 00 00 00 00 00 |........g.......| +000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 42 |..............9B| +00000140 00 00 00 09 31 32 4d 50 42 54 34 75 2e 00 |....12MPBT4u..| +2026-04-27T09:46:20.224Z DEBUG [server:fdbf716d] [auth] login payload len=334 +00000000 4d 6f 6f 73 65 00 00 00 00 00 00 00 00 00 00 00 |Moose...........| +00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 45 6e 67 69 6e |Kesmai CommEngin| +00000080 65 20 33 2e 32 39 00 00 00 00 00 00 00 00 00 00 |e 3.29..........| +00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000e0 00 00 00 00 00 00 00 00 67 00 00 00 00 00 00 00 |........g.......| +000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 42 |..............9B| +00000140 00 00 00 09 31 32 4d 50 42 54 34 75 2e 00 |....12MPBT4u..| +2026-04-27T09:46:20.225Z INFO [server:fdbf716d] [auth] login from "Moose" ver="Kesmai CommEngine 3.29" service="g" pwLen=9 +2026-04-27T09:46:20.226Z INFO [server:fdbf716d] [login] credentials received: user="Moose" service="g" clientVer="Kesmai CommEngine 3.29" +2026-04-27T09:46:20.564Z INFO [server:fdbf716d] [login] authenticated: user="Moose" (id=15) +2026-04-27T09:46:20.565Z INFO [server:fdbf716d] [login] sending SYNC ack — 12 bytes +2026-04-27T09:46:20.565Z INFO [server:fdbf716d] [login] sending WELCOME escape — 45 bytes +2026-04-27T09:46:20.575Z DEBUG [server:fdbf716d] recv 23 bytes (total=369, phase=lobby) +2026-04-27T09:46:20.576Z DEBUG [server:fdbf716d] pkt type=0x00 tag=0x0 payloadLen=11 +2026-04-27T09:46:20.576Z DEBUG [server:fdbf716d] [rx] +00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| +2026-04-27T09:46:20.577Z DEBUG [server:fdbf716d] [game] rx type-0 len=11 +00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| +2026-04-27T09:46:20.578Z DEBUG [server:fdbf716d] [game] client seq=0 cmd=3 phase=lobby +2026-04-27T09:46:20.583Z INFO [server:fdbf716d] [game] character found: displayName="Moose" allegiance=Marik mech_id=98 mech_slot=36 → REDIRECT to world +2026-04-27T09:46:20.583Z INFO [server:fdbf716d] [game] recorded world launch: displayName="Moose" allegiance=Marik mech=CPLT-C1 (id=98) +2026-04-27T09:46:20.584Z INFO [server:fdbf716d] [game] sending REDIRECT → 192.168.1.109:2001 +2026-04-27T09:46:20.585Z INFO [server:fdbf716d] Client disconnected (phase=closing, bytes=369) +2026-04-27T09:46:20.591Z INFO [server:f38959b6] [world] client connected from 192.168.1.109:62918 (session f38959b6-6677-4338-b7ea-b9602eb33eb0) +2026-04-27T09:46:20.591Z INFO [server:f38959b6] [world] sending LOGIN_REQUEST (12 bytes) +2026-04-27T09:46:20.592Z DEBUG [server:f38959b6] [world] recv 337 bytes (total=337, phase=auth) +2026-04-27T09:46:20.593Z DEBUG [server:f38959b6] [world] pkt type=0x15 tag=0x0 payloadLen=325 +2026-04-27T09:46:20.593Z DEBUG [server:f38959b6] [world][rx] +00000000 4d 6f 6f 73 65 00 00 00 00 00 00 00 00 00 00 00 |Moose...........| +00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 45 6e 67 69 6e |Kesmai CommEngin| +00000080 65 20 33 2e 32 39 00 00 00 00 00 00 00 00 00 00 |e 3.29..........| +00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000e0 00 00 00 00 00 00 00 00 31 39 32 2e 31 36 38 2e |........192.168.| +000000f0 31 2e 31 30 39 3a 32 30 30 31 00 00 00 00 00 00 |1.109:2001......| +00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 42 |..............9B| +00000140 00 00 00 00 00 |.....| +2026-04-27T09:46:20.594Z DEBUG [server:f38959b6] [auth] login payload len=325 +00000000 4d 6f 6f 73 65 00 00 00 00 00 00 00 00 00 00 00 |Moose...........| +00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 45 6e 67 69 6e |Kesmai CommEngin| +00000080 65 20 33 2e 32 39 00 00 00 00 00 00 00 00 00 00 |e 3.29..........| +00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000e0 00 00 00 00 00 00 00 00 31 39 32 2e 31 36 38 2e |........192.168.| +000000f0 31 2e 31 30 39 3a 32 30 30 31 00 00 00 00 00 00 |1.109:2001......| +00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 42 |..............9B| +00000140 00 00 00 00 00 |.....| +2026-04-27T09:46:20.595Z INFO [server:f38959b6] [auth] login from "Moose" ver="Kesmai CommEngine 3.29" service="192.168.1.109:2001" pwLen=0 +2026-04-27T09:46:20.595Z INFO [server:f38959b6] [world-login] launch record found: displayName="Moose" allegiance=Marik mech=CPLT-C1 (id=98 slot=36 rosterId=100015 room=146) +2026-04-27T09:46:20.595Z INFO [server:f38959b6] [world-login] accepted: user="Moose" displayName="Moose" allegiance=Marik service="192.168.1.109:2001" +2026-04-27T09:46:20.596Z INFO [server:f38959b6] [world-login] sending SYNC ack +2026-04-27T09:46:20.596Z INFO [server:f38959b6] [world-login] sending WELCOME escape (45 bytes) +2026-04-27T09:46:20.598Z DEBUG [server:f38959b6] [world] recv 23 bytes (total=360, phase=world) +2026-04-27T09:46:20.598Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=11 +2026-04-27T09:46:20.598Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| +2026-04-27T09:46:20.599Z DEBUG [server:f38959b6] [world] rx type-0 len=11 +00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| +2026-04-27T09:46:20.599Z DEBUG [server:f38959b6] [world] client seq=0 cmd=3 +2026-04-27T09:46:20.600Z INFO [server:f38959b6] [world] cmd-3 (client-ready) → sending world init sequence +2026-04-27T09:46:20.600Z INFO [server:f38959b6] [world] sending Cmd4 SceneInit (logicalRoom=146 sceneRoom=146 header="Solaris Starport" detail="Click on the stone arch icon to the east for a tutorial if you are new to Solaris City and need help. Click on the tram icon to the south if you know your way around and you're ready to go find a fight. Remember players with [MPBT] in front of their names are the game staff. They will help to teach you the game. Other players that want to train you or that want you to join their group may not have the best intentions.") +2026-04-27T09:46:20.601Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) +2026-04-27T09:46:20.602Z INFO [server:f38959b6] [world] world init sequence complete +2026-04-27T09:46:20.603Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" +2026-04-27T09:46:29.537Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=380, phase=world) +2026-04-27T09:46:29.537Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:46:29.537Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 26 2c 2c 65 4c 1b |.!&,,eL.| +2026-04-27T09:46:29.537Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 26 2c 2c 65 4c 1b |.!&,,eL.| +2026-04-27T09:46:29.538Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 +2026-04-27T09:46:29.538Z INFO [server:f38959b6] [world] cmd-5 scene action: type=11 +2026-04-27T09:46:29.538Z INFO [server:f38959b6] [world] sending Cmd43 Solaris travel map: logicalRoomId=146 sceneRoomId=146 +2026-04-27T09:46:30.480Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=406, phase=world) +2026-04-27T09:46:30.480Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:46:30.481Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2b 23 3d 21 21 21 22 74 41 5f 45 1b |.!+#=!!!"tA_E.| +2026-04-27T09:46:30.481Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 2b 23 3d 21 21 21 22 74 41 5f 45 1b |.!+#=!!!"tA_E.| +2026-04-27T09:46:30.481Z DEBUG [server:f38959b6] [world] client seq=0 cmd=10 +2026-04-27T09:46:30.482Z INFO [server:f38959b6] [world] cmd-10 map reply: context=198 selection=168 selectedRoomId=167 +2026-04-27T09:46:30.482Z INFO [server:f38959b6] [world] sending arena ready-room menu: arena=167 options=1 +2026-04-27T09:46:32.609Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=432, phase=world) +2026-04-27T09:46:32.701Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:46:32.701Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 71 21 21 21 21 22 43 31 4b 1b |.!(,q!!!!"C1K.| +2026-04-27T09:46:32.701Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 71 21 21 21 21 22 43 31 4b 1b |.!(,q!!!!"C1K.| +2026-04-27T09:46:32.701Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:46:32.702Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1015 selection=1 +2026-04-27T09:46:32.703Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" +2026-04-27T09:46:32.703Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" +2026-04-27T09:46:32.704Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) +2026-04-27T09:46:32.704Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" +2026-04-27T09:46:40.170Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=458, phase=world) +2026-04-27T09:46:40.170Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:46:40.170Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| +2026-04-27T09:46:40.171Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| +2026-04-27T09:46:40.171Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:46:50.597Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:46:50.598Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=470, phase=world) +2026-04-27T09:46:50.599Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:46:50.599Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:46:54.593Z DEBUG [server:f38959b6] [world] recv 31 bytes (total=501, phase=world) +2026-04-27T09:46:54.593Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=19 +2026-04-27T09:46:54.594Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2b 2f 62 6f 74 64 69 66 66 20 33 51 |.!%!+/botdiff 3Q| +00000010 54 50 1b |TP.| +2026-04-27T09:46:54.594Z DEBUG [server:f38959b6] [world] rx type-0 len=19 +00000000 1b 21 25 21 2b 2f 62 6f 74 64 69 66 66 20 33 51 |.!%!+/botdiff 3Q| +00000010 54 50 1b |TP.| +2026-04-27T09:46:54.594Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:46:54.595Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botdiff 3 +2026-04-27T09:46:54.596Z INFO [server:f38959b6] [world] /botdifficulty: configured bot difficulty=3 +2026-04-27T09:47:02.713Z DEBUG [server:f38959b6] [world] recv 32 bytes (total=533, phase=world) +2026-04-27T09:47:02.713Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=20 +2026-04-27T09:47:02.714Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2c 2f 62 6f 74 63 6f 75 6e 74 20 37 |.!%!,/botcount 7| +00000010 40 56 4f 1b |@VO.| +2026-04-27T09:47:02.714Z DEBUG [server:f38959b6] [world] rx type-0 len=20 +00000000 1b 21 25 21 2c 2f 62 6f 74 63 6f 75 6e 74 20 37 |.!%!,/botcount 7| +00000010 40 56 4f 1b |@VO.| +2026-04-27T09:47:02.714Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:47:02.715Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botcount 7 +2026-04-27T09:47:02.715Z INFO [server:f38959b6] [world] /botcount: configured 7 bot opponents +2026-04-27T09:47:20.599Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:47:20.600Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=545, phase=world) +2026-04-27T09:47:20.600Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:47:20.601Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:47:23.368Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=565, phase=world) +2026-04-27T09:47:23.368Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:47:23.369Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| +2026-04-27T09:47:23.369Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| +2026-04-27T09:47:23.369Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 +2026-04-27T09:47:23.369Z INFO [server:f38959b6] [world] cmd-5 scene action: type=10 +2026-04-27T09:47:23.370Z INFO [server:f38959b6] [world] sending arena status list (1 entries) +2026-04-27T09:47:27.057Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=591, phase=world) +2026-04-27T09:47:27.058Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:47:27.058Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:47:27.058Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:47:27.059Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:47:27.059Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 +2026-04-27T09:47:27.059Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 +2026-04-27T09:47:28.240Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=611, phase=world) +2026-04-27T09:47:28.240Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:47:28.241Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 26 2a 2c 65 34 1b |.!&*,e4.| +2026-04-27T09:47:28.241Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 26 2a 2c 65 34 1b |.!&*,e4.| +2026-04-27T09:47:28.241Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 +2026-04-27T09:47:28.242Z INFO [server:f38959b6] [world] cmd-5 scene action: type=9 +2026-04-27T09:47:28.242Z INFO [server:f38959b6] [world] sending arena side menu +2026-04-27T09:47:30.922Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=637, phase=world) +2026-04-27T09:47:30.922Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:47:30.923Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 70 21 21 21 21 22 43 2f 75 1b |.!(,p!!!!"C/u.| +2026-04-27T09:47:30.923Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 70 21 21 21 21 22 43 2f 75 1b |.!(,p!!!!"C/u.| +2026-04-27T09:47:30.923Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:47:30.923Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1014 selection=1 +2026-04-27T09:47:30.924Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" +2026-04-27T09:47:30.924Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) +2026-04-27T09:47:30.925Z INFO [server:f38959b6] [world] sending arena status list (1 entries) +2026-04-27T09:47:30.926Z INFO [server:f38959b6] [world] arena side selected: callsign="Moose" side=1 room=167 +2026-04-27T09:47:34.970Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=663, phase=world) +2026-04-27T09:47:34.970Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:47:34.970Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:47:34.971Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:47:34.971Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:47:34.971Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 +2026-04-27T09:47:34.971Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 +2026-04-27T09:47:38.304Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=689, phase=world) +2026-04-27T09:47:38.305Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:47:38.305Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| +2026-04-27T09:47:38.306Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| +2026-04-27T09:47:38.306Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:47:50.601Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:47:50.602Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=701, phase=world) +2026-04-27T09:47:50.603Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:47:50.603Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:48:13.385Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=734, phase=world) +2026-04-27T09:48:13.385Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:48:13.385Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 31 20 |.!%!-/botside 1 | +00000010 31 59 30 42 1b |1Y0B.| +2026-04-27T09:48:13.386Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 31 20 |.!%!-/botside 1 | +00000010 31 59 30 42 1b |1Y0B.| +2026-04-27T09:48:13.386Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:48:13.386Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 1 1 +2026-04-27T09:48:13.387Z INFO [server:f38959b6] [world] /botside: slot=1 configured side=1 +2026-04-27T09:48:20.607Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:48:20.608Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=746, phase=world) +2026-04-27T09:48:20.609Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:48:20.609Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:48:22.855Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=766, phase=world) +2026-04-27T09:48:22.856Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:48:22.857Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 38 23 2c 63 2a 1b |.!8#,c*.| +2026-04-27T09:48:22.857Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 38 23 2c 63 2a 1b |.!8#,c*.| +2026-04-27T09:48:22.857Z DEBUG [server:f38959b6] [world] client seq=0 cmd=23 +2026-04-27T09:48:22.858Z INFO [server:f38959b6] [world] cmd-23 location action: room=167 slot=2 cached=true -> room=2138 +2026-04-27T09:48:22.858Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" +2026-04-27T09:48:22.858Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=2138 sceneRoomId=169 header="Halloran St" +2026-04-27T09:48:22.859Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) +2026-04-27T09:48:22.859Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" +2026-04-27T09:48:23.701Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=786, phase=world) +2026-04-27T09:48:23.701Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:48:23.702Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 38 28 2c 62 2b 1b |.!8(,b+.| +2026-04-27T09:48:23.702Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 38 28 2c 62 2b 1b |.!8(,b+.| +2026-04-27T09:48:23.702Z DEBUG [server:f38959b6] [world] client seq=0 cmd=23 +2026-04-27T09:48:23.703Z INFO [server:f38959b6] [world] cmd-23 location action: room=2138 slot=3 cached=false -> room=167 +2026-04-27T09:48:23.703Z INFO [server:f38959b6] [world] sending arena ready-room menu: arena=167 options=1 +2026-04-27T09:48:25.641Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=812, phase=world) +2026-04-27T09:48:25.642Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:48:25.642Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 71 21 21 21 21 22 43 31 4b 1b |.!(,q!!!!"C1K.| +2026-04-27T09:48:25.642Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 71 21 21 21 21 22 43 31 4b 1b |.!(,q!!!!"C1K.| +2026-04-27T09:48:25.643Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:48:25.643Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1015 selection=1 +2026-04-27T09:48:25.643Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" +2026-04-27T09:48:25.643Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" +2026-04-27T09:48:25.644Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) +2026-04-27T09:48:25.644Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" +2026-04-27T09:48:28.465Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=838, phase=world) +2026-04-27T09:48:28.465Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:48:28.466Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| +2026-04-27T09:48:28.466Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| +2026-04-27T09:48:28.466Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:48:31.208Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=858, phase=world) +2026-04-27T09:48:31.209Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:48:31.209Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| +2026-04-27T09:48:31.209Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| +2026-04-27T09:48:31.210Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 +2026-04-27T09:48:31.210Z INFO [server:f38959b6] [world] cmd-5 scene action: type=10 +2026-04-27T09:48:31.210Z INFO [server:f38959b6] [world] sending arena status list (1 entries) +2026-04-27T09:48:35.945Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=884, phase=world) +2026-04-27T09:48:35.945Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:48:35.946Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:48:35.946Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:48:35.946Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:48:35.947Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 +2026-04-27T09:48:35.947Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 +2026-04-27T09:48:37.408Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=904, phase=world) +2026-04-27T09:48:37.408Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:48:37.408Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 26 2a 2c 65 34 1b |.!&*,e4.| +2026-04-27T09:48:37.409Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 26 2a 2c 65 34 1b |.!&*,e4.| +2026-04-27T09:48:37.409Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 +2026-04-27T09:48:37.409Z INFO [server:f38959b6] [world] cmd-5 scene action: type=9 +2026-04-27T09:48:37.409Z INFO [server:f38959b6] [world] sending arena side menu +2026-04-27T09:48:38.778Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=930, phase=world) +2026-04-27T09:48:38.778Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:48:38.778Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 70 21 21 21 21 22 43 2f 75 1b |.!(,p!!!!"C/u.| +2026-04-27T09:48:38.779Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 70 21 21 21 21 22 43 2f 75 1b |.!(,p!!!!"C/u.| +2026-04-27T09:48:38.779Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:48:38.779Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1014 selection=1 +2026-04-27T09:48:38.779Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" +2026-04-27T09:48:38.780Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) +2026-04-27T09:48:38.780Z INFO [server:f38959b6] [world] sending arena status list (1 entries) +2026-04-27T09:48:38.780Z INFO [server:f38959b6] [world] arena side selected: callsign="Moose" side=1 room=167 +2026-04-27T09:48:40.904Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=950, phase=world) +2026-04-27T09:48:40.904Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:48:40.905Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| +2026-04-27T09:48:40.905Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| +2026-04-27T09:48:40.905Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 +2026-04-27T09:48:40.905Z INFO [server:f38959b6] [world] cmd-5 scene action: type=10 +2026-04-27T09:48:40.906Z INFO [server:f38959b6] [world] sending arena status list (1 entries) +2026-04-27T09:48:42.969Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=976, phase=world) +2026-04-27T09:48:42.970Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:48:42.970Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:48:42.970Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:48:42.971Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:48:42.971Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 +2026-04-27T09:48:42.971Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 +2026-04-27T09:48:45.440Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=1002, phase=world) +2026-04-27T09:48:45.441Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:48:45.441Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| +2026-04-27T09:48:45.442Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| +2026-04-27T09:48:45.442Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:48:47.744Z DEBUG [server:f38959b6] [world] recv 31 bytes (total=1033, phase=world) +2026-04-27T09:48:47.745Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=19 +2026-04-27T09:48:47.745Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2b 2f 62 6f 74 64 69 66 66 20 33 51 |.!%!+/botdiff 3Q| +00000010 54 50 1b |TP.| +2026-04-27T09:48:47.745Z DEBUG [server:f38959b6] [world] rx type-0 len=19 +00000000 1b 21 25 21 2b 2f 62 6f 74 64 69 66 66 20 33 51 |.!%!+/botdiff 3Q| +00000010 54 50 1b |TP.| +2026-04-27T09:48:47.745Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:48:47.746Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botdiff 3 +2026-04-27T09:48:47.746Z INFO [server:f38959b6] [world] /botdifficulty: configured bot difficulty=3 +2026-04-27T09:48:50.614Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:48:50.615Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=1045, phase=world) +2026-04-27T09:48:50.615Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:48:50.615Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:48:56.874Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1078, phase=world) +2026-04-27T09:48:56.874Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:48:56.875Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 31 20 |.!%!-/botside 1 | +00000010 31 59 30 42 1b |1Y0B.| +2026-04-27T09:48:56.875Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 31 20 |.!%!-/botside 1 | +00000010 31 59 30 42 1b |1Y0B.| +2026-04-27T09:48:56.875Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:48:56.875Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 1 1 +2026-04-27T09:48:56.876Z INFO [server:f38959b6] [world] /botside: slot=1 configured side=1 +2026-04-27T09:48:58.921Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1111, phase=world) +2026-04-27T09:48:58.921Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:48:58.921Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 32 20 |.!%!-/botside 2 | +00000010 31 59 30 72 1b |1Y0r.| +2026-04-27T09:48:58.922Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 32 20 |.!%!-/botside 2 | +00000010 31 59 30 72 1b |1Y0r.| +2026-04-27T09:48:58.922Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:48:58.922Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 2 1 +2026-04-27T09:48:58.922Z INFO [server:f38959b6] [world] /botside: slot=2 configured side=1 +2026-04-27T09:49:01.384Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1144, phase=world) +2026-04-27T09:49:01.385Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:49:01.385Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 33 20 |.!%!-/botside 3 | +00000010 31 59 31 4d 1b |1Y1M.| +2026-04-27T09:49:01.386Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 33 20 |.!%!-/botside 3 | +00000010 31 59 31 4d 1b |1Y1M.| +2026-04-27T09:49:01.386Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:49:01.386Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 3 1 +2026-04-27T09:49:01.387Z INFO [server:f38959b6] [world] /botside: slot=3 configured side=1 +2026-04-27T09:49:05.224Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1177, phase=world) +2026-04-27T09:49:05.225Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:49:05.225Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 34 20 |.!%!-/botside 4 | +00000010 31 59 2f 67 1b |1Y/g.| +2026-04-27T09:49:05.225Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 34 20 |.!%!-/botside 4 | +00000010 31 59 2f 67 1b |1Y/g.| +2026-04-27T09:49:05.225Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:49:05.226Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 4 1 +2026-04-27T09:49:05.226Z INFO [server:f38959b6] [world] /botside: slot=4 configured side=1 +2026-04-27T09:49:09.544Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1210, phase=world) +2026-04-27T09:49:09.545Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:49:09.545Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 34 20 |.!%!-/botside 4 | +00000010 32 59 2f 73 1b |2Y/s.| +2026-04-27T09:49:09.546Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 34 20 |.!%!-/botside 4 | +00000010 32 59 2f 73 1b |2Y/s.| +2026-04-27T09:49:09.546Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:49:09.546Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 4 2 +2026-04-27T09:49:09.546Z INFO [server:f38959b6] [world] /botside: slot=4 configured side=2 +2026-04-27T09:49:12.953Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1243, phase=world) +2026-04-27T09:49:12.954Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:49:12.954Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 35 20 |.!%!-/botside 5 | +00000010 32 59 2f 43 1b |2Y/C.| +2026-04-27T09:49:12.954Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 35 20 |.!%!-/botside 5 | +00000010 32 59 2f 43 1b |2Y/C.| +2026-04-27T09:49:12.954Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:49:12.955Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 5 2 +2026-04-27T09:49:12.955Z INFO [server:f38959b6] [world] /botside: slot=5 configured side=2 +2026-04-27T09:49:16.128Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1276, phase=world) +2026-04-27T09:49:16.129Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:49:16.129Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 36 20 |.!%!-/botside 6 | +00000010 32 59 2e 68 1b |2Y.h.| +2026-04-27T09:49:16.129Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 36 20 |.!%!-/botside 6 | +00000010 32 59 2e 68 1b |2Y.h.| +2026-04-27T09:49:16.129Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:49:16.130Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 6 2 +2026-04-27T09:49:16.130Z INFO [server:f38959b6] [world] /botside: slot=6 configured side=2 +2026-04-27T09:49:18.280Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1309, phase=world) +2026-04-27T09:49:18.281Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 +2026-04-27T09:49:18.281Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 37 20 |.!%!-/botside 7 | +00000010 32 59 2f 23 1b |2Y/#.| +2026-04-27T09:49:18.281Z DEBUG [server:f38959b6] [world] rx type-0 len=21 +00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 37 20 |.!%!-/botside 7 | +00000010 32 59 2f 23 1b |2Y/#.| +2026-04-27T09:49:18.281Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 +2026-04-27T09:49:18.282Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 7 2 +2026-04-27T09:49:18.282Z INFO [server:f38959b6] [world] /botside: slot=7 configured side=2 +2026-04-27T09:49:20.007Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=1329, phase=world) +2026-04-27T09:49:20.008Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:49:20.008Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| +2026-04-27T09:49:20.008Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| +2026-04-27T09:49:20.009Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 +2026-04-27T09:49:20.009Z INFO [server:f38959b6] [world] cmd-5 scene action: type=10 +2026-04-27T09:49:20.009Z INFO [server:f38959b6] [world] sending arena status list (1 entries) +2026-04-27T09:49:20.627Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:49:20.628Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=1341, phase=world) +2026-04-27T09:49:20.628Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:49:20.628Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:49:39.050Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=1367, phase=world) +2026-04-27T09:49:39.050Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 +2026-04-27T09:49:39.051Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:49:39.051Z DEBUG [server:f38959b6] [world] rx type-0 len=14 +00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| +2026-04-27T09:49:39.051Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 +2026-04-27T09:49:39.051Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 +2026-04-27T09:49:39.051Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 +2026-04-27T09:49:43.183Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=1387, phase=world) +2026-04-27T09:49:43.184Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T09:49:43.184Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 26 26 2c 65 64 1b |.!&&,ed.| +2026-04-27T09:49:43.184Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 26 26 2c 65 64 1b |.!&&,ed.| +2026-04-27T09:49:43.185Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 +2026-04-27T09:49:43.185Z INFO [server:f38959b6] [world] cmd-5 scene action: type=5 +2026-04-27T09:49:43.185Z INFO [server:f38959b6] [world] cmd-5 Fight button: triggering combat bootstrap room=167 +2026-04-27T09:49:43.186Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" +2026-04-27T09:49:43.187Z INFO [server:f38959b6] [world] delaying combat bootstrap by 4000ms so DROP can display +2026-04-27T09:49:43.212Z DEBUG [server:f38959b6] [world] recv 23 bytes (total=1410, phase=combat) +2026-04-27T09:49:43.213Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=11 +2026-04-27T09:49:43.213Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 24 22 3e 24 22 39 55 31 1b |.!$">$"9U1.| +2026-04-27T09:49:43.213Z DEBUG [server:f38959b6] [world] rx type-0 len=11 +00000000 1b 21 24 22 3e 24 22 39 55 31 1b |.!$">$"9U1.| +2026-04-27T09:49:43.214Z DEBUG [server:f38959b6] [world] client seq=0 cmd=3 +2026-04-27T09:49:43.214Z DEBUG [server:f38959b6] [world] duplicate cmd-3 after initialization — ignoring +2026-04-27T09:49:47.198Z INFO [server:f38959b6] [world] sending Cmd72 combat bootstrap (mech_id=98 callsign="Moose" type=CPLT-C1 allegiance=Marik) +2026-04-27T09:49:47.200Z INFO [server:f38959b6] [world] bot actor: slot=1 side=1 mech_id=98 type=CPLT-C1 x=-149400 y=-258768 +2026-04-27T09:49:47.200Z INFO [server:f38959b6] [world] bot actor: slot=2 side=1 mech_id=98 type=CPLT-C1 x=0 y=-298800 +2026-04-27T09:49:47.201Z INFO [server:f38959b6] [world] bot actor: slot=3 side=1 mech_id=98 type=CPLT-C1 x=149400 y=-258768 +2026-04-27T09:49:47.201Z INFO [server:f38959b6] [world] bot actor: slot=4 side=2 mech_id=98 type=CPLT-C1 x=166671 y=249441 +2026-04-27T09:49:47.202Z INFO [server:f38959b6] [world] bot actor: slot=5 side=2 mech_id=98 type=CPLT-C1 x=58527 y=294236 +2026-04-27T09:49:47.202Z INFO [server:f38959b6] [world] bot actor: slot=6 side=2 mech_id=98 type=CPLT-C1 x=-58527 y=294236 +2026-04-27T09:49:47.202Z INFO [server:f38959b6] [world] bot actor: slot=7 side=2 mech_id=98 type=CPLT-C1 x=-166671 y=249441 +2026-04-27T09:49:47.204Z INFO [server:f38959b6] [world] combat entry complete for "Moose" +2026-04-27T09:49:47.246Z DEBUG [server:f38959b6] [world] recv 19 bytes (total=1429, phase=combat) +2026-04-27T09:49:47.247Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=7 +2026-04-27T09:49:47.247Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 34 27 25 21 1b |.!4'%!.| +2026-04-27T09:49:47.247Z DEBUG [server:f38959b6] [world] rx type-0 len=7 +00000000 1b 21 34 27 25 21 1b |.!4'%!.| +2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world] client seq=0 cmd=19 +2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world/combat] inbound combat cmd=19 len=7 — not yet handled +2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1463, phase=combat) +2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4c 50 |.!)KKKKKKKK!!!LP| +00000010 4b 4b 21 2c 26 1b |KK!,&.| +2026-04-27T09:49:47.249Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4c 50 |.!)KKKKKKKK!!!LP| +00000010 4b 4b 21 2c 26 1b |KK!,&.| +2026-04-27T09:49:47.249Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:47.250Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3702 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:48.241Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1497, phase=combat) +2026-04-27T09:49:48.241Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:48.241Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:48.242Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:48.242Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:48.242Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:49.249Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1531, phase=combat) +2026-04-27T09:49:49.250Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:49.250Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:49.251Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:49.251Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:49.251Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:50.256Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1565, phase=combat) +2026-04-27T09:49:50.257Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:50.257Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:50.257Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:50.257Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:50.258Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:51.267Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1599, phase=combat) +2026-04-27T09:49:51.267Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:51.267Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:51.268Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:51.268Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:51.268Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:52.267Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1633, phase=combat) +2026-04-27T09:49:52.267Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:52.268Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:52.268Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:52.268Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:52.268Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:53.266Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1667, phase=combat) +2026-04-27T09:49:53.266Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:53.267Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:53.267Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:53.268Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:53.268Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:54.265Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1701, phase=combat) +2026-04-27T09:49:54.266Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:54.266Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:54.266Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:54.266Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:54.267Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:55.266Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1735, phase=combat) +2026-04-27T09:49:55.267Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:55.267Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:55.267Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:55.268Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:55.268Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:56.268Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1769, phase=combat) +2026-04-27T09:49:56.268Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:56.269Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:56.269Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:56.269Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:56.270Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:57.281Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1803, phase=combat) +2026-04-27T09:49:57.282Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:57.282Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:57.282Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:57.282Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:57.283Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:58.294Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1837, phase=combat) +2026-04-27T09:49:58.294Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:58.294Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:58.295Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| +00000010 4b 4b 21 24 6e 1b |KK!$n.| +2026-04-27T09:49:58.295Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:58.295Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) +2026-04-27T09:49:59.302Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1871, phase=combat) +2026-04-27T09:49:59.302Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:49:59.303Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 4b 62 4b 4b 4b 4b 21 21 21 4a 4e |.!)KKKbKKKK!!!JN| +00000010 4d 72 35 47 2f 1b |Mr5G/.| +2026-04-27T09:49:59.303Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 4b 62 4b 4b 4b 4b 21 21 21 4a 4e |.!)KKKbKKKK!!!JN| +00000010 4d 72 35 47 2f 1b |Mr5G/.| +2026-04-27T09:49:59.303Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:49:59.304Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=23 y=0 altitude=0 facingRaw=3530 clientSpeed=209 effectiveSpeed=209 -> no echo (trust local key events) +2026-04-27T09:50:00.310Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1905, phase=combat) +2026-04-27T09:50:00.311Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:00.311Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 51 21 4b 4b 4c 4c 21 21 21 4a 67 |.!)KKQ!KKLL!!!Jg| +00000010 53 72 66 31 6c 1b |Srf1l.| +2026-04-27T09:50:00.312Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 51 21 4b 4b 4c 4c 21 21 21 4a 67 |.!)KKQ!KKLL!!!Jg| +00000010 53 72 66 31 6c 1b |Srf1l.| +2026-04-27T09:50:00.312Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:00.312Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=468 y=86 altitude=0 facingRaw=3555 clientSpeed=719 effectiveSpeed=719 -> no echo (trust local key events) +2026-04-27T09:50:01.307Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1939, phase=combat) +2026-04-27T09:50:01.307Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:01.307Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 5c 33 4b 4b 52 61 21 21 21 4b 2b |.!)KK\3KKRa!!!K+| +00000010 5a 21 56 5e 59 1b |Z!V^Y.| +2026-04-27T09:50:01.308Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 5c 33 4b 4b 52 61 21 21 21 4b 2b |.!)KK\3KKRa!!!K+| +00000010 5a 21 56 5e 59 1b |Z!V^Y.| +2026-04-27T09:50:01.308Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:01.308Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=1421 y=617 altitude=0 facingRaw=3580 clientSpeed=1233 effectiveSpeed=1233 -> no echo (trust local key events) +2026-04-27T09:50:02.315Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1973, phase=combat) +2026-04-27T09:50:02.316Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:02.316Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 67 6d 4b 4b 5f 22 21 21 21 4b 3f |.!)KKgmKK_"!!!K?| +00000010 5c 3e 5c 3a 3b 1b |\>\:;.| +2026-04-27T09:50:02.316Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 67 6d 4b 4b 5f 22 21 21 21 4b 3f |.!)KKgmKK_"!!!K?| +00000010 5c 3e 5c 3a 3b 1b |\>\:;.| +2026-04-27T09:50:02.317Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:02.317Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=2414 y=1659 altitude=0 facingRaw=3600 clientSpeed=1432 effectiveSpeed=1432 -> no echo (trust local key events) +2026-04-27T09:50:03.316Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2007, phase=combat) +2026-04-27T09:50:03.316Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:03.317Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4b 70 5e 4b 4b 6f 53 21 21 21 4b 3f |.!)KKp^KKoS!!!K?| +00000010 5f 26 2f 2b 73 1b |_&/+s.| +2026-04-27T09:50:03.317Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4b 70 5e 4b 4b 6f 53 21 21 21 4b 3f |.!)KKp^KKoS!!!K?| +00000010 5f 26 2f 2b 73 1b |_&/+s.| +2026-04-27T09:50:03.317Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:03.317Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=3164 y=3068 altitude=0 facingRaw=3600 clientSpeed=1663 effectiveSpeed=1663 -> no echo (trust local key events) +2026-04-27T09:50:04.315Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2041, phase=combat) +2026-04-27T09:50:04.316Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:04.316Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 22 61 4b 4c 2d 4f 21 21 21 4b 42 |.!)KL"aKL-O!!!KB| +00000010 60 3a 36 21 50 1b |`:6!P.| +2026-04-27T09:50:04.316Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 22 61 4b 4c 2d 4f 21 21 21 4b 42 |.!)KL"aKL-O!!!KB| +00000010 60 3a 36 21 50 1b |`:6!P.| +2026-04-27T09:50:04.317Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:04.317Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=3762 y=4679 altitude=0 facingRaw=3603 clientSpeed=1768 effectiveSpeed=1768 -> no echo (trust local key events) +2026-04-27T09:50:05.326Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2075, phase=combat) +2026-04-27T09:50:05.326Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:05.326Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 28 25 4b 4c 42 21 21 21 21 4b 48 |.!)KL(%KLB!!!!KH| +00000010 60 53 4a 48 6d 1b |`SJHm.| +2026-04-27T09:50:05.327Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 28 25 4b 4c 42 21 21 21 21 4b 48 |.!)KL(%KLB!!!!KH| +00000010 60 53 4a 48 6d 1b |`SJHm.| +2026-04-27T09:50:05.327Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:05.327Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=4212 y=6418 altitude=0 facingRaw=3609 clientSpeed=1793 effectiveSpeed=1793 -> no echo (trust local key events) +2026-04-27T09:50:06.326Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2109, phase=combat) +2026-04-27T09:50:06.327Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:06.327Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 2b 57 4b 4c 57 22 21 21 21 4b 48 |.!)KL+WKLW"!!!KH| +00000010 61 28 2c 23 28 1b |a(,#(.| +2026-04-27T09:50:06.328Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 2b 57 4b 4c 57 22 21 21 21 4b 48 |.!)KL+WKLW"!!!KH| +00000010 61 28 2c 23 28 1b |a(,#(.| +2026-04-27T09:50:06.328Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:06.328Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=4517 y=8204 altitude=0 facingRaw=3609 clientSpeed=1835 effectiveSpeed=1835 -> no echo (trust local key events) +2026-04-27T09:50:07.334Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2143, phase=combat) +2026-04-27T09:50:07.334Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:07.335Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 2e 35 4b 4c 6c 52 21 21 21 4b 48 |.!)KL.5KLlR!!!KH| +00000010 61 41 3d 66 4a 1b |aA=fJ.| +2026-04-27T09:50:07.335Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 2e 35 4b 4c 6c 52 21 21 21 4b 48 |.!)KL.5KLlR!!!KH| +00000010 61 41 3d 66 4a 1b |aA=fJ.| +2026-04-27T09:50:07.335Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:07.335Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=4738 y=10037 altitude=0 facingRaw=3609 clientSpeed=1860 effectiveSpeed=1860 -> no echo (trust local key events) +2026-04-27T09:50:08.333Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2177, phase=combat) +2026-04-27T09:50:08.333Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:08.334Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 30 2d 4b 4d 2d 31 21 21 21 4b 48 |.!)KL0-KM-1!!!KH| +00000010 61 4d 30 5e 43 1b |aM0^C.| +2026-04-27T09:50:08.334Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 30 2d 4b 4d 2d 31 21 21 21 4b 48 |.!)KL0-KM-1!!!KH| +00000010 61 4d 30 5e 43 1b |aM0^C.| +2026-04-27T09:50:08.335Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:08.335Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=4900 y=11874 altitude=0 facingRaw=3609 clientSpeed=1872 effectiveSpeed=1872 -> no echo (trust local key events) +2026-04-27T09:50:09.348Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2211, phase=combat) +2026-04-27T09:50:09.349Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:09.349Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 31 6a 4b 4d 43 30 21 21 21 4b 48 |.!)KL1jKMC0!!!KH| +00000010 61 53 41 34 70 1b |aSA4p.| +2026-04-27T09:50:09.349Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 31 6a 4b 4d 43 30 21 21 21 4b 48 |.!)KL1jKMC0!!!KH| +00000010 61 53 41 34 70 1b |aSA4p.| +2026-04-27T09:50:09.349Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:09.350Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5046 y=13743 altitude=0 facingRaw=3609 clientSpeed=1878 effectiveSpeed=1878 -> no echo (trust local key events) +2026-04-27T09:50:10.341Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2245, phase=combat) +2026-04-27T09:50:10.342Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:10.342Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 33 3b 4b 4d 58 73 21 21 21 4b 48 |.!)KL3;KMXs!!!KH| +00000010 61 51 61 45 6f 1b |aQaEo.| +2026-04-27T09:50:10.342Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 33 3b 4b 4d 58 73 21 21 21 4b 48 |.!)KL3;KMXs!!!KH| +00000010 61 51 61 45 6f 1b |aQaEo.| +2026-04-27T09:50:10.343Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:10.343Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5169 y=15595 altitude=0 facingRaw=3609 clientSpeed=1876 effectiveSpeed=1876 -> no echo (trust local key events) +2026-04-27T09:50:11.351Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2279, phase=combat) +2026-04-27T09:50:11.351Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:11.352Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 34 59 4b 4d 6e 74 21 21 21 4b 48 |.!)KL4YKMnt!!!KH| +00000010 61 53 54 5f 60 1b |aST_`.| +2026-04-27T09:50:11.352Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 34 59 4b 4d 6e 74 21 21 21 4b 48 |.!)KL4YKMnt!!!KH| +00000010 61 53 54 5f 60 1b |aST_`.| +2026-04-27T09:50:11.353Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:11.353Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5284 y=17466 altitude=0 facingRaw=3609 clientSpeed=1878 effectiveSpeed=1878 -> no echo (trust local key events) +2026-04-27T09:50:12.353Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2313, phase=combat) +2026-04-27T09:50:12.354Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:12.354Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 36 23 4b 4e 2f 61 21 21 21 4b 40 |.!)KL6#KN/a!!!K@| +00000010 61 47 24 31 60 1b |aG$1`.| +2026-04-27T09:50:12.354Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 36 23 4b 4e 2f 61 21 21 21 4b 40 |.!)KL6#KN/a!!!K@| +00000010 61 47 24 31 60 1b |aG$1`.| +2026-04-27T09:50:12.355Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:12.355Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5400 y=19317 altitude=0 facingRaw=3601 clientSpeed=1866 effectiveSpeed=1866 -> no echo (trust local key events) +2026-04-27T09:50:13.361Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2347, phase=combat) +2026-04-27T09:50:13.362Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:13.362Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 38 35 4b 4e 45 5d 21 21 21 4b 40 |.!)KL85KNE]!!!K@| +00000010 61 51 2e 44 37 1b |aQ.D7.| +2026-04-27T09:50:13.362Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 38 35 4b 4e 45 5d 21 21 21 4b 40 |.!)KL85KNE]!!!K@| +00000010 61 51 2e 44 37 1b |aQ.D7.| +2026-04-27T09:50:13.363Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:13.363Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5588 y=21183 altitude=0 facingRaw=3601 clientSpeed=1876 effectiveSpeed=1876 -> no echo (trust local key events) +2026-04-27T09:50:14.366Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2381, phase=combat) +2026-04-27T09:50:14.366Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:14.367Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 3b 3d 4b 4e 5b 3b 21 21 21 4b 3f |.!)KL;=KN[;!!!K?| +00000010 61 54 41 66 26 1b |aTAf&.| +2026-04-27T09:50:14.367Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 3b 3d 4b 4e 5b 3b 21 21 21 4b 3f |.!)KL;=KN[;!!!K?| +00000010 61 54 41 66 26 1b |aTAf&.| +2026-04-27T09:50:14.367Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:14.368Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5851 y=23019 altitude=0 facingRaw=3600 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) +2026-04-27T09:50:15.366Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2415, phase=combat) +2026-04-27T09:50:15.366Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:15.367Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 3e 75 4b 4e 70 6e 21 21 21 4b 3f |.!)KL>uKNpn!!!K?| +00000010 61 57 3c 63 6c 1b |aWuKNpn!!!K?| +00000010 61 57 3c 63 6c 1b |aW no echo (trust local key events) +2026-04-27T09:50:16.376Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2449, phase=combat) +2026-04-27T09:50:16.377Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:16.377Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 43 24 4b 4f 31 47 21 21 21 4b 44 |.!)KLC$KO1G!!!KD| +00000010 61 50 3f 5d 57 1b |aP?]W.| +2026-04-27T09:50:16.378Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 43 24 4b 4f 31 47 21 21 21 4b 44 |.!)KLC$KO1G!!!KD| +00000010 61 50 3f 5d 57 1b |aP?]W.| +2026-04-27T09:50:16.378Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:16.378Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=6506 y=26686 altitude=0 facingRaw=3605 clientSpeed=1875 effectiveSpeed=1875 -> no echo (trust local key events) +2026-04-27T09:50:17.376Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2483, phase=combat) +2026-04-27T09:50:17.377Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:17.377Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 46 57 4b 4f 46 74 21 21 21 4b 44 |.!)KLFWKOFt!!!KD| +00000010 61 53 5a 47 5a 1b |aSZGZ.| +2026-04-27T09:50:17.377Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 46 57 4b 4f 46 74 21 21 21 4b 44 |.!)KLFWKOFt!!!KD| +00000010 61 53 5a 47 5a 1b |aSZGZ.| +2026-04-27T09:50:17.378Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:17.378Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=6812 y=28516 altitude=0 facingRaw=3605 clientSpeed=1878 effectiveSpeed=1878 -> no echo (trust local key events) +2026-04-27T09:50:18.375Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2517, phase=combat) +2026-04-27T09:50:18.375Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:18.375Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 49 6c 4b 4f 5c 51 21 21 21 4b 42 |.!)KLIlKO\Q!!!KB| +00000010 61 52 63 47 35 1b |aRcG5.| +2026-04-27T09:50:18.376Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 49 6c 4b 4f 5c 51 21 21 21 4b 42 |.!)KLIlKO\Q!!!KB| +00000010 61 52 63 47 35 1b |aRcG5.| +2026-04-27T09:50:18.376Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:18.376Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=7088 y=30351 altitude=0 facingRaw=3603 clientSpeed=1877 effectiveSpeed=1877 -> no echo (trust local key events) +2026-04-27T09:50:19.374Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2551, phase=combat) +2026-04-27T09:50:19.374Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:19.375Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 4d 45 4b 4f 72 24 21 21 21 4b 3c |.!)KLMEKOr$!!!K<| +00000010 61 4e 67 28 26 1b |aNg(&.| +2026-04-27T09:50:19.375Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 4d 45 4b 4f 72 24 21 21 21 4b 3c |.!)KLMEKOr$!!!K<| +00000010 61 4e 67 28 26 1b |aNg(&.| +2026-04-27T09:50:19.376Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:19.376Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=7389 y=32176 altitude=0 facingRaw=3597 clientSpeed=1873 effectiveSpeed=1873 -> no echo (trust local key events) +2026-04-27T09:50:20.373Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2585, phase=combat) +2026-04-27T09:50:20.374Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:20.374Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 51 74 4b 50 32 3a 21 21 21 4b 3c |.!)KLQtKP2:!!!K<| +00000010 61 55 5b 45 6b 1b |aU[Ek.| +2026-04-27T09:50:20.375Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 51 74 4b 50 32 3a 21 21 21 4b 3c |.!)KLQtKP2:!!!K<| +00000010 61 55 5b 45 6b 1b |aU[Ek.| +2026-04-27T09:50:20.375Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:20.375Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=7776 y=33983 altitude=0 facingRaw=3597 clientSpeed=1880 effectiveSpeed=1880 -> no echo (trust local key events) +2026-04-27T09:50:21.395Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2619, phase=combat) +2026-04-27T09:50:21.395Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:21.396Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 57 32 4b 50 47 6d 21 21 21 4b 33 |.!)KLW2KPGm!!!K3| +00000010 61 39 57 53 75 1b |a9WSu.| +2026-04-27T09:50:21.396Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 57 32 4b 50 47 6d 21 21 21 4b 33 |.!)KLW2KPGm!!!K3| +00000010 61 39 57 53 75 1b |a9WSu.| +2026-04-27T09:50:21.396Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:21.396Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=8220 y=35819 altitude=0 facingRaw=3588 clientSpeed=1852 effectiveSpeed=1852 -> no echo (trust local key events) +2026-04-27T09:50:22.404Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2653, phase=combat) +2026-04-27T09:50:22.404Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:22.405Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 5e 3a 4b 50 5c 53 21 21 21 4b 2e |.!)KL^:KP\S!!!K.| +00000010 61 38 26 6f 3e 1b |a8&o>.| +2026-04-27T09:50:22.405Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 5e 3a 4b 50 5c 53 21 21 21 4b 2e |.!)KL^:KP\S!!!K.| +00000010 61 38 26 6f 3e 1b |a8&o>.| +2026-04-27T09:50:22.405Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:22.406Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=8823 y=37578 altitude=0 facingRaw=3583 clientSpeed=1851 effectiveSpeed=1851 -> no echo (trust local key events) +2026-04-27T09:50:23.406Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2687, phase=combat) +2026-04-27T09:50:23.407Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:23.407Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 66 55 4b 50 70 54 21 21 21 4b 3a |.!)KLfUKPpT!!!K:| +00000010 61 47 58 3c 35 1b |aGX<5.| +2026-04-27T09:50:23.408Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 66 55 4b 50 70 54 21 21 21 4b 3a |.!)KLfUKPpT!!!K:| +00000010 61 47 58 3c 35 1b |aGX<5.| +2026-04-27T09:50:23.408Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:23.408Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=9530 y=39279 altitude=0 facingRaw=3595 clientSpeed=1866 effectiveSpeed=1866 -> no echo (trust local key events) +2026-04-27T09:50:24.419Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2721, phase=combat) +2026-04-27T09:50:24.420Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:24.420Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 6e 34 4b 51 30 29 21 21 21 4b 3a |.!)KLn4KQ0)!!!K:| +00000010 61 4f 30 67 4e 1b |aO0gN.| +2026-04-27T09:50:24.420Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4c 6e 34 4b 51 30 29 21 21 21 4b 3a |.!)KLn4KQ0)!!!K:| +00000010 61 4f 30 67 4e 1b |aO0gN.| +2026-04-27T09:50:24.421Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:24.421Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=10177 y=41021 altitude=0 facingRaw=3595 clientSpeed=1874 effectiveSpeed=1874 -> no echo (trust local key events) +2026-04-27T09:50:25.416Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2755, phase=combat) +2026-04-27T09:50:25.416Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:25.416Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4c 75 3c 4b 51 44 5c 21 21 21 4b 41 |.!)KLu no echo (trust local key events) +2026-04-27T09:50:26.425Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2789, phase=combat) +2026-04-27T09:50:26.425Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:26.426Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 25 71 4b 51 59 6c 21 21 21 4b 47 |.!)KM%qKQYl!!!KG| +00000010 61 38 59 29 33 1b |a8Y)3.| +2026-04-27T09:50:26.426Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 25 71 4b 51 59 6c 21 21 21 4b 47 |.!)KM%qKQYl!!!KG| +00000010 61 38 59 29 33 1b |a8Y)3.| +2026-04-27T09:50:26.426Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:26.427Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=11258 y=44573 altitude=0 facingRaw=3608 clientSpeed=1851 effectiveSpeed=1851 -> no echo (trust local key events) +2026-04-27T09:50:27.427Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2823, phase=combat) +2026-04-27T09:50:27.427Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:27.427Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 29 67 4b 51 6f 31 21 21 21 4b 47 |.!)KM)gKQo1!!!KG| +00000010 61 49 52 2f 40 1b |aIR/@.| +2026-04-27T09:50:27.428Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 29 67 4b 51 6f 31 21 21 21 4b 47 |.!)KM)gKQo1!!!KG| +00000010 61 49 52 2f 40 1b |aIR/@.| +2026-04-27T09:50:27.428Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:27.428Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=11588 y=46384 altitude=0 facingRaw=3608 clientSpeed=1868 effectiveSpeed=1868 -> no echo (trust local key events) +2026-04-27T09:50:28.432Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2857, phase=combat) +2026-04-27T09:50:28.433Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:28.433Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 2c 4c 4b 52 30 24 21 21 21 4b 47 |.!)KM,LKR0$!!!KG| +00000010 61 50 66 70 3d 1b |aPfp=.| +2026-04-27T09:50:28.433Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 2c 4c 4b 52 30 24 21 21 21 4b 47 |.!)KM,LKR0$!!!KG| +00000010 61 50 66 70 3d 1b |aPfp=.| +2026-04-27T09:50:28.433Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:28.434Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=11816 y=48241 altitude=0 facingRaw=3608 clientSpeed=1875 effectiveSpeed=1875 -> no echo (trust local key events) +2026-04-27T09:50:29.447Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2891, phase=combat) +2026-04-27T09:50:29.447Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:29.448Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 2e 61 4b 52 46 24 21 21 21 4b 47 |.!)KM.aKRF$!!!KG| +00000010 61 52 59 6d 3d 1b |aRYm=.| +2026-04-27T09:50:29.448Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 2e 61 4b 52 46 24 21 21 21 4b 47 |.!)KM.aKRF$!!!KG| +00000010 61 52 59 6d 3d 1b |aRYm=.| +2026-04-27T09:50:29.448Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:29.448Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12007 y=50111 altitude=0 facingRaw=3608 clientSpeed=1877 effectiveSpeed=1877 -> no echo (trust local key events) +2026-04-27T09:50:30.450Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2925, phase=combat) +2026-04-27T09:50:30.450Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:30.450Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 30 4f 4b 52 5b 66 21 21 21 4b 47 |.!)KM0OKR[f!!!KG| +00000010 61 54 3d 4c 3e 1b |aT=L>.| +2026-04-27T09:50:30.451Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 30 4f 4b 52 5b 66 21 21 21 4b 47 |.!)KM0OKR[f!!!KG| +00000010 61 54 3d 4c 3e 1b |aT=L>.| +2026-04-27T09:50:30.451Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:30.451Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12159 y=51962 altitude=0 facingRaw=3608 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) +2026-04-27T09:50:31.443Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2959, phase=combat) +2026-04-27T09:50:31.443Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:31.444Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 32 30 4b 52 71 56 21 21 21 4b 47 |.!)KM20KRqV!!!KG| +00000010 61 54 49 46 64 1b |aTIFd.| +2026-04-27T09:50:31.444Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 32 30 4b 52 71 56 21 21 21 4b 47 |.!)KM20KRqV!!!KG| +00000010 61 54 49 46 64 1b |aTIFd.| +2026-04-27T09:50:31.444Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:31.445Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12298 y=53816 altitude=0 facingRaw=3608 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) +2026-04-27T09:50:32.448Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2993, phase=combat) +2026-04-27T09:50:32.449Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:32.449Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 33 67 4b 53 32 44 21 21 21 4b 47 |.!)KM3gKS2D!!!KG| +00000010 61 57 40 3e 39 1b |aW@>9.| +2026-04-27T09:50:32.450Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 33 67 4b 53 32 44 21 21 21 4b 47 |.!)KM3gKS2D!!!KG| +00000010 61 57 40 3e 39 1b |aW@>9.| +2026-04-27T09:50:32.450Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:32.450Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12438 y=55668 altitude=0 facingRaw=3608 clientSpeed=1882 effectiveSpeed=1882 -> no echo (trust local key events) +2026-04-27T09:50:33.452Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3027, phase=combat) +2026-04-27T09:50:33.452Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:33.452Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 35 46 4b 53 48 44 21 21 21 4b 47 |.!)KM5FKSHD!!!KG| +00000010 61 57 44 3b 38 1b |aWD;8.| +2026-04-27T09:50:33.453Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 35 46 4b 53 48 44 21 21 21 4b 47 |.!)KM5FKSHD!!!KG| +00000010 61 57 44 3b 38 1b |aWD;8.| +2026-04-27T09:50:33.453Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:33.453Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12575 y=57538 altitude=0 facingRaw=3608 clientSpeed=1882 effectiveSpeed=1882 -> no echo (trust local key events) +2026-04-27T09:50:34.452Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3061, phase=combat) +2026-04-27T09:50:34.452Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:34.452Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 36 61 4b 53 5e 31 21 21 21 4b 47 |.!)KM6aKS^1!!!KG| +00000010 61 57 3a 24 5d 1b |aW:$].| +2026-04-27T09:50:34.453Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 36 61 4b 53 5e 31 21 21 21 4b 47 |.!)KM6aKS^1!!!KG| +00000010 61 57 3a 24 5d 1b |aW:$].| +2026-04-27T09:50:34.453Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:34.453Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12687 y=59389 altitude=0 facingRaw=3608 clientSpeed=1882 effectiveSpeed=1882 -> no echo (trust local key events) +2026-04-27T09:50:35.468Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3095, phase=combat) +2026-04-27T09:50:35.469Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:35.469Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 38 22 4b 53 74 2e 21 21 21 4b 51 |.!)KM8"KSt.!!!KQ| +00000010 61 39 46 68 6f 1b |a9Fho.| +2026-04-27T09:50:35.469Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 38 22 4b 53 74 2e 21 21 21 4b 51 |.!)KM8"KSt.!!!KQ| +00000010 61 39 46 68 6f 1b |a9Fho.| +2026-04-27T09:50:35.470Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:35.470Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12794 y=61256 altitude=0 facingRaw=3618 clientSpeed=1852 effectiveSpeed=1852 -> no echo (trust local key events) +2026-04-27T09:50:36.466Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3129, phase=combat) +2026-04-27T09:50:36.467Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:36.467Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 38 28 4b 54 34 70 21 21 21 4b 51 |.!)KM8(KT4p!!!KQ| +00000010 61 46 54 36 45 1b |aFT6E.| +2026-04-27T09:50:36.467Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 38 28 4b 54 34 70 21 21 21 4b 51 |.!)KM8(KT4p!!!KQ| +00000010 61 46 54 36 45 1b |aFT6E.| +2026-04-27T09:50:36.468Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:36.468Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12800 y=63107 altitude=0 facingRaw=3618 clientSpeed=1865 effectiveSpeed=1865 -> no echo (trust local key events) +2026-04-27T09:50:37.467Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3163, phase=combat) +2026-04-27T09:50:37.468Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:37.468Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 37 36 4b 54 4a 5d 21 21 21 4b 51 |.!)KM76KTJ]!!!KQ| +00000010 61 51 5f 40 54 1b |aQ_@T.| +2026-04-27T09:50:37.468Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 37 36 4b 54 4a 5d 21 21 21 4b 51 |.!)KM76KTJ]!!!KQ| +00000010 61 51 5f 40 54 1b |aQ_@T.| +2026-04-27T09:50:37.468Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:37.469Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12729 y=64958 altitude=0 facingRaw=3618 clientSpeed=1876 effectiveSpeed=1876 -> no echo (trust local key events) +2026-04-27T09:50:38.462Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3197, phase=combat) +2026-04-27T09:50:38.462Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:38.463Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 35 5d 4b 54 60 4a 21 21 21 4b 51 |.!)KM5]KT`J!!!KQ| +00000010 61 53 46 71 3a 1b |aSFq:.| +2026-04-27T09:50:38.463Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 35 5d 4b 54 60 4a 21 21 21 4b 51 |.!)KM5]KT`J!!!KQ| +00000010 61 53 46 71 3a 1b |aSFq:.| +2026-04-27T09:50:38.463Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:38.464Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12598 y=66809 altitude=0 facingRaw=3618 clientSpeed=1878 effectiveSpeed=1878 -> no echo (trust local key events) +2026-04-27T09:50:39.477Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3231, phase=combat) +2026-04-27T09:50:39.477Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:39.477Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 33 68 4b 55 21 4b 21 21 21 4b 51 |.!)KM3hKU!K!!!KQ| +00000010 61 52 25 24 3a 1b |aR%$:.| +2026-04-27T09:50:39.478Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 33 68 4b 55 21 4b 21 21 21 4b 51 |.!)KM3hKU!K!!!KQ| +00000010 61 52 25 24 3a 1b |aR%$:.| +2026-04-27T09:50:39.478Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:39.478Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12439 y=68680 altitude=0 facingRaw=3618 clientSpeed=1877 effectiveSpeed=1877 -> no echo (trust local key events) +2026-04-27T09:50:40.471Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3265, phase=combat) +2026-04-27T09:50:40.472Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:40.472Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 31 4a 4b 55 37 32 21 21 21 4b 51 |.!)KM1JKU72!!!KQ| +00000010 61 54 26 71 54 1b |aT&qT.| +2026-04-27T09:50:40.473Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 31 4a 4b 55 37 32 21 21 21 4b 51 |.!)KM1JKU72!!!KQ| +00000010 61 54 26 71 54 1b |aT&qT.| +2026-04-27T09:50:40.473Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:40.474Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12239 y=70525 altitude=0 facingRaw=3618 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) +2026-04-27T09:50:41.483Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3299, phase=combat) +2026-04-27T09:50:41.484Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:41.484Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 2f 2a 4b 55 4d 30 21 21 21 4b 50 |.!)KM/*KUM0!!!KP| +00000010 61 54 49 63 35 1b |aTIc5.| +2026-04-27T09:50:41.484Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 2f 2a 4b 55 4d 30 21 21 21 4b 50 |.!)KM/*KUM0!!!KP| +00000010 61 54 49 63 35 1b |aTIc5.| +2026-04-27T09:50:41.484Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:41.485Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12037 y=72393 altitude=0 facingRaw=3617 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) +2026-04-27T09:50:42.490Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3333, phase=combat) +2026-04-27T09:50:42.490Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:42.491Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 2d 56 4b 55 62 72 21 21 21 4b 37 |.!)KM-VKUbr!!!K7| +00000010 5f 73 33 34 2c 1b |_s34,.| +2026-04-27T09:50:42.491Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 2d 56 4b 55 62 72 21 21 21 4b 37 |.!)KM-VKUbr!!!K7| +00000010 5f 73 33 34 2c 1b |_s34,.| +2026-04-27T09:50:42.491Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:42.492Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=11911 y=74244 altitude=0 facingRaw=3592 clientSpeed=1740 effectiveSpeed=1740 -> no echo (trust local key events) +2026-04-27T09:50:43.497Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3367, phase=combat) +2026-04-27T09:50:43.497Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:43.497Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 2f 62 4b 56 23 48 21 21 21 4b 2b |.!)KM/bKV#H!!!K+| +00000010 5f 5a 4c 59 3a 1b |_ZLY:.| +2026-04-27T09:50:43.498Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 2f 62 4b 56 23 48 21 21 21 4b 2b |.!)KM/bKV#H!!!K+| +00000010 5f 5a 4c 59 3a 1b |_ZLY:.| +2026-04-27T09:50:43.498Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:43.498Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12093 y=76072 altitude=0 facingRaw=3580 clientSpeed=1715 effectiveSpeed=1715 -> no echo (trust local key events) +2026-04-27T09:50:44.505Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3401, phase=combat) +2026-04-27T09:50:44.506Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:44.506Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 36 36 4b 56 37 58 21 21 21 4b 21 |.!)KM66KV7X!!!K!| +00000010 5f 6e 54 54 63 1b |_nTTc.| +2026-04-27T09:50:44.506Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 36 36 4b 56 37 58 21 21 21 4b 21 |.!)KM66KV7X!!!K!| +00000010 5f 6e 54 54 63 1b |_nTTc.| +2026-04-27T09:50:44.507Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:44.507Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12644 y=77788 altitude=0 facingRaw=3570 clientSpeed=1735 effectiveSpeed=1735 -> no echo (trust local key events) +2026-04-27T09:50:45.503Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3435, phase=combat) +2026-04-27T09:50:45.503Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:45.503Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 40 32 4b 56 4a 32 21 21 21 4b 30 |.!)KM@2KVJ2!!!K0| +00000010 61 24 56 72 33 1b |a$Vr3.| +2026-04-27T09:50:45.504Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 40 32 4b 56 4a 32 21 21 21 4b 30 |.!)KM@2KVJ2!!!K0| +00000010 61 24 56 72 33 1b |a$Vr3.| +2026-04-27T09:50:45.504Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:45.504Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=13490 y=79365 altitude=0 facingRaw=3585 clientSpeed=1831 effectiveSpeed=1831 -> no echo (trust local key events) +2026-04-27T09:50:46.510Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3469, phase=combat) +2026-04-27T09:50:46.511Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:46.511Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 49 6d 4b 56 5d 27 21 21 21 4b 43 |.!)KMImKV]'!!!KC| +00000010 60 3e 5a 3c 55 1b |`>ZZ no echo (trust local key events) +2026-04-27T09:50:47.516Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3503, phase=combat) +2026-04-27T09:50:47.517Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:47.517Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 50 71 4b 56 71 47 21 21 21 4b 40 |.!)KMPqKVqG!!!K@| +00000010 61 30 50 28 5b 1b |a0P([.| +2026-04-27T09:50:47.517Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 50 71 4b 56 71 47 21 21 21 4b 40 |.!)KMPqKVqG!!!K@| +00000010 61 30 50 28 5b 1b |a0P([.| +2026-04-27T09:50:47.518Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:47.518Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=14913 y=82701 altitude=0 facingRaw=3601 clientSpeed=1843 effectiveSpeed=1843 -> no echo (trust local key events) +2026-04-27T09:50:48.520Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3537, phase=combat) +2026-04-27T09:50:48.521Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:48.521Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 57 27 4b 57 30 75 21 21 21 4b 33 |.!)KMW'KW0u!!!K3| +00000010 61 3b 32 49 26 1b |a;2I&.| +2026-04-27T09:50:48.521Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 57 27 4b 57 30 75 21 21 21 4b 33 |.!)KMW'KW0u!!!K3| +00000010 61 3b 32 49 26 1b |a;2I&.| +2026-04-27T09:50:48.522Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:48.522Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=15434 y=84447 altitude=0 facingRaw=3588 clientSpeed=1854 effectiveSpeed=1854 -> no echo (trust local key events) +2026-04-27T09:50:49.526Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3571, phase=combat) +2026-04-27T09:50:49.527Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:49.527Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 5e 32 4b 57 45 5b 21 21 21 4b 3e |.!)KM^2KWE[!!!K>| +00000010 61 43 3e 46 55 1b |aC>FU.| +2026-04-27T09:50:49.528Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 5e 32 4b 57 45 5b 21 21 21 4b 3e |.!)KM^2KWE[!!!K>| +00000010 61 43 3e 46 55 1b |aC>FU.| +2026-04-27T09:50:49.528Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:49.528Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=16040 y=86206 altitude=0 facingRaw=3599 clientSpeed=1862 effectiveSpeed=1862 -> no echo (trust local key events) +2026-04-27T09:50:50.526Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3605, phase=combat) +2026-04-27T09:50:50.527Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:50.527Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 64 3a 4b 57 5a 39 21 21 21 4b 3f |.!)KMd:KWZ9!!!K?| +00000010 61 2a 51 2a 26 1b |a*Q*&.| +2026-04-27T09:50:50.528Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 64 3a 4b 57 5a 39 21 21 21 4b 3f |.!)KMd:KWZ9!!!K?| +00000010 61 2a 51 2a 26 1b |a*Q*&.| +2026-04-27T09:50:50.528Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:50.528Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=16558 y=87957 altitude=0 facingRaw=3600 clientSpeed=1837 effectiveSpeed=1837 -> no echo (trust local key events) +2026-04-27T09:50:51.538Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3639, phase=combat) +2026-04-27T09:50:51.538Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:51.538Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 69 3d 4b 57 6d 6f 21 21 21 4b 3f |.!)KMi=KWmo!!!K?| +00000010 5e 73 3b 51 65 1b |^s;Qe.| +2026-04-27T09:50:51.539Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 69 3d 4b 57 6d 6f 21 21 21 4b 3f |.!)KMi=KWmo!!!K?| +00000010 5e 73 3b 51 65 1b |^s;Qe.| +2026-04-27T09:50:51.539Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:51.539Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=16986 y=89626 altitude=0 facingRaw=3600 clientSpeed=1655 effectiveSpeed=1655 -> no echo (trust local key events) +2026-04-27T09:50:52.541Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3673, phase=combat) +2026-04-27T09:50:52.542Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:52.542Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 6d 4e 4b 58 2b 27 21 21 21 4b 3f |.!)KMmNKX+'!!!K?| +00000010 5e 22 67 68 72 1b |^"ghr.| +2026-04-27T09:50:52.542Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 6d 4e 4b 58 2b 27 21 21 21 4b 3f |.!)KMmNKX+'!!!K?| +00000010 5e 22 67 68 72 1b |^"ghr.| +2026-04-27T09:50:52.543Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:52.543Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=17343 y=91169 altitude=0 facingRaw=3600 clientSpeed=1574 effectiveSpeed=1574 -> no echo (trust local key events) +2026-04-27T09:50:53.547Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3707, phase=combat) +2026-04-27T09:50:53.548Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:53.548Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 71 3b 4b 58 3c 39 21 21 21 4b 3f |.!)KMq;KX<9!!!K?| +00000010 5c 5a 48 23 52 1b |\ZH#R.| +2026-04-27T09:50:53.548Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 71 3b 4b 58 3c 39 21 21 21 4b 3f |.!)KMq;KX<9!!!K?| +00000010 5c 5a 48 23 52 1b |\ZH#R.| +2026-04-27T09:50:53.549Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:53.549Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=17664 y=92632 altitude=0 facingRaw=3600 clientSpeed=1460 effectiveSpeed=1460 -> no echo (trust local key events) +2026-04-27T09:50:54.554Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3741, phase=combat) +2026-04-27T09:50:54.554Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 +2026-04-27T09:50:54.554Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 29 4b 4d 74 6b 4b 58 4c 5f 21 21 21 4b 3f |.!)KMtkKXL_!!!K?| +00000010 5d 30 28 53 44 1b |]0(SD.| +2026-04-27T09:50:54.555Z DEBUG [server:f38959b6] [world] rx type-0 len=22 +00000000 1b 21 29 4b 4d 74 6b 4b 58 4c 5f 21 21 21 4b 3f |.!)KMtkKXL_!!!K?| +00000010 5d 30 28 53 44 1b |]0(SD.| +2026-04-27T09:50:54.555Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 +2026-04-27T09:50:54.555Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=17967 y=94030 altitude=0 facingRaw=3600 clientSpeed=1503 effectiveSpeed=1503 -> no echo (trust local key events) +2026-04-27T09:50:55.554Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=3781, phase=combat) +2026-04-27T09:50:55.555Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 +2026-04-27T09:50:55.555Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2a 4b 4e 23 63 4b 58 5f 26 21 21 21 4b 3f |.!*KN#cKX_&!!!K?| +00000010 4b 4b 4b 4c 4b 4b 5f 51 3a 3d 66 1b |KKKLKK_Q:=f.| +2026-04-27T09:50:55.555Z DEBUG [server:f38959b6] [world] rx type-0 len=28 +00000000 1b 21 2a 4b 4e 23 63 4b 58 5f 26 21 21 21 4b 3f |.!*KN#cKX_&!!!K?| +00000010 4b 4b 4b 4c 4b 4b 5f 51 3a 3d 66 1b |KKKLKK_Q:=f.| +2026-04-27T09:50:55.556Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 +2026-04-27T09:50:55.556Z DEBUG [server:f38959b6] [world/combat] cmd9 moving: altitude=0 facingRaw=3600 pitchRaw=3613 torsoYawRaw=3612 clientSpeed=1706 effectiveSpeed=1706 pitch=182 torsoYaw=0 localEcho=none +2026-04-27T09:50:56.556Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=3821, phase=combat) +2026-04-27T09:50:56.556Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 +2026-04-27T09:50:56.557Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2a 4b 4e 27 6d 4b 58 72 6b 21 21 21 4b 3f |.!*KN'mKXrk!!!K?| +00000010 4b 4b 4b 4c 4b 4b 60 59 55 47 75 1b |KKKLKK`YUGu.| +2026-04-27T09:50:56.557Z DEBUG [server:f38959b6] [world] rx type-0 len=28 +00000000 1b 21 2a 4b 4e 27 6d 4b 58 72 6b 21 21 21 4b 3f |.!*KN'mKXrk!!!K?| +00000010 4b 4b 4b 4c 4b 4b 60 59 55 47 75 1b |KKKLKK`YUGu.| +2026-04-27T09:50:56.557Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 +2026-04-27T09:50:56.557Z DEBUG [server:f38959b6] [world/combat] cmd9 moving: altitude=0 facingRaw=3600 pitchRaw=3613 torsoYawRaw=3612 clientSpeed=1799 effectiveSpeed=1799 pitch=182 torsoYaw=0 localEcho=none +2026-04-27T09:50:57.575Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=3861, phase=combat) +2026-04-27T09:50:57.575Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 +2026-04-27T09:50:57.576Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2a 4b 4e 2c 2c 4b 59 32 6b 21 21 21 4b 3f |.!*KN,,KY2k!!!K?| +00000010 4b 4b 4b 4c 4b 4b 61 25 31 38 58 1b |KKKLKKa%18X.| +2026-04-27T09:50:57.576Z DEBUG [server:f38959b6] [world] rx type-0 len=28 +00000000 1b 21 2a 4b 4e 2c 2c 4b 59 32 6b 21 21 21 4b 3f |.!*KN,,KY2k!!!K?| +00000010 4b 4b 4b 4c 4b 4b 61 25 31 38 58 1b |KKKLKKa%18X.| +2026-04-27T09:50:57.576Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 +2026-04-27T09:50:57.577Z DEBUG [server:f38959b6] [world/combat] cmd9 moving: altitude=0 facingRaw=3600 pitchRaw=3613 torsoYawRaw=3612 clientSpeed=1832 effectiveSpeed=1832 pitch=182 torsoYaw=0 localEcho=none +2026-04-27T09:50:58.315Z INFO [server:f38959b6] [world/combat] bot jump start: slot=5 mechId=98 fuel=120 from=(34227,172450) to=(31791,160700) apex=6000 reason=aggressive range close +2026-04-27T09:50:58.592Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=3901, phase=combat) +2026-04-27T09:50:58.592Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 +2026-04-27T09:50:58.593Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2a 4b 4e 30 3c 4b 59 48 29 21 21 21 4b 3f |.!*KN010 risk=0 expected=%.1f targetHealth=5.3999999999999995 updates=255 shots=[1] 0:LRM-15:15:center-torso-front:slot=5:target=player:attach=31:chance=18:roll=8:band=long:cross=1:health=255:updates=0x19=9,1:LRM-15:miss:target=player:chance=18:roll=26:band=long:cross=1:impact=16797/106416/0 +2026-04-27T09:51:01.619Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=4021, phase=combat) +2026-04-27T09:51:01.619Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 +2026-04-27T09:51:01.620Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2a 4b 4e 3c 6b 4b 5a 33 4e 21 21 21 4b 3f |.!*KN no echo (trust local key events) +2026-04-27T09:51:04.420Z DEBUG [server:f38959b6] [world] recv 56 bytes (total=4151, phase=combat) +2026-04-27T09:51:04.421Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=44 +2026-04-27T09:51:04.421Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2b 21 27 23 4b 4b 4b 4b 4b 4f 68 35 4e 57 |.!+!'#KKKKKOh5NW| +00000010 2c 49 75 75 75 2b 22 27 23 4b 4b 4b 4b 4b 54 2f |,Iuuu+"'#KKKKKT/| +00000020 74 4e 6d 26 6f 75 75 75 5f 68 42 1b |tNm&ouuu_hB.| +2026-04-27T09:51:04.421Z DEBUG [server:f38959b6] [world] rx type-0 len=44 +00000000 1b 21 2b 21 27 23 4b 4b 4b 4b 4b 4f 68 35 4e 57 |.!+!'#KKKKKOh5NW| +00000010 2c 49 75 75 75 2b 22 27 23 4b 4b 4b 4b 4b 54 2f |,Iuuu+"'#KKKKKT/| +00000020 74 4e 6d 26 6f 75 75 75 5f 68 42 1b |tNm&ouuu_hB.| +2026-04-27T09:51:04.421Z DEBUG [server:f38959b6] [world] client seq=0 cmd=10 +2026-04-27T09:51:04.423Z DEBUG [server:f38959b6] [world/combat] cmd10 direct-cmd10 path: no recent cmd12/action0 (age=n/a records=2) — compatible with TIC fire geometry +2026-04-27T09:51:04.424Z INFO [server:f38959b6] [world/combat] cmd10 weapon fire accepted: firePath=direct-cmd10 records=2 weaponSlots=0/1 botHealth=1890 updates=0 shots=[0:LRM-15:15:slot=5:miss:5/1:chance=34:roll=50:band=long:cross=0:bot=30986/156859,1:LRM-15:15:slot=5:miss:5/1:chance=34:roll=72:band=long:cross=0:bot=30986/156859] +2026-04-27T09:51:04.621Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=4191, phase=combat) +2026-04-27T09:51:04.622Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 +2026-04-27T09:51:04.622Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2a 4b 4e 49 3d 4b 5a 73 54 21 21 21 4b 3f |.!*KNI=KZsT!!!K?| +00000010 4b 4b 4b 49 4b 4b 60 55 28 33 5a 1b |KKKIKK`U(3Z.| +2026-04-27T09:51:04.622Z DEBUG [server:f38959b6] [world] rx type-0 len=28 +00000000 1b 21 2a 4b 4e 49 3d 4b 5a 73 54 21 21 21 4b 3f |.!*KNI=KZsT!!!K?| +00000010 4b 4b 4b 49 4b 4b 60 55 28 33 5a 1b |KKKIKK`U(3Z.| +2026-04-27T09:51:04.623Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 +2026-04-27T09:51:04.623Z DEBUG [server:f38959b6] [world/combat] cmd9 moving: altitude=0 facingRaw=3600 pitchRaw=3610 torsoYawRaw=3612 clientSpeed=1795 effectiveSpeed=1795 pitch=-364 torsoYaw=0 localEcho=none +2026-04-27T09:51:05.069Z INFO [server:f38959b6] [world/combat] bot volley: slot=6 side=2 mechId=98 target=player tic=B distance=623m weapons=0/1 heat=0->10 risk=0 expected=%.1f targetHealth=5.3999999999999995 updates=240 shots=[2] 0:LRM-15:miss:target=player:chance=18:roll=92:band=long:cross=73:impact=19268/108708/0,1:LRM-15:15:center-torso-front:slot=6:target=player:attach=31:chance=18:roll=8:band=long:cross=73:health=240:updates=0x19=0/0x24=15 +2026-04-27T09:51:05.581Z INFO [server:f38959b6] [world/combat] bot volley: slot=4 side=2 mechId=98 target=player tic=B distance=625m weapons=0/1 heat=0->10 risk=0 expected=%.1f targetHealth=5.3999999999999995 updates=225 shots=[3] 0:LRM-15:15:center-torso-front:slot=4:target=player:attach=31:chance=18:roll=4:band=long:cross=74:health=225:updates=0x24=0/0x2c=1/0x2d=1,1:LRM-15:miss:target=player:chance=18:roll=37:band=long:cross=74:impact=24382/109326/0 +2026-04-27T09:51:05.581Z INFO [server:f38959b6] [world/combat] queued match result=loss in 750ms (center torso destroyed) +2026-04-27T09:51:05.628Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=4231, phase=combat) +2026-04-27T09:51:05.629Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 +2026-04-27T09:51:05.629Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 2a 4b 4e 4d 37 4b 5b 31 63 21 21 21 4b 3f |.!*KNM7K[1c!!!K?| +00000010 4b 4b 4b 47 4b 4b 5e 33 67 64 6e 1b |KKKGKK^3gdn.| +2026-04-27T09:51:05.629Z DEBUG [server:f38959b6] [world] rx type-0 len=28 +00000000 1b 21 2a 4b 4e 4d 37 4b 5b 31 63 21 21 21 4b 3f |.!*KNM7K[1c!!!K?| +00000010 4b 4b 4b 47 4b 4b 5e 33 67 64 6e 1b |KKKGKK^3gdn.| +2026-04-27T09:51:05.630Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 +2026-04-27T09:51:05.630Z DEBUG [server:f38959b6] [world/combat] cmd8/cmd9 ignored while result transition is pending +2026-04-27T09:51:06.332Z INFO [server:f38959b6] [world/combat] sending Cmd75/Cmd63 result transition (loss) +2026-04-27T09:51:16.848Z INFO [server:f38959b6] [world/combat] restoring world mode after result scene (loss) +2026-04-27T09:51:16.849Z DEBUG [server:f38959b6] [world] recv 23 bytes (total=4254, phase=world) +2026-04-27T09:51:16.850Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=11 +2026-04-27T09:51:16.850Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| +2026-04-27T09:51:16.850Z DEBUG [server:f38959b6] [world] rx type-0 len=11 +00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| +2026-04-27T09:51:16.850Z DEBUG [server:f38959b6] [world] client seq=0 cmd=3 +2026-04-27T09:51:16.851Z INFO [server:f38959b6] [world] completing deferred world scene refresh (client-ready) +2026-04-27T09:51:16.851Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" +2026-04-27T09:51:16.852Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) +2026-04-27T09:51:16.853Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" +2026-04-27T09:51:20.655Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:51:20.657Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4266, phase=world) +2026-04-27T09:51:20.658Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:51:20.658Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:51:50.662Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:51:50.663Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4278, phase=world) +2026-04-27T09:51:50.664Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:51:50.664Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:52:20.676Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:52:20.678Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4290, phase=world) +2026-04-27T09:52:20.678Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:52:20.678Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:52:50.686Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:52:50.687Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4302, phase=world) +2026-04-27T09:52:50.687Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:52:50.687Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:53:20.690Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:53:20.691Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4314, phase=world) +2026-04-27T09:53:20.692Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:53:20.692Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:53:50.706Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:53:50.707Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4326, phase=world) +2026-04-27T09:53:50.707Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:53:50.707Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:54:20.710Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:54:20.711Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4338, phase=world) +2026-04-27T09:54:20.711Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:54:20.711Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:54:50.715Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:54:50.717Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4350, phase=world) +2026-04-27T09:54:50.717Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:54:50.718Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:55:20.726Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:55:20.727Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4362, phase=world) +2026-04-27T09:55:20.727Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:55:20.728Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:55:50.731Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:55:50.733Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4374, phase=world) +2026-04-27T09:55:50.733Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:55:50.733Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:56:20.742Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:56:20.743Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4386, phase=world) +2026-04-27T09:56:20.744Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:56:20.744Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:56:50.755Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:56:50.756Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4398, phase=world) +2026-04-27T09:56:50.757Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:56:50.757Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:57:20.756Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:57:20.758Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4410, phase=world) +2026-04-27T09:57:20.758Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:57:20.758Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:57:50.770Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:57:50.771Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4422, phase=world) +2026-04-27T09:57:50.771Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:57:50.772Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:58:20.777Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:58:20.778Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4434, phase=world) +2026-04-27T09:58:20.779Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:58:20.779Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:58:50.785Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:58:50.786Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4446, phase=world) +2026-04-27T09:58:50.787Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:58:50.787Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:59:20.790Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:59:20.791Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4458, phase=world) +2026-04-27T09:59:20.791Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:59:20.792Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T09:59:50.791Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T09:59:50.792Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4470, phase=world) +2026-04-27T09:59:50.793Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T09:59:50.793Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:00:20.800Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:00:20.801Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4482, phase=world) +2026-04-27T10:00:20.801Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:00:20.802Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:00:50.813Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:00:50.814Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4494, phase=world) +2026-04-27T10:00:50.814Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:00:50.814Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:01:20.814Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:01:20.815Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4506, phase=world) +2026-04-27T10:01:20.816Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:01:20.816Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:01:50.820Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:01:50.821Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4518, phase=world) +2026-04-27T10:01:50.821Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:01:50.822Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:02:20.820Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:02:20.821Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4530, phase=world) +2026-04-27T10:02:20.822Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:02:20.822Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:02:50.835Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:02:50.836Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4542, phase=world) +2026-04-27T10:02:50.836Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:02:50.836Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:03:20.847Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:03:20.848Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4554, phase=world) +2026-04-27T10:03:20.848Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:03:20.849Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:03:50.853Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:03:50.855Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4566, phase=world) +2026-04-27T10:03:50.855Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:03:50.856Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:04:20.861Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:04:20.862Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4578, phase=world) +2026-04-27T10:04:20.862Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:04:20.863Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:04:50.871Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:04:50.872Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4590, phase=world) +2026-04-27T10:04:50.872Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:04:50.873Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:05:20.886Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:05:20.887Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4602, phase=world) +2026-04-27T10:05:20.887Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:05:20.887Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:05:50.895Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:05:50.896Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4614, phase=world) +2026-04-27T10:05:50.896Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:05:50.896Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:06:20.896Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:06:20.897Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4626, phase=world) +2026-04-27T10:06:20.898Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:06:20.898Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:06:50.906Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:06:50.907Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4638, phase=world) +2026-04-27T10:06:50.907Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:06:50.908Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:07:20.912Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:07:20.913Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4650, phase=world) +2026-04-27T10:07:20.913Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:07:20.914Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:07:50.913Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:07:50.914Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4662, phase=world) +2026-04-27T10:07:50.915Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:07:50.915Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:08:20.928Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:08:20.929Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4674, phase=world) +2026-04-27T10:08:20.930Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:08:20.930Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:08:50.933Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:08:50.934Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4686, phase=world) +2026-04-27T10:08:50.935Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:08:50.935Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:09:20.948Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:09:20.949Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4698, phase=world) +2026-04-27T10:09:20.950Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:09:20.950Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:09:50.962Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:09:50.964Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4710, phase=world) +2026-04-27T10:09:50.964Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:09:50.964Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:10:20.969Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:10:20.971Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4722, phase=world) +2026-04-27T10:10:20.971Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:10:20.971Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:10:50.977Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:10:50.978Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4734, phase=world) +2026-04-27T10:10:50.979Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:10:50.979Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:11:20.979Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:11:20.980Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4746, phase=world) +2026-04-27T10:11:20.980Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:11:20.980Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:11:50.983Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:11:50.984Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4758, phase=world) +2026-04-27T10:11:50.984Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:11:50.985Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:12:20.988Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:12:20.990Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4770, phase=world) +2026-04-27T10:12:20.990Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:12:20.990Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:12:50.996Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:12:50.997Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4782, phase=world) +2026-04-27T10:12:50.997Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:12:50.997Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:13:21.001Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:13:21.002Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4794, phase=world) +2026-04-27T10:13:21.003Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:13:21.003Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:13:51.012Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:13:51.013Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4806, phase=world) +2026-04-27T10:13:51.013Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:13:51.014Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:14:21.019Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:14:21.021Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4818, phase=world) +2026-04-27T10:14:21.021Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:14:21.021Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:14:51.028Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:14:51.030Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4830, phase=world) +2026-04-27T10:14:51.030Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:14:51.030Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:15:21.032Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:15:21.033Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4842, phase=world) +2026-04-27T10:15:21.034Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:15:21.034Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:15:51.039Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:15:51.040Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4854, phase=world) +2026-04-27T10:15:51.040Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:15:51.041Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:16:21.052Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:16:21.053Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4866, phase=world) +2026-04-27T10:16:21.053Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:16:21.053Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:16:51.053Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:16:51.054Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4878, phase=world) +2026-04-27T10:16:51.055Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:16:51.055Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:17:21.056Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:17:21.057Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4890, phase=world) +2026-04-27T10:17:21.057Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:17:21.057Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:17:51.059Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:17:51.060Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4902, phase=world) +2026-04-27T10:17:51.060Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:17:51.061Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:18:21.069Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:18:21.070Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4914, phase=world) +2026-04-27T10:18:21.070Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:18:21.071Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:18:51.082Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:18:51.083Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4926, phase=world) +2026-04-27T10:18:51.083Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:18:51.083Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:19:21.083Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:19:21.084Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4938, phase=world) +2026-04-27T10:19:21.084Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:19:21.085Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:19:51.098Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:19:51.100Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4950, phase=world) +2026-04-27T10:19:51.101Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:19:51.101Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:20:21.113Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:20:21.114Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4962, phase=world) +2026-04-27T10:20:21.114Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:20:21.115Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:20:51.125Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:20:51.127Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4974, phase=world) +2026-04-27T10:20:51.127Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:20:51.127Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:21:21.139Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:21:21.140Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4986, phase=world) +2026-04-27T10:21:21.141Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:21:21.141Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:21:51.152Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:21:51.153Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4998, phase=world) +2026-04-27T10:21:51.153Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:21:51.154Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:22:21.155Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:22:21.156Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=5010, phase=world) +2026-04-27T10:22:21.156Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:22:21.157Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:22:51.164Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:22:51.165Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=5022, phase=world) +2026-04-27T10:22:51.165Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:22:51.165Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:23:21.167Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:23:21.168Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=5034, phase=world) +2026-04-27T10:23:21.168Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:23:21.168Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:23:51.175Z DEBUG [server:f38959b6] [world] keepalive — sending ping +2026-04-27T10:23:51.176Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=5046, phase=world) +2026-04-27T10:23:51.177Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 +2026-04-27T10:23:51.177Z DEBUG [server:f38959b6] [world] keepalive response received +2026-04-27T10:24:20.102Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=5066, phase=world) +2026-04-27T10:24:20.103Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T10:24:20.103Z DEBUG [server:f38959b6] [world][rx] +00000000 1b 21 27 23 2c 66 33 1b |.!'#,f3.| +2026-04-27T10:24:20.103Z DEBUG [server:f38959b6] [world] rx type-0 len=8 +00000000 1b 21 27 23 2c 66 33 1b |.!'#,f3.| +2026-04-27T10:24:20.103Z DEBUG [server:f38959b6] [world] client seq=0 cmd=6 +2026-04-27T10:24:20.104Z DEBUG [server:f38959b6] [world] cmd=6 — not yet handled (M3 stub) +2026-04-27T10:24:20.110Z INFO [server:f38959b6] [world] client disconnected (phase=world, bytes=5066) +2026-04-27T10:24:20.111Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" +2026-04-27T10:49:11.126Z INFO [server:2c39e89c] Client connected from 127.0.0.1:58647 (session 2c39e89c-0de7-4d00-b95f-3a217fc36f92) +2026-04-27T10:49:11.128Z INFO [server:2c39e89c] Sending LOGIN_REQUEST (0x16) — 12 bytes +2026-04-27T10:49:11.130Z DEBUG [server:2c39e89c] recv 349 bytes (total=349, phase=auth) +2026-04-27T10:49:11.130Z DEBUG [server:2c39e89c] pkt type=0x15 tag=0x0 payloadLen=337 +2026-04-27T10:49:11.131Z DEBUG [server:2c39e89c] [rx] +00000000 73 6d 6f 6b 65 5f 73 6f 6c 6f 5f 62 6f 74 5f 61 |smoke_solo_bot_a| +00000010 6c 6c 5f 61 6c 6c 79 5f 67 61 74 65 5f 6d 6f 68 |ll_ally_gate_moh| +00000020 32 72 67 37 71 33 33 32 77 00 00 00 00 00 00 00 |2rg7q332w.......| +00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 20 45 6e 67 69 |Kesmai Comm Engi| +00000080 6e 65 20 33 2e 32 32 00 00 00 00 00 00 00 00 00 |ne 3.22.........| +00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000e0 00 00 00 00 00 00 00 00 42 41 54 54 4c 45 54 45 |........BATTLETE| +000000f0 43 48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |CH..............| +00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 00 |..............9.| +00000140 00 00 0c 00 53 6d 6f 6b 65 50 61 73 73 31 32 33 |....SmokePass123| +00000150 00 |.| +2026-04-27T10:49:11.131Z DEBUG [server:2c39e89c] [auth] login payload len=337 +00000000 73 6d 6f 6b 65 5f 73 6f 6c 6f 5f 62 6f 74 5f 61 |smoke_solo_bot_a| +00000010 6c 6c 5f 61 6c 6c 79 5f 67 61 74 65 5f 6d 6f 68 |ll_ally_gate_moh| +00000020 32 72 67 37 71 33 33 32 77 00 00 00 00 00 00 00 |2rg7q332w.......| +00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 20 45 6e 67 69 |Kesmai Comm Engi| +00000080 6e 65 20 33 2e 32 32 00 00 00 00 00 00 00 00 00 |ne 3.22.........| +00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +000000e0 00 00 00 00 00 00 00 00 42 41 54 54 4c 45 54 45 |........BATTLETE| +000000f0 43 48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |CH..............| +00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 00 |..............9.| +00000140 00 00 0c 00 53 6d 6f 6b 65 50 61 73 73 31 32 33 |....SmokePass123| +00000150 00 |.| +2026-04-27T10:49:11.131Z INFO [server:2c39e89c] [auth] login from "smoke_solo_bot_all_ally_gate_moh2rg7q332w" ver="Kesmai Comm Engine 3.22" service="BATTLETECH" pwLen=12 +2026-04-27T10:49:11.132Z INFO [server:2c39e89c] [login] credentials received: user="smoke_solo_bot_all_ally_gate_moh2rg7q332w" service="BATTLETECH" clientVer="Kesmai Comm Engine 3.22" +2026-04-27T10:49:11.456Z INFO [server:2c39e89c] [login] new account created for "smoke_solo_bot_all_ally_gate_moh2rg7q332w" (id=697) +2026-04-27T10:49:11.456Z INFO [server:2c39e89c] [login] sending SYNC ack — 12 bytes +2026-04-27T10:49:11.457Z INFO [server:2c39e89c] [login] sending WELCOME escape — 45 bytes +2026-04-27T10:49:11.458Z DEBUG [server:2c39e89c] recv 20 bytes (total=369, phase=lobby) +2026-04-27T10:49:11.458Z DEBUG [server:2c39e89c] pkt type=0x00 tag=0x0 payloadLen=8 +2026-04-27T10:49:11.458Z DEBUG [server:2c39e89c] [rx] +00000000 1b 21 24 20 2c 65 6c 1b |.!$ ,el.| +2026-04-27T10:49:11.458Z DEBUG [server:2c39e89c] [game] rx type-0 len=8 +00000000 1b 21 24 20 2c 65 6c 1b |.!$ ,el.| +2026-04-27T10:49:11.459Z DEBUG [server:2c39e89c] [game] client seq=0 cmd=3 phase=lobby +2026-04-27T10:49:11.462Z INFO [server:2c39e89c] [game] no character for account 697 — starting char creation +2026-04-27T10:49:11.462Z INFO [server:2c39e89c] [game] sending Cmd9 character creation prompt (Davion/Steiner/Liao/Marik/Kurita) +2026-04-27T10:49:11.463Z DEBUG [server:2c39e89c] recv 41 bytes (total=410, phase=char-creation) +2026-04-27T10:49:11.464Z DEBUG [server:2c39e89c] pkt type=0x00 tag=0x0 payloadLen=29 +2026-04-27T10:49:11.464Z DEBUG [server:2c39e89c] [rx] +00000000 1b 22 2a 22 33 50 69 6c 6f 74 5f 6d 6f 68 32 72 |."*"3Pilot_moh2r| +00000010 67 37 71 33 33 32 77 25 20 32 60 21 1b |g7q332w% 2`!.| +2026-04-27T10:49:11.465Z DEBUG [server:2c39e89c] [game] rx type-0 len=29 +00000000 1b 22 2a 22 33 50 69 6c 6f 74 5f 6d 6f 68 32 72 |."*"3Pilot_moh2r| +00000010 67 37 71 33 33 32 77 25 20 32 60 21 1b |g7q332w% 2`!.| +2026-04-27T10:49:11.465Z DEBUG [server:2c39e89c] [game] client seq=1 cmd=9 phase=char-creation +2026-04-27T10:49:11.465Z INFO [server:2c39e89c] [game] cmd 9 character reply: subcmd=1 displayName="Pilot_moh2rg7q332w" selection=4 phase=char-creation +2026-04-27T10:49:11.467Z INFO [server:2c39e89c] [game] char-creation Cmd9 accepted: displayName="Pilot_moh2rg7q332w" allegiance=Marik mech=ANH-1A (id=156) → REDIRECT +2026-04-27T10:49:11.468Z INFO [server:2c39e89c] [game] recorded world launch: displayName="Pilot_moh2rg7q332w" allegiance=Marik mech=ANH-1A (id=156) +2026-04-27T10:49:11.468Z INFO [server:2c39e89c] [game] sending REDIRECT → 192.168.1.109:2001 +2026-04-27T10:49:11.986Z ERROR [server:2c39e89c] Socket error: read ECONNRESET +2026-04-27T10:49:11.986Z INFO [server:2c39e89c] Client disconnected (phase=closing, bytes=410) diff --git a/src/api.ts b/src/api.ts index 8d9c8e2..80dc9bf 100644 --- a/src/api.ts +++ b/src/api.ts @@ -5,23 +5,30 @@ * Godot 4 client. The ARIES TCP protocol (ports 2000/2001) is unaffected. * * Endpoints: - * GET /health → { ok: true, version, name } - * GET /world/rooms → { ok: true, rooms: WorldRoom[], source_available: boolean } - * POST /world/travel → { ok: true, room: WorldRoom | null } - * Body: { roomId: number } - * Header: X-Username (authenticated display name) - * GET /world/presence → { ok: true, rooms: Array<{ roomId, occupants: string[] }> } - * POST /world/chat → { ok: true } - * Body: { roomId: number, text: string (max 200 chars) } - * Header: X-Username - * Broadcasts room_chat WebSocket event to all clients - * WS /ws → real-time push: presence_update, room_chat events + * GET /health → { ok: true, version, name } + * GET /mechs → { ok: true, mechs: MechApiEntry[] } + * GET /world/rooms → { ok: true, rooms: WorldRoom[], source_available: boolean } + * POST /world/travel → { ok: true, room: WorldRoom | null } + * Body: { roomId: number } + * Header: X-Username (authenticated display name) + * GET /world/presence → { ok: true, rooms: Array<{ roomId, occupants: string[] }> } + * POST /world/chat → { ok: true } + * Body: { roomId: number, text: string (max 200 chars) } + * Header: X-Username + * Broadcasts room_chat WebSocket event to all clients + * PATCH /world/mech/select → { ok: true, mechId, typeString, slot } + * Body: { mechId: number } + * Header: X-Username + * WS /ws → real-time push: presence_update, room_chat events */ import * as http from 'http'; import { readFileSync } from 'fs'; import { Logger } from './util/logger.js'; import { loadSolarisRooms } from './data/maps.js'; +import { WORLD_MECHS } from './world/world-data.js'; +import { MECH_STATS } from './data/mech-stats.js'; +import { findCharacterByDisplayName, updateCharacterMech } from './db/characters.js'; import { presenceStore } from './world/presence.js'; import { wsBroadcaster } from './world/ws_broadcaster.js'; @@ -31,7 +38,7 @@ const _pkg = JSON.parse( function setCors(res: http.ServerResponse): void { res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Username'); } @@ -82,6 +89,28 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se return; } + if (req.method === 'GET' && pathname === '/mechs') { + const mechs = WORLD_MECHS.map((entry) => { + const stats = MECH_STATS.get(entry.typeString) ?? null; + return { + id: entry.id, + slot: entry.slot, + typeString: entry.typeString, + name: stats?.name ?? '', + weightClass: stats?.weightClass ?? 'unknown', + tonnage: stats?.tonnage ?? entry.tonnage ?? null, + maxSpeedKph: stats?.maxSpeedKph ?? null, + armor: stats?.armor ?? null, + jumpMeters: stats?.jumpMeters ?? null, + armament: stats?.armament ?? [], + effectiveRange: stats?.effectiveRange ?? null, + disabled: stats?.disabled ?? true, + }; + }); + jsonOk(res, { ok: true, mechs }); + return; + } + if (req.method === 'GET' && pathname === '/world/rooms') { const rooms = loadSolarisRooms() ?? []; jsonOk(res, { ok: true, rooms, source_available: rooms.length > 0 }); @@ -180,6 +209,52 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se return; } + if (req.method === 'PATCH' && pathname === '/world/mech/select') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { + jsonError(res, 400, 'X-Username header required'); + return; + } + let body: string; + try { + body = await readBody(req); + } catch { + jsonError(res, 400, 'failed to read request body'); + return; + } + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch { + jsonError(res, 400, 'invalid JSON body'); + return; + } + const mechId = + parsed !== null && + typeof parsed === 'object' && + typeof (parsed as Record).mechId === 'number' + ? ((parsed as Record).mechId as number) + : NaN; + if (!Number.isFinite(mechId)) { + jsonError(res, 400, 'mechId must be a number'); + return; + } + const entry = WORLD_MECHS.find((m) => m.id === mechId); + if (!entry) { + jsonError(res, 404, `mech id ${mechId} not in roster`); + return; + } + const character = await findCharacterByDisplayName(username); + if (!character) { + jsonError(res, 404, 'character not found'); + return; + } + await updateCharacterMech(character.account_id, mechId, entry.slot); + apiLog.info('%s selected mech %s (id=%d slot=%d)', username, entry.typeString, mechId, entry.slot); + jsonOk(res, { ok: true, mechId, typeString: entry.typeString, slot: entry.slot }); + return; + } + res.writeHead(404); res.end(); }); diff --git a/src/db/characters.ts b/src/db/characters.ts index 9af6e69..0b21c98 100644 --- a/src/db/characters.ts +++ b/src/db/characters.ts @@ -165,6 +165,18 @@ export async function settleDuelStakeTransfer( } } +/** Find the character for a given display name (case-insensitive), or null if none exists. */ +export async function findCharacterByDisplayName(displayName: string): Promise { + const res = await pool.query( + `SELECT id, account_id, display_name, allegiance, cbills, mech_id, mech_slot, created_at + FROM characters + WHERE lower(display_name) = lower($1) + LIMIT 1`, + [displayName], + ); + return res.rows[0] ?? null; +} + /** * Check whether a display name is already taken by another character. * Used to re-prompt when a chosen name is unavailable. diff --git a/src/protocol/combat.ts b/src/protocol/combat.ts index a5e5d26..3390062 100644 --- a/src/protocol/combat.ts +++ b/src/protocol/combat.ts @@ -145,8 +145,9 @@ export function buildCmd63ArenaSceneInitPacket(seq = 0): Buffer { // Wire layout (confirmed by Ghidra decompile): // slot byte — external slot ID; client assigns internal slot and // stores reverse-mapping in DAT_00478d98/DAT_00478dc0 -// actorTypeByte byte — stored at DAT_004f2036 + internalSlot*0x49c; purpose -// semantically unconfirmed; use 0 for prototype +// actorTypeByte byte — stored at DAT_004f2036 + internalSlot*0x49c; v1.29 RE +// shows 0 keeps normal LOS-gated visibility while non-zero +// forces actor flag 0x20 each tick (use 1 for teammates) // identity0 str11 — max 11 bytes; trailing digits → actor display index // identity1 str31 — max 31 bytes // identity2 str39 — max 39 bytes @@ -162,7 +163,8 @@ export interface Cmd64RemoteActor { slot: number; /** * Second byte after slot; stored at DAT_004f2036 + internalSlot*0x49c. - * Semantics unconfirmed — send 0 for prototype. + * v1.29 RE: 0 keeps normal LOS gating; non-zero forces actor flag 0x20. + * Use 1 for teammate remotes that should stay rendered/radar-visible. */ actorTypeByte: number; /** max 11 bytes — identity string 0; trailing digits → actor display index */ diff --git a/src/server-world.ts b/src/server-world.ts index 9233fef..43ac9c1 100644 --- a/src/server-world.ts +++ b/src/server-world.ts @@ -159,10 +159,12 @@ import { handleArenaSideSelection, completePendingWorldReadySceneRefresh, flushPendingDuelSettlementNotice, + sendWorldCommandHelp, } from './world/world-handlers.js'; const _pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }; const WELCOME_TEXT = `Welcome to the game world. (Server v${_pkg.version})`; +const COMMAND_HINT_TEXT = "Commands are available in chat. Type /help for command syntax."; // ── Login handler ───────────────────────────────────────────────────────────── @@ -229,6 +231,10 @@ async function handleWorldLogin( session.selectedMechSlot = accountResume?.selectedMechSlot ?? launch.mechSlot; session.worldArenaSide = accountResume?.worldArenaSide; session.worldArenaReadyRoomId = accountResume?.worldArenaReadyRoomId; + session.combatBotOpponentCount = accountResume?.combatBotOpponentCount; + session.combatBotLoadoutIds = accountResume?.combatBotLoadoutIds ? [...accountResume.combatBotLoadoutIds] : undefined; + session.combatBotSides = accountResume?.combatBotSides ? [...accountResume.combatBotSides] : undefined; + session.combatBotDifficultyLevel = accountResume?.combatBotDifficultyLevel; session.pendingDuelSettlementNotice = accountResume?.pendingDuelSettlementNotice; if (session.accountId !== undefined) { session.worldRosterId = 100000 + session.accountId; @@ -587,6 +593,10 @@ function handleWorldGameData( return; } if (session.phase === 'combat') { + if (textCmd === '/help') { + sendWorldCommandHelp(session, capture); + return; + } if (handleBotMechTextCommand(session, parsed.text, connLog, capture, { suppressBroadcast: true })) { return; } @@ -1081,6 +1091,7 @@ function sendWorldInitSequence( // Cmd3 — TextBroadcast: welcome message. g_chatReady is set to 1 by Cmd4, so // this is the earliest point at which Cmd3 will be displayed by the client. send(socket, buildCmd3BroadcastPacket(WELCOME_TEXT, nextSeq(session)), capture, 'CMD3_WELCOME'); + send(socket, buildCmd3BroadcastPacket(COMMAND_HINT_TEXT, nextSeq(session)), capture, 'CMD3_HELP_HINT'); // Cmd5 — CursorNormal: restore the arrow cursor. send(socket, buildCmd5CursorNormalPacket(nextSeq(session)), capture, 'CMD5_NORMAL'); diff --git a/src/state/players.ts b/src/state/players.ts index adcb440..fe3f569 100644 --- a/src/state/players.ts +++ b/src/state/players.ts @@ -4,6 +4,7 @@ import { randomUUID } from 'crypto'; import type { Socket } from 'net'; +import type { BotDifficultyLevel } from '../world/combat-config.js'; export type SessionPhase = | 'connected' // TCP accepted, waiting for first bytes @@ -72,6 +73,43 @@ export interface WorldScrollListState { classKey?: 'LIGHT' | 'MEDIUM' | 'HEAVY' | 'ASSAULT'; } +export type MechPickerTarget = 'player' | 'bot'; + +export interface SoloCombatBotActorState { + slot: number; + mechId: number; + side: number; + health: number; + armorValues: number[]; + internalValues: number[]; + criticalStateBytes: number[]; + headArmor: number; + x: number; + y: number; + z: number; + facing: number; + speedMag: number; + moveVectorX: number; + moveVectorY: number; + weaponReadyAtBySlot: number[]; + ammoStateValues: number[]; + heat: number; + jumpActive: boolean; + jumpFuel: number; + lastMoveAt?: number; + lastAimLimitLogAt?: number; + jumpStartedAt?: number; + jumpDurationMs?: number; + jumpStartFuel?: number; + jumpApexUnits?: number; + jumpStartX?: number; + jumpStartY?: number; + jumpTargetX?: number; + jumpTargetY?: number; + lastJumpAt?: number; + deathTimer?: ReturnType; +} + /** * NOTE — cross-session writes and CaptureLogger: * @@ -154,7 +192,7 @@ export interface ClientSession { combatJumpActive?: boolean; /** Repeating setInterval that regenerates jump-jet fuel while grounded. */ combatJumpFuelRegenTimer?: ReturnType; - /** Aggregate scripted bot durability for logging and simple win gating. */ + /** Aggregate scripted bot durability across all active solo-combat bot actors. */ botHealth?: number; /** * Server-side approximation of the player's remaining IS health. @@ -353,6 +391,25 @@ export interface ClientSession { * used instead of the player's own mech when bootstrapping combat. */ combatBotMechId?: number; + /** + * Configured bot roster count for single-player bot/droid fights. + * Valid range is 1..7 (8 total combatants including the local player). + */ + combatBotOpponentCount?: number; + /** + * Configured per-bot mech overrides for single-player bot/droid fights. + * Index 0 corresponds to the first remote bot slot, index 6 to the seventh. + */ + combatBotLoadoutIds?: number[]; + /** + * Configured per-bot team/side assignments for single-player bot/droid fights. + * Index 0 corresponds to the first remote bot slot, index 6 to the seventh. + */ + combatBotSides?: number[]; + /** Configured solo-bot difficulty preset (1 easiest .. 5 hardest). Defaults to 4 when unset. */ + combatBotDifficultyLevel?: BotDifficultyLevel; + /** Runtime solo-combat bot actors keyed by remote slot order (1..7). */ + combatBotActors?: SoloCombatBotActorState[]; /** Wall-clock timestamp (Date.now()) when the current combat bootstrap was sent. */ combatStartAt?: number; /** Per-mech run/max speedMag cap (round(mec_speed * 1.5) * 300), set at combat bootstrap. */ @@ -464,6 +521,10 @@ export interface ClientSession { /** Which step of the mech-picker dialog the player is on. */ mechPickerStep?: 'class' | 'chassis' | 'variant'; + /** Target the active mech picker is editing. */ + mechPickerTarget?: MechPickerTarget; + /** 0-based bot index targeted by the active mech picker when `mechPickerTarget === "bot"`. */ + mechPickerTargetBotIndex?: number; /** Weight-class index (0=Light, 1=Medium, 2=Heavy, 3=Assault) chosen in step 1. */ mechPickerClass?: number; /** Chassis name (e.g. "Jenner") chosen in step 2. */ diff --git a/src/state/world-resume.ts b/src/state/world-resume.ts index 4b5fe45..c422542 100644 --- a/src/state/world-resume.ts +++ b/src/state/world-resume.ts @@ -7,6 +7,7 @@ */ import type { ClientSession } from './players.js'; +import type { BotDifficultyLevel } from '../world/combat-config.js'; export interface PendingWorldResume { accountId?: number; @@ -19,6 +20,10 @@ export interface PendingWorldResume { worldArenaReadyRoomId?: number; selectedMechId?: number; selectedMechSlot?: number; + combatBotOpponentCount?: number; + combatBotLoadoutIds?: number[]; + combatBotSides?: number[]; + combatBotDifficultyLevel?: BotDifficultyLevel; pendingDuelSettlementNotice?: string; } @@ -34,6 +39,10 @@ type ResumeSnapshotSource = Pick< | 'worldArenaReadyRoomId' | 'selectedMechId' | 'selectedMechSlot' + | 'combatBotOpponentCount' + | 'combatBotLoadoutIds' + | 'combatBotSides' + | 'combatBotDifficultyLevel' | 'pendingDuelSettlementNotice' >; @@ -66,6 +75,10 @@ class WorldResumeRegistry { worldArenaReadyRoomId: source.worldArenaReadyRoomId, selectedMechId: source.selectedMechId, selectedMechSlot: source.selectedMechSlot, + combatBotOpponentCount: source.combatBotOpponentCount, + combatBotLoadoutIds: source.combatBotLoadoutIds ? [...source.combatBotLoadoutIds] : undefined, + combatBotSides: source.combatBotSides ? [...source.combatBotSides] : undefined, + combatBotDifficultyLevel: source.combatBotDifficultyLevel, pendingDuelSettlementNotice: source.pendingDuelSettlementNotice, }; diff --git a/src/world/combat-config.ts b/src/world/combat-config.ts index 2178f38..9c03c49 100644 --- a/src/world/combat-config.ts +++ b/src/world/combat-config.ts @@ -25,6 +25,9 @@ export const BOT_SPAWN_DISTANCE = 3_000 * COMBAT_WORLD_UNITS_PER_METER; /** Initial single-player AI-bot stand-off distance in combat world units (3000m north). */ export const BOT_AI_SPAWN_DISTANCE = BOT_SPAWN_DISTANCE; +/** Same-team combat spawn spacing in combat world units (20m apart). */ +export const COMBAT_TEAMMATE_SPAWN_SPACING = 20 * COMBAT_WORLD_UNITS_PER_METER; + /** * Fallback per-weapon damage used when the firing mech's weapon loadout is not * documented in BT-MAN yet. Chosen to approximate a medium weapon hit. @@ -170,6 +173,81 @@ export const BOT_TO_HIT_TARGET_CROSSING_MAX_PENALTY = 0.22; export const BOT_TO_HIT_ATTACKER_JUMP_PENALTY = 0.08; export const BOT_TO_HIT_TARGET_JUMP_PENALTY = 0.12; +export type BotDifficultyLevel = 1 | 2 | 3 | 4 | 5; + +export interface BotDifficultyProfile { + level: BotDifficultyLevel; + rangePressureBonusMeters: number; + finisherPushHealthThreshold: number; + toHitBaseChance: number; + toHitMaxChance: number; + toHitShortRangeBonus: number; +} + +/** Default live-bot difficulty. Level 4 matches the current tuned AI. */ +export const DEFAULT_BOT_DIFFICULTY_LEVEL: BotDifficultyLevel = 4; + +/** + * Solo-bot difficulty presets. + * + * Level 4 is the current tuned live behavior. + * Level 5 restores the pre-tuning values from before bots were softened. + * Levels 1-3 are progressively easier extrapolations below the current level 4. + */ +export const BOT_DIFFICULTY_PROFILES: Record = { + 1: { + level: 1, + rangePressureBonusMeters: 10, + finisherPushHealthThreshold: 12, + toHitBaseChance: 0.54, + toHitMaxChance: 0.76, + toHitShortRangeBonus: 0.06, + }, + 2: { + level: 2, + rangePressureBonusMeters: 15, + finisherPushHealthThreshold: 16, + toHitBaseChance: 0.59, + toHitMaxChance: 0.80, + toHitShortRangeBonus: 0.08, + }, + 3: { + level: 3, + rangePressureBonusMeters: 20, + finisherPushHealthThreshold: 20, + toHitBaseChance: 0.64, + toHitMaxChance: 0.84, + toHitShortRangeBonus: 0.10, + }, + 4: { + level: 4, + rangePressureBonusMeters: BOT_AI_RANGE_PRESSURE_BONUS_METERS, + finisherPushHealthThreshold: BOT_AI_FINISHER_PUSH_HEALTH_THRESHOLD, + toHitBaseChance: BOT_TO_HIT_BASE_CHANCE, + toHitMaxChance: BOT_TO_HIT_MAX_CHANCE, + toHitShortRangeBonus: BOT_TO_HIT_SHORT_RANGE_BONUS, + }, + 5: { + level: 5, + rangePressureBonusMeters: 35, + finisherPushHealthThreshold: 32, + toHitBaseChance: 0.74, + toHitMaxChance: 0.92, + toHitShortRangeBonus: 0.15, + }, +}; + +export function clampBotDifficultyLevel(level: number | undefined): BotDifficultyLevel { + const normalized = Math.trunc(level ?? DEFAULT_BOT_DIFFICULTY_LEVEL); + if (normalized <= 1) return 1; + if (normalized >= 5) return 5; + return normalized as BotDifficultyLevel; +} + +export function getBotDifficultyProfile(level: number | undefined): BotDifficultyProfile { + return BOT_DIFFICULTY_PROFILES[clampBotDifficultyLevel(level)]; +} + /** Miss visuals land beside the target instead of always "hitting" center mass. */ export const BOT_MISS_OFFSET_MIN_METERS = 10; export const BOT_MISS_OFFSET_MAX_METERS = 36; diff --git a/src/world/world-handlers.ts b/src/world/world-handlers.ts index e9bdcfb..f88890c 100644 --- a/src/world/world-handlers.ts +++ b/src/world/world-handlers.ts @@ -68,7 +68,7 @@ import { MOTION_DIV, MOTION_NEUTRAL, } from '../protocol/combat.js'; -import { PlayerRegistry, ClientSession, type CombatSession, type DuelCombatSession } from '../state/players.js'; +import { PlayerRegistry, ClientSession, type CombatSession, type DuelCombatSession, type SoloCombatBotActorState } from '../state/players.js'; import { worldResumeRegistry } from '../state/world-resume.js'; import { countSavedUnreadMessages, @@ -180,6 +180,7 @@ import { BOT_AI_SPAWN_DISTANCE, BOT_FALLBACK_WEAPON_DAMAGE, COMBAT_WORLD_UNITS_PER_METER, + COMBAT_TEAMMATE_SPAWN_SPACING, JUMP_JET_DEFAULT_APEX_METERS, JUMP_JET_ASCENT_STEPS, JUMP_JET_TICK_MS, @@ -209,18 +210,14 @@ import { BOT_AI_RANGE_FIT_MEDIUM_WEIGHT, BOT_AI_RANGE_FIT_LONG_WEIGHT, BOT_AI_RANGE_ADVANTAGE_BONUS_METERS, - BOT_AI_RANGE_PRESSURE_BONUS_METERS, - BOT_AI_FINISHER_PUSH_HEALTH_THRESHOLD, + DEFAULT_BOT_DIFFICULTY_LEVEL, BOT_AI_HEAT_DISSIPATION_WINDOW_MS, BOT_AI_TIC_ALPHA_HEAT_RATIO, BOT_AI_TIC_SUSTAIN_HEAT_RATIO, BOT_AI_TIC_POKE_HEAT_RATIO, BOT_AI_TIC_SAFE_OVERHEAT_RISK, BOT_AI_TIC_FINISHER_OVERHEAT_RISK, - BOT_TO_HIT_BASE_CHANCE, BOT_TO_HIT_MIN_CHANCE, - BOT_TO_HIT_MAX_CHANCE, - BOT_TO_HIT_SHORT_RANGE_BONUS, BOT_TO_HIT_MEDIUM_RANGE_BONUS, BOT_TO_HIT_LONG_RANGE_MAX_PENALTY, BOT_TO_HIT_ATTACKER_SPEED_MAX_PENALTY, @@ -230,6 +227,9 @@ import { BOT_TO_HIT_TARGET_JUMP_PENALTY, BOT_MISS_OFFSET_MIN_METERS, BOT_MISS_OFFSET_MAX_METERS, + clampBotDifficultyLevel, + getBotDifficultyProfile, + type BotDifficultyProfile, VERIFY_DELAY_MS, VERIFY_SWEEP_STEP_MS, VERIFY_DAMAGE_CODES, @@ -248,14 +248,13 @@ function regenJumpFuelIfGrounded( } function regenBotJumpFuelIfGrounded( - session: ClientSession, + bot: SoloCombatBotActorState, amount: number, ): void { - if (session.combatBotJumpActive) return; - if ((session.combatBotZ ?? 0) > 0) return; - const fuel = session.combatBotJumpFuel ?? JUMP_JET_FUEL_MAX; - if (fuel >= JUMP_JET_FUEL_MAX) return; - session.combatBotJumpFuel = Math.min(JUMP_JET_FUEL_MAX, fuel + amount); + if (bot.jumpActive) return; + if (bot.z > 0) return; + if (bot.jumpFuel >= JUMP_JET_FUEL_MAX) return; + bot.jumpFuel = Math.min(JUMP_JET_FUEL_MAX, bot.jumpFuel + amount); } function mechSupportsJumpJets(mechId: number | undefined): boolean { @@ -993,6 +992,17 @@ function getSharedArenaSide( return side; } +const COMBAT_REMOTE_ACTOR_TYPE_DEFAULT = 0; +const COMBAT_REMOTE_ACTOR_TYPE_FORCE_VISIBLE = 1; + +function getCombatRemoteActorTypeByte(forceVisible: boolean): number { + // v1.29 RE: non-zero actorTypeByte makes Combat_MainLoop force actor flag 0x20 + // instead of relying on LOS/occlusion, which keeps teammates rendered and on radar. + return forceVisible + ? COMBAT_REMOTE_ACTOR_TYPE_FORCE_VISIBLE + : COMBAT_REMOTE_ACTOR_TYPE_DEFAULT; +} + function getLiveArenaRoomParticipants(players: PlayerRegistry, roomId: string): ClientSession[] { return players.inRoom(roomId).filter(other => other.phase === 'world' @@ -1165,7 +1175,7 @@ export function handleArenaReadyRoomSelection( session, connLog, capture, - `Entered ${getSolarisRoomName(arenaRoomId)} - ${getArenaReadyRoomLabel(readyRoomId)}.`, + `Entered ${getSolarisRoomName(arenaRoomId)} - ${getArenaReadyRoomLabel(readyRoomId)}. Type /help to see available commands.`, ); notifyRoomArrival(players, session, connLog); } @@ -1378,6 +1388,24 @@ function getCombatSourceSlotForViewer( return getCombatRemoteSlotForViewer(combatSession, viewer, source) ?? 0; } +function buildCombatTeamClusterPositions( + items: readonly T[], + baseX: number, + baseY: number, + tangentX: number, + tangentY: number, +): Array<{ item: T; x: number; y: number }> { + const centerOffset = (items.length - 1) / 2; + return items.map((item, index) => { + const lateralOffset = (index - centerOffset) * COMBAT_TEAMMATE_SPAWN_SPACING; + return { + item, + x: Math.round(baseX + (tangentX * lateralOffset)), + y: Math.round(baseY + (tangentY * lateralOffset)), + }; + }); +} + function getArenaCombatSpawnPoints(participants: ClientSession[]): Array<{ x: number; y: number }> { if (participants.length <= 1) { return [{ x: 0, y: 0 }]; @@ -1396,7 +1424,6 @@ function getArenaCombatSpawnPoints(participants: ClientSession[]): Array<{ x: nu const groupedSides = [...groups.entries()] .sort(([sideA], [sideB]) => sideA - sideB); const radius = BOT_SPAWN_DISTANCE; - const teammateSpacing = Math.round(BOT_SPAWN_DISTANCE * 0.18); const spawnByParticipantId = new Map(); for (let groupIndex = 0; groupIndex < groupedSides.length; groupIndex += 1) { @@ -1406,14 +1433,10 @@ function getArenaCombatSpawnPoints(participants: ClientSession[]): Array<{ x: nu const baseY = Math.cos(angle) * radius; const tangentX = Math.cos(angle); const tangentY = -Math.sin(angle); - const centerOffset = (groupParticipants.length - 1) / 2; - - for (let memberIndex = 0; memberIndex < groupParticipants.length; memberIndex += 1) { - const participant = groupParticipants[memberIndex]; - const lateralOffset = (memberIndex - centerOffset) * teammateSpacing; - spawnByParticipantId.set(participant.id, { - x: Math.round(baseX + (tangentX * lateralOffset)), - y: Math.round(baseY + (tangentY * lateralOffset)), + for (const position of buildCombatTeamClusterPositions(groupParticipants, baseX, baseY, tangentX, tangentY)) { + spawnByParticipantId.set(position.item.id, { + x: position.x, + y: position.y, }); } } @@ -3093,7 +3116,11 @@ function getCombatRangeBandForDistance( return 'long'; } -function estimateCombatToHit(input: CombatToHitRollInput): CombatToHitEstimate { +function estimateCombatToHit( + input: CombatToHitRollInput, + difficultyProfile?: BotDifficultyProfile, +): CombatToHitEstimate { + const profile = difficultyProfile ?? getBotDifficultyProfile(DEFAULT_BOT_DIFFICULTY_LEVEL); const { shortRangeCap, mediumRangeCap, longRangeCap } = getCombatRangeCaps(input.weaponSpec, input.maxRangeMeters); const rangeBand = getCombatRangeBandForDistance(input.weaponSpec, input.distanceMeters, input.maxRangeMeters); const attackerSpeedRatio = getSpeedRatio(input.attackerSpeedMag, input.attackerMaxSpeedMag); @@ -3126,7 +3153,7 @@ function estimateCombatToHit(input: CombatToHitRollInput): CombatToHitEstimate { let rangeModifier = 0; if (rangeBand === 'short') { - rangeModifier = BOT_TO_HIT_SHORT_RANGE_BONUS; + rangeModifier = profile.toHitShortRangeBonus; } else if (rangeBand === 'medium') { rangeModifier = BOT_TO_HIT_MEDIUM_RANGE_BONUS; } else if (longRangeCap !== undefined && longRangeCap > mediumRangeCap) { @@ -3140,7 +3167,7 @@ function estimateCombatToHit(input: CombatToHitRollInput): CombatToHitEstimate { return { chance: clampNumber( - BOT_TO_HIT_BASE_CHANCE + profile.toHitBaseChance + rangeModifier - (attackerSpeedRatio * BOT_TO_HIT_ATTACKER_SPEED_MAX_PENALTY) - (targetSpeedRatio * BOT_TO_HIT_TARGET_SPEED_MAX_PENALTY) @@ -3148,15 +3175,18 @@ function estimateCombatToHit(input: CombatToHitRollInput): CombatToHitEstimate { - (input.attackerAirborne ? BOT_TO_HIT_ATTACKER_JUMP_PENALTY : 0) - (input.targetAirborne ? BOT_TO_HIT_TARGET_JUMP_PENALTY : 0), BOT_TO_HIT_MIN_CHANCE, - BOT_TO_HIT_MAX_CHANCE, + profile.toHitMaxChance, ), rangeBand, crossingFactor, }; } -function resolveCombatToHitRoll(input: CombatToHitRollInput): CombatToHitRoll { - const estimate = estimateCombatToHit(input); +function resolveCombatToHitRoll( + input: CombatToHitRollInput, + difficultyProfile?: BotDifficultyProfile, +): CombatToHitRoll { + const estimate = estimateCombatToHit(input, difficultyProfile); const roll = Math.random(); return { hit: roll <= estimate.chance, @@ -3197,9 +3227,9 @@ function getMissImpactPoint( }; } -function setBotMoveVector(session: ClientSession, moveVectorX: number, moveVectorY: number): void { - session.combatBotMoveVectorX = moveVectorX; - session.combatBotMoveVectorY = moveVectorY; +function setBotMoveVector(bot: SoloCombatBotActorState, moveVectorX: number, moveVectorY: number): void { + bot.moveVectorX = moveVectorX; + bot.moveVectorY = moveVectorY; } function setPlayerMoveVector(session: ClientSession, moveVectorX: number, moveVectorY: number): void { @@ -3233,8 +3263,8 @@ interface BotTicPreset { overheatRisk: number; } -function getBotHeatSinkCount(session: ClientSession): number { - return Math.max(1, getMechEntryForId(getBotMechId(session))?.heatSinks ?? 10); +function getBotHeatSinkCount(bot: SoloCombatBotActorState): number { + return Math.max(1, getMechEntryForId(bot.mechId)?.heatSinks ?? 10); } function getBotHeatCapacity(heatSinks: number): number { @@ -3251,13 +3281,13 @@ function estimateBotOverheatRisk(currentHeat: number, addedHeat: number, heatSin ); } -function coolBotHeat(session: ClientSession, heatSinks: number): void { - const currentHeat = session.combatBotHeat ?? 0; +function coolBotHeat(bot: SoloCombatBotActorState, heatSinks: number): void { + const currentHeat = bot.heat; const cooledHeat = Math.max( 0, currentHeat - ((heatSinks * BOT_AI_TICK_MS) / BOT_AI_HEAT_DISSIPATION_WINDOW_MS), ); - session.combatBotHeat = cooledHeat; + bot.heat = cooledHeat; } function buildBotTicPresetFromCandidates( @@ -3344,6 +3374,7 @@ function chooseBotTicPreset( playerHealth: number, currentHeat: number, heatSinks: number, + difficultyProfile: BotDifficultyProfile, ): BotTicPreset | undefined { const alpha = presets.A; const sustain = presets.B; @@ -3356,7 +3387,7 @@ function chooseBotTicPreset( const heatRatio = currentHeat / heatCapacity; if ( - playerHealth <= BOT_AI_FINISHER_PUSH_HEALTH_THRESHOLD + playerHealth <= difficultyProfile.finisherPushHealthThreshold && alpha.shots.length > 0 && alpha.overheatRisk <= BOT_AI_TIC_FINISHER_OVERHEAT_RISK && alpha.totalExpectedDamage >= Math.max(5, sustain.totalExpectedDamage * 0.85) @@ -3542,11 +3573,11 @@ function markWeaponSlotFired( } function getBotWeaponAmmoGate( - session: ClientSession, + bot: SoloCombatBotActorState, weaponSlot: number, consume: boolean, ): { allowed: boolean; remainingAmmo?: number; ammoBinIndex?: number; damageCode?: number; weaponName?: string } { - const botMechId = session.combatBotMechId ?? session.selectedMechId ?? FALLBACK_MECH_ID; + const botMechId = bot.mechId; const mechEntry = getMechEntryForId(botMechId); const weaponName = getWeaponNameForMechSlot(botMechId, weaponSlot); const weaponTypeId = getWeaponTypeIdForMechSlot(botMechId, weaponSlot); @@ -3568,8 +3599,8 @@ function getBotWeaponAmmoGate( } const ammoStateValues = consume - ? getOrCreateAmmoStateValues(session.combatBotAmmoStateValues, mechEntry.ammoBinTypeIds) - : (session.combatBotAmmoStateValues ?? getInitialCombatAmmoStateValues(mechEntry.ammoBinTypeIds)); + ? getOrCreateAmmoStateValues(bot.ammoStateValues, mechEntry.ammoBinTypeIds) + : (bot.ammoStateValues.length > 0 ? bot.ammoStateValues : getInitialCombatAmmoStateValues(mechEntry.ammoBinTypeIds)); const ammoBinIndex = matchingAmmoBinIndexes.find(index => (ammoStateValues[index] ?? 0) > 0); if (ammoBinIndex === undefined) { return { allowed: false, weaponName }; @@ -3578,7 +3609,7 @@ function getBotWeaponAmmoGate( const remainingAmmo = Math.max(0, (ammoStateValues[ammoBinIndex] ?? 0) - 1); if (consume) { ammoStateValues[ammoBinIndex] = remainingAmmo; - session.combatBotAmmoStateValues = ammoStateValues; + bot.ammoStateValues = ammoStateValues; } return { allowed: true, @@ -3590,21 +3621,21 @@ function getBotWeaponAmmoGate( } function peekBotWeaponAmmo( - session: ClientSession, + bot: SoloCombatBotActorState, weaponSlot: number, ): { allowed: boolean; remainingAmmo?: number; ammoBinIndex?: number; damageCode?: number; weaponName?: string } { - return getBotWeaponAmmoGate(session, weaponSlot, false); + return getBotWeaponAmmoGate(bot, weaponSlot, false); } function consumeBotWeaponAmmo( - session: ClientSession, + bot: SoloCombatBotActorState, weaponSlot: number, ): { allowed: boolean; remainingAmmo?: number; ammoBinIndex?: number; damageCode?: number; weaponName?: string } { - return getBotWeaponAmmoGate(session, weaponSlot, true); + return getBotWeaponAmmoGate(bot, weaponSlot, true); } function markBotWeaponSlotFired( - session: ClientSession, + bot: SoloCombatBotActorState, weaponSlot: number, cooldownMs: number | undefined, now: number, @@ -3612,10 +3643,7 @@ function markBotWeaponSlotFired( if (cooldownMs === undefined || cooldownMs <= 0) { return; } - if (!session.combatBotWeaponReadyAtBySlot) { - session.combatBotWeaponReadyAtBySlot = []; - } - session.combatBotWeaponReadyAtBySlot[weaponSlot] = now + cooldownMs; + bot.weaponReadyAtBySlot[weaponSlot] = now + cooldownMs; } function clearPendingCombatAction0Followup(session: ClientSession): void { @@ -3735,12 +3763,12 @@ function applyDamageToSection( } function chooseRetaliationHitSection( - session: ClientSession, + session: ClientSession | undefined, armorValues: readonly number[], internalValues: readonly number[], headArmor: number, ): CombatAttachmentHitSection { - const start = session.combatRetaliationCursor ?? 0; + const start = session?.combatRetaliationCursor ?? 0; for (let offset = 0; offset < LOCAL_RETALIATION_SECTIONS.length; offset++) { const idx = (start + offset) % LOCAL_RETALIATION_SECTIONS.length; const section = LOCAL_RETALIATION_SECTIONS[idx]; @@ -3750,14 +3778,18 @@ function chooseRetaliationHitSection( : section.internalIndex === 7 ? headArmor : 0) > 0 || (internalValues[section.internalIndex] ?? 0) > 0 ) { - session.combatRetaliationCursor = (idx + 1) % LOCAL_RETALIATION_SECTIONS.length; + if (session) { + session.combatRetaliationCursor = (idx + 1) % LOCAL_RETALIATION_SECTIONS.length; + } return section; } } const fallback = LOCAL_RETALIATION_SECTIONS[start % LOCAL_RETALIATION_SECTIONS.length] ?? LOCAL_RETALIATION_SECTIONS[0]; - session.combatRetaliationCursor = ((start % LOCAL_RETALIATION_SECTIONS.length) + 1) - % LOCAL_RETALIATION_SECTIONS.length; + if (session) { + session.combatRetaliationCursor = ((start % LOCAL_RETALIATION_SECTIONS.length) + 1) + % LOCAL_RETALIATION_SECTIONS.length; + } return fallback; } @@ -4060,27 +4092,470 @@ function getBotMechId(session: ClientSession): number { return session.combatBotMechId ?? session.selectedMechId ?? FALLBACK_MECH_ID; } +const MAX_SINGLE_PLAYER_BOT_OPPONENTS = 7; +const MIN_SOLO_COMBAT_SIDE = 1; +const MAX_SOLO_COMBAT_SIDE = 8; + +function clampBotOpponentCount(count: number | undefined): number { + return Math.max(1, Math.min(MAX_SINGLE_PLAYER_BOT_OPPONENTS, Math.trunc(count ?? 1))); +} + +function clampSoloCombatSide(side: number | undefined, fallback = MIN_SOLO_COMBAT_SIDE): number { + if (side === undefined || !Number.isFinite(side)) { + return fallback; + } + return Math.max(MIN_SOLO_COMBAT_SIDE, Math.min(MAX_SOLO_COMBAT_SIDE, Math.trunc(side))); +} + +function getConfiguredSoloCombatPlayerSide(session: ClientSession): number { + return clampSoloCombatSide(session.worldArenaSide, MIN_SOLO_COMBAT_SIDE); +} + +function getConfiguredSoloCombatDefaultEnemySide(session: ClientSession): number { + const playerSide = getConfiguredSoloCombatPlayerSide(session); + return playerSide === MIN_SOLO_COMBAT_SIDE ? 2 : MIN_SOLO_COMBAT_SIDE; +} + +function getConfiguredBotOpponentCount(session: ClientSession): number { + return clampBotOpponentCount(session.combatBotOpponentCount); +} + +function getConfiguredBotDifficultyLevel(session: ClientSession): number { + return clampBotDifficultyLevel(session.combatBotDifficultyLevel ?? DEFAULT_BOT_DIFFICULTY_LEVEL); +} + +function getConfiguredBotDifficultyProfile(session: ClientSession): BotDifficultyProfile { + return getBotDifficultyProfile(getConfiguredBotDifficultyLevel(session)); +} + +function getConfiguredBotLoadoutId( + session: ClientSession, + botIndex: number, +): number { + const index = Math.max(0, Math.min(MAX_SINGLE_PLAYER_BOT_OPPONENTS - 1, botIndex)); + const configured = session.combatBotLoadoutIds?.[index]; + if (configured !== undefined) { + return configured; + } + const primary = session.combatBotLoadoutIds?.[0] ?? session.combatBotMechId; + return primary ?? session.selectedMechId ?? FALLBACK_MECH_ID; +} + +function getConfiguredBotLoadoutLabel( + session: ClientSession, + botIndex: number, +): string { + const mechId = getConfiguredBotLoadoutId(session, botIndex); + const mechEntry = WORLD_MECH_BY_ID.get(mechId); + return mechEntry + ? `${mechEntry.typeString} (id=${mechId})` + : `Mech ${mechId}`; +} + +function setConfiguredBotLoadoutId( + session: ClientSession, + botIndex: number, + mechId: number, +): void { + const index = Math.max(0, Math.min(MAX_SINGLE_PLAYER_BOT_OPPONENTS - 1, botIndex)); + const loadoutIds = [...(session.combatBotLoadoutIds ?? [])]; + loadoutIds[index] = mechId; + if (index === 0) { + session.combatBotMechId = mechId; + loadoutIds[0] = mechId; + } + session.combatBotLoadoutIds = loadoutIds; + session.combatBotOpponentCount = Math.max(getConfiguredBotOpponentCount(session), index + 1); +} + +function getConfiguredBotSide( + session: ClientSession, + botIndex: number, + verificationMode?: ClientSession['combatVerificationMode'], +): number { + if (verificationMode) { + return getConfiguredSoloCombatDefaultEnemySide(session); + } + const index = Math.max(0, Math.min(MAX_SINGLE_PLAYER_BOT_OPPONENTS - 1, botIndex)); + const configured = session.combatBotSides?.[index]; + return clampSoloCombatSide(configured, getConfiguredSoloCombatDefaultEnemySide(session)); +} + +function setConfiguredBotSide( + session: ClientSession, + botIndex: number, + side: number, +): void { + const index = Math.max(0, Math.min(MAX_SINGLE_PLAYER_BOT_OPPONENTS - 1, botIndex)); + const sides = [...(session.combatBotSides ?? [])]; + sides[index] = clampSoloCombatSide(side, getConfiguredSoloCombatDefaultEnemySide(session)); + session.combatBotSides = sides; + session.combatBotOpponentCount = Math.max(getConfiguredBotOpponentCount(session), index + 1); +} + +function getConfiguredBotSideSummaryLabel( + session: ClientSession, + botIndex: number, + verificationMode?: ClientSession['combatVerificationMode'], +): string { + const botSide = getConfiguredBotSide(session, botIndex, verificationMode); + return botSide === getConfiguredSoloCombatPlayerSide(session) + ? `${getArenaSideLabel(botSide)} (player team)` + : getArenaSideLabel(botSide); +} + +function hasConfiguredSoloCombatHostileBots( + session: ClientSession, + verificationMode?: ClientSession['combatVerificationMode'], +): boolean { + const playerSide = getConfiguredSoloCombatPlayerSide(session); + const botCount = getConfiguredSoloCombatBotCount(session, verificationMode); + for (let index = 0; index < botCount; index += 1) { + if (getConfiguredBotSide(session, index, verificationMode) !== playerSide) { + return true; + } + } + return false; +} + +function buildBotConfigurationSummary(session: ClientSession): string { + const count = getConfiguredBotOpponentCount(session); + const difficultyLevel = getConfiguredBotDifficultyLevel(session); + const playerSide = getConfiguredSoloCombatPlayerSide(session); + const loadouts = Array.from({ length: count }, (_, index) => + `#${index + 1} ${getConfiguredBotSideSummaryLabel(session, index)} ${getConfiguredBotLoadoutLabel(session, index)}`, + ); + return `Bot roster: ${count}. Player side: ${getArenaSideLabel(playerSide)}. Difficulty: ${difficultyLevel}/5. Slots: ${loadouts.join('; ')}`; +} + +function getConfiguredSoloCombatBotCount( + session: ClientSession, + verificationMode: ClientSession['combatVerificationMode'], +): number { + return verificationMode ? 1 : getConfiguredBotOpponentCount(session); +} + +function getSoloCombatBotSlotLabel( + slot: number, + side: number, + playerSide: number, +): string { + const prefix = side === playerSide ? 'Ally' : 'Opponent'; + return slot === 1 ? prefix : `${prefix} ${slot}`; +} + +function buildSoloCombatBotClusterSpawnPoints( + indexes: number[], + centerAngle: number, + radiusUnits: number, +): Array<{ index: number; x: number; y: number }> { + if (indexes.length === 0) { + return []; + } + const baseX = Math.cos(centerAngle) * radiusUnits; + const baseY = Math.sin(centerAngle) * radiusUnits; + const tangentX = -Math.sin(centerAngle); + const tangentY = Math.cos(centerAngle); + return buildCombatTeamClusterPositions(indexes, baseX, baseY, tangentX, tangentY).map(position => ({ + index: position.item, + x: position.x, + y: position.y, + })); +} + +function getSoloCombatSpawnLayout( + botSides: number[], + playerSide: number, +): { playerSpawn: { x: number; y: number }; botSpawnPoints: Array<{ x: number; y: number }> } { + const allyIndexes: number[] = []; + const hostileIndexes: number[] = []; + for (let index = 0; index < botSides.length; index += 1) { + if (botSides[index] === playerSide) { + allyIndexes.push(index); + } else { + hostileIndexes.push(index); + } + } + + const spawnPoints = Array.from({ length: botSides.length }, () => ({ x: 0, y: BOT_AI_SPAWN_DISTANCE })); + let playerSpawn = { x: 0, y: 0 }; + const allyFormation = [...allyIndexes]; + allyFormation.splice(Math.floor(allyIndexes.length / 2), 0, -1); + for (const position of buildCombatTeamClusterPositions(allyFormation, 0, 0, 1, 0)) { + if (position.item === -1) { + playerSpawn = { x: position.x, y: position.y }; + continue; + } + spawnPoints[position.item] = { x: position.x, y: position.y }; + } + for (const point of buildSoloCombatBotClusterSpawnPoints(hostileIndexes, Math.PI / 2, BOT_AI_SPAWN_DISTANCE)) { + spawnPoints[point.index] = { x: point.x, y: point.y }; + } + return { playerSpawn, botSpawnPoints: spawnPoints }; +} + +function getSoloCombatBots(session: ClientSession): SoloCombatBotActorState[] { + return session.combatBotActors ?? []; +} + +function getActiveSoloCombatBots(session: ClientSession): SoloCombatBotActorState[] { + return getSoloCombatBots(session).filter(bot => bot.health > 0); +} + +function getActiveHostileSoloCombatBots(session: ClientSession): SoloCombatBotActorState[] { + const playerSide = getConfiguredSoloCombatPlayerSide(session); + return getActiveSoloCombatBots(session).filter(bot => bot.side !== playerSide); +} + +function getSoloCombatBotBySlot( + session: ClientSession, + slot: number, +): SoloCombatBotActorState | undefined { + return getSoloCombatBots(session).find(bot => bot.slot === slot); +} + +interface SoloCombatBotTargetState { + kind: 'player' | 'bot'; + slot: number; + side: number; + mechId: number; + x: number; + y: number; + z: number; + facing: number; + speedMag: number; + maxSpeedMag?: number; + airborne: boolean; + moveVectorX: number; + moveVectorY: number; + health: number; + internalValues: readonly number[]; + bot?: SoloCombatBotActorState; +} + +function getPreferredSoloCombatTarget( + session: ClientSession, + attacker: SoloCombatBotActorState, +): SoloCombatBotTargetState | undefined { + const candidates: SoloCombatBotTargetState[] = []; + const playerSide = getConfiguredSoloCombatPlayerSide(session); + const playerInternalValues = session.combatPlayerInternalValues ?? DEFAULT_BOT_INTERNAL_VALUES; + const playerHealth = session.playerHealth ?? getCombatDurability( + session.combatPlayerArmorValues ?? DEFAULT_BOT_ARMOR_VALUES, + playerInternalValues, + ) + (session.combatPlayerHeadArmor ?? HEAD_ARMOR_VALUE); + if (playerSide !== attacker.side && playerHealth > 0 && !isActorDestroyed(playerInternalValues)) { + candidates.push({ + kind: 'player', + slot: 0, + side: playerSide, + mechId: session.selectedMechId ?? FALLBACK_MECH_ID, + x: session.combatX ?? 0, + y: session.combatY ?? 0, + z: session.combatJumpAltitude ?? 0, + facing: getCombatCmd65Facing(session), + speedMag: session.combatSpeedMag ?? 0, + maxSpeedMag: session.combatMaxSpeedMag, + airborne: (session.combatJumpAltitude ?? 0) > 0, + moveVectorX: session.combatMoveVectorX ?? 0, + moveVectorY: session.combatMoveVectorY ?? 0, + health: playerHealth, + internalValues: playerInternalValues, + }); + } + + for (const bot of getActiveSoloCombatBots(session)) { + if (bot.slot === attacker.slot || bot.side === attacker.side || bot.health <= 0) { + continue; + } + candidates.push({ + kind: 'bot', + slot: bot.slot, + side: bot.side, + mechId: bot.mechId, + x: bot.x, + y: bot.y, + z: bot.z, + facing: bot.facing, + speedMag: bot.speedMag, + maxSpeedMag: getMechEntryForId(bot.mechId)?.maxSpeedMag, + airborne: bot.jumpActive === true || bot.z > 0, + moveVectorX: bot.moveVectorX, + moveVectorY: bot.moveVectorY, + health: bot.health, + internalValues: bot.internalValues, + bot, + }); + } + + if (candidates.length === 0) { + return undefined; + } + + candidates.sort((a, b) => { + const distanceDiff = Math.hypot(attacker.x - a.x, attacker.y - a.y) - Math.hypot(attacker.x - b.x, attacker.y - b.y); + if (Math.abs(distanceDiff) > 0.5) { + return distanceDiff; + } + const healthDiff = a.health - b.health; + if (healthDiff !== 0) { + return healthDiff; + } + return a.slot - b.slot; + }); + return candidates[0]; +} + +function clearLegacySoloCombatBotState(session: ClientSession): void { + session.botHealth = undefined; + session.combatBotArmorValues = undefined; + session.combatBotInternalValues = undefined; + session.combatBotHeadArmor = undefined; + session.combatBotCriticalStateBytes = undefined; + session.combatBotX = undefined; + session.combatBotY = undefined; + session.combatBotZ = undefined; + session.combatBotFacing = undefined; + session.combatBotSpeedMag = undefined; + session.combatBotLastMoveAt = undefined; + session.combatBotLastAimLimitLogAt = undefined; + session.combatBotMoveVectorX = undefined; + session.combatBotMoveVectorY = undefined; + session.combatBotWeaponReadyAtBySlot = undefined; + session.combatBotAmmoStateValues = undefined; + session.combatBotHeat = undefined; + session.combatBotJumpActive = undefined; + session.combatBotJumpFuel = undefined; + session.combatBotJumpStartedAt = undefined; + session.combatBotJumpDurationMs = undefined; + session.combatBotJumpStartFuel = undefined; + session.combatBotJumpApexUnits = undefined; + session.combatBotJumpStartX = undefined; + session.combatBotJumpStartY = undefined; + session.combatBotJumpTargetX = undefined; + session.combatBotJumpTargetY = undefined; + session.combatBotLastJumpAt = undefined; +} + +function syncLegacySoloCombatBotState(session: ClientSession): void { + const bots = getSoloCombatBots(session); + if (bots.length === 0) { + clearLegacySoloCombatBotState(session); + return; + } + + const primaryBot = bots[0]; + session.botHealth = bots.reduce((sum, bot) => sum + Math.max(0, bot.health), 0); + session.combatBotArmorValues = [...primaryBot.armorValues]; + session.combatBotInternalValues = [...primaryBot.internalValues]; + session.combatBotHeadArmor = primaryBot.headArmor; + session.combatBotCriticalStateBytes = [...primaryBot.criticalStateBytes]; + session.combatBotX = primaryBot.x; + session.combatBotY = primaryBot.y; + session.combatBotZ = primaryBot.z; + session.combatBotFacing = primaryBot.facing; + session.combatBotSpeedMag = primaryBot.speedMag; + session.combatBotLastMoveAt = primaryBot.lastMoveAt; + session.combatBotLastAimLimitLogAt = primaryBot.lastAimLimitLogAt; + session.combatBotMoveVectorX = primaryBot.moveVectorX; + session.combatBotMoveVectorY = primaryBot.moveVectorY; + session.combatBotWeaponReadyAtBySlot = [...primaryBot.weaponReadyAtBySlot]; + session.combatBotAmmoStateValues = [...primaryBot.ammoStateValues]; + session.combatBotHeat = primaryBot.heat; + session.combatBotJumpActive = primaryBot.jumpActive; + session.combatBotJumpFuel = primaryBot.jumpFuel; + session.combatBotJumpStartedAt = primaryBot.jumpStartedAt; + session.combatBotJumpDurationMs = primaryBot.jumpDurationMs; + session.combatBotJumpStartFuel = primaryBot.jumpStartFuel; + session.combatBotJumpApexUnits = primaryBot.jumpApexUnits; + session.combatBotJumpStartX = primaryBot.jumpStartX; + session.combatBotJumpStartY = primaryBot.jumpStartY; + session.combatBotJumpTargetX = primaryBot.jumpTargetX; + session.combatBotJumpTargetY = primaryBot.jumpTargetY; + session.combatBotLastJumpAt = primaryBot.lastJumpAt; +} + +function clearSoloCombatBotDeathTimers(session: ClientSession): void { + for (const bot of getSoloCombatBots(session)) { + if (bot.deathTimer !== undefined) { + clearTimeout(bot.deathTimer); + bot.deathTimer = undefined; + } + } +} + +function createSoloCombatBotActor( + session: ClientSession, + slot: number, + mechId: number, + side: number, + spawnX: number, + spawnY: number, +): SoloCombatBotActorState { + const mechEntry = WORLD_MECH_BY_ID.get(mechId); + const armorValues = [...(mechEntry?.armorLikeMaxValues ?? DEFAULT_BOT_ARMOR_VALUES)]; + const internalValues = mechEntry !== undefined + ? mechInternalStateBytes(mechEntry.tonnage) + : [...DEFAULT_BOT_INTERNAL_VALUES]; + const headArmor = HEAD_ARMOR_VALUE; + const playerX = session.combatX ?? 0; + const playerY = session.combatY ?? 0; + const playerSide = getConfiguredSoloCombatPlayerSide(session); + const initialFacingTargetY = side === playerSide + ? playerY + BOT_AI_SPAWN_DISTANCE + : playerY; + return { + slot, + mechId, + side, + health: getCombatDurability(armorValues, internalValues) + headArmor, + armorValues, + internalValues, + criticalStateBytes: createCriticalStateBytes(mechEntry?.extraCritCount), + headArmor, + x: spawnX, + y: spawnY, + z: 0, + facing: getBotFacingAccumulatorTowardTarget( + spawnX, + spawnY, + playerX, + initialFacingTargetY, + FACING_ACCUMULATOR_NEUTRAL, + ), + speedMag: 0, + moveVectorX: 0, + moveVectorY: 0, + weaponReadyAtBySlot: [], + ammoStateValues: getInitialCombatAmmoStateValues(mechEntry?.ammoBinTypeIds), + heat: 0, + jumpActive: false, + jumpFuel: JUMP_JET_FUEL_MAX, + }; +} + function sendBotPositionSync( session: ClientSession, + bot: SoloCombatBotActorState, capture: CaptureLogger, label: string, ): void { if (session.socket.destroyed || !session.socket.writable || session.phase !== 'combat') { return; } - const { throttle, legVel } = getBotCmd65UpperBodyChannels(session); + const { throttle, legVel } = getBotCmd65UpperBodyChannels(session, bot); send( session.socket, buildCmd65PositionSyncPacket( { - slot: 1, - x: session.combatBotX ?? 0, - y: session.combatBotY ?? BOT_AI_SPAWN_DISTANCE, - z: session.combatBotZ ?? 0, - facing: session.combatBotFacing ?? FACING_ACCUMULATOR_NEUTRAL, + slot: bot.slot, + x: bot.x, + y: bot.y, + z: bot.z, + facing: bot.facing, throttle, legVel, - speedMag: session.combatBotSpeedMag ?? 0, + speedMag: bot.speedMag, }, nextSeq(session), ), @@ -4198,14 +4673,17 @@ function getBotPitchToTarget( ); } -function getBotCmd65UpperBodyChannels(session: ClientSession): { throttle: number; legVel: number } { - const botX = session.combatBotX ?? 0; - const botY = session.combatBotY ?? BOT_AI_SPAWN_DISTANCE; - const botZ = session.combatBotZ ?? 0; +function getBotCmd65UpperBodyChannels( + session: ClientSession, + bot: SoloCombatBotActorState, +): { throttle: number; legVel: number } { + const botX = bot.x; + const botY = bot.y; + const botZ = bot.z; const targetX = session.combatX ?? 0; const targetY = session.combatY ?? 0; const targetZ = session.combatJumpAltitude ?? 0; - const botFacing = session.combatBotFacing ?? FACING_ACCUMULATOR_NEUTRAL; + const botFacing = bot.facing; const torsoYaw = clampNumber( getBotAimDeltaToTarget( botX, @@ -4224,7 +4702,7 @@ function getBotCmd65UpperBodyChannels(session: ClientSession): { throttle: numbe } function maybeLogBotAimLimit( - session: ClientSession, + bot: SoloCombatBotActorState, connLog: Logger, botX: number, botY: number, @@ -4237,14 +4715,15 @@ function maybeLogBotAimLimit( ): void { const now = Date.now(); if ( - session.combatBotLastAimLimitLogAt !== undefined - && now - session.combatBotLastAimLimitLogAt < BOT_AI_AIM_LIMIT_LOG_COOLDOWN_MS + bot.lastAimLimitLogAt !== undefined + && now - bot.lastAimLimitLogAt < BOT_AI_AIM_LIMIT_LOG_COOLDOWN_MS ) { return; } - session.combatBotLastAimLimitLogAt = now; + bot.lastAimLimitLogAt = now; connLog.debug( - '[world/combat] bot fire gated by aim: yaw=%d pitch=%d limit=%d bot=(%d,%d,%d) target=(%d,%d,%d)', + '[world/combat] bot fire gated by aim: slot=%d yaw=%d pitch=%d limit=%d bot=(%d,%d,%d) target=(%d,%d,%d)', + bot.slot, rawYaw, rawPitch, BOT_TORSO_AIM_LIMIT_UNITS, @@ -4355,17 +4834,17 @@ function getWeaponRangeProfileForMech( }; } -function isBotWeaponUsableForRangePlanning(session: ClientSession, weaponSlot: number): boolean { - return peekBotWeaponAmmo(session, weaponSlot).allowed; +function isBotWeaponUsableForRangePlanning(bot: SoloCombatBotActorState, weaponSlot: number): boolean { + return peekBotWeaponAmmo(bot, weaponSlot).allowed; } -function getBotWeaponRangeProfile(session: ClientSession): { +function getBotWeaponRangeProfile(bot: SoloCombatBotActorState): { shortestRangeMeters: number; longestRangeMeters: number; preferredRangeMeters: number; hasUsableWeapon: boolean; } { - const botMechId = getBotMechId(session); + const botMechId = bot.mechId; const mechEntry = getMechEntryForId(botMechId); if (!mechEntry) { return { @@ -4380,8 +4859,8 @@ function getBotWeaponRangeProfile(session: ClientSession): { let longestRangeMeters = 0; let hasUsableWeapon = false; for (let weaponSlot = 0; weaponSlot < mechEntry.weaponTypeIds.length; weaponSlot += 1) { - const mountGate = getWeaponMountGateForMech(botMechId, session.combatBotInternalValues, weaponSlot); - if (!mountGate.allowed || !isBotWeaponUsableForRangePlanning(session, weaponSlot)) continue; + const mountGate = getWeaponMountGateForMech(botMechId, bot.internalValues, weaponSlot); + if (!mountGate.allowed || !isBotWeaponUsableForRangePlanning(bot, weaponSlot)) continue; const maxRangeMeters = getWeaponLongRangeMeters(getWeaponSpecForMechSlot(botMechId, weaponSlot)) ?? 0; if (maxRangeMeters <= 0) continue; hasUsableWeapon = true; @@ -4410,8 +4889,8 @@ function getBotWeaponRangeProfile(session: ClientSession): { }; } -function getBotPreferredRangeMeters(session: ClientSession): number { - return getBotWeaponRangeProfile(session).preferredRangeMeters; +function getBotPreferredRangeMeters(bot: SoloCombatBotActorState): number { + return getBotWeaponRangeProfile(bot).preferredRangeMeters; } function getWeaponFitScoreForMechAtDistance( @@ -4484,18 +4963,19 @@ function chooseWeakestSurvivingSection( } function chooseBotAttackSection( - session: ClientSession, + bot: SoloCombatBotActorState, weaponSlot: number, armorValues: readonly number[], internalValues: readonly number[], headArmor: number, distanceMeters: number, + targetHealth: number, ): CombatAttachmentHitSection { - const botMechId = getBotMechId(session); + const botMechId = bot.mechId; const weaponSpec = getWeaponSpecForMechSlot(botMechId, weaponSlot); const weaponDamage = weaponSpec?.damage ?? BOT_FALLBACK_WEAPON_DAMAGE; - const weaponRangeMeters = getWeaponLongRangeMeters(weaponSpec) ?? getBotPreferredRangeMeters(session); - const botBehindOnDurability = (session.botHealth ?? BOT_INITIAL_HEALTH) < (session.playerHealth ?? BOT_INITIAL_HEALTH); + const weaponRangeMeters = getWeaponLongRangeMeters(weaponSpec) ?? getBotPreferredRangeMeters(bot); + const botBehindOnDurability = bot.health < targetHealth; const headRemaining = getSectionRemainingDurability( armorValues, @@ -4546,7 +5026,7 @@ function chooseBotAttackSection( if ( weakestLegDurability <= weaponDamage * 3 || botBehindOnDurability - || session.combatBotJumpActive + || bot.jumpActive ) { return weakestLeg; } @@ -4572,34 +5052,35 @@ function chooseBotAttackSection( return weakestArm; } - return chooseRetaliationHitSection(session, armorValues, internalValues, headArmor); + return chooseRetaliationHitSection(undefined, armorValues, internalValues, headArmor); } -function getBotJumpTravelUnits(session: ClientSession): number { - const { apexUnits } = getJumpArcForMechId(getBotMechId(session)); +function getBotJumpTravelUnits(bot: SoloCombatBotActorState): number { + const { apexUnits } = getJumpArcForMechId(bot.mechId); return Math.max(24 * COMBAT_WORLD_UNITS_PER_METER, apexUnits * 2); } function startBotJump( session: ClientSession, + bot: SoloCombatBotActorState, connLog: Logger, capture: CaptureLogger, landingX: number, landingY: number, reason: string, ): boolean { - const botMechId = getBotMechId(session); + const botMechId = bot.mechId; const now = Date.now(); - const fuel = session.combatBotJumpFuel ?? JUMP_JET_FUEL_MAX; - const currentBotX = session.combatBotX ?? 0; - const currentBotY = session.combatBotY ?? BOT_SPAWN_DISTANCE; + const fuel = bot.jumpFuel; + const currentBotX = bot.x; + const currentBotY = bot.y; if (!mechSupportsJumpJets(botMechId)) return false; - if (session.combatBotJumpActive) return false; + if (bot.jumpActive) return false; if (fuel <= JUMP_JET_START_FUEL_THRESHOLD) return false; if ( - session.combatBotLastJumpAt !== undefined - && now - session.combatBotLastJumpAt < BOT_AI_JUMP_COOLDOWN_MS + bot.lastJumpAt !== undefined + && now - bot.lastJumpAt < BOT_AI_JUMP_COOLDOWN_MS ) { return false; } @@ -4608,29 +5089,33 @@ function startBotJump( } const jumpArc = getJumpArcForMechId(botMechId); - session.combatBotJumpActive = true; - session.combatBotJumpStartedAt = now; - session.combatBotJumpDurationMs = getJumpMirrorDurationMsForMechId(botMechId); - session.combatBotJumpStartFuel = fuel; - session.combatBotJumpFuel = fuel; - session.combatBotJumpApexUnits = jumpArc.apexUnits; - session.combatBotJumpStartX = currentBotX; - session.combatBotJumpStartY = currentBotY; - session.combatBotJumpTargetX = landingX; - session.combatBotJumpTargetY = landingY; - session.combatBotLastJumpAt = now; - session.combatBotZ = 0; - session.combatBotSpeedMag = 0; - setBotMoveVector(session, 0, 0); - session.combatBotFacing = getBotFacingAccumulatorTowardTarget( - currentBotX, - currentBotY, - session.combatX ?? 0, - session.combatY ?? 0, - session.combatBotFacing ?? FACING_ACCUMULATOR_NEUTRAL, - ); + bot.jumpActive = true; + bot.jumpStartedAt = now; + bot.jumpDurationMs = getJumpMirrorDurationMsForMechId(botMechId); + bot.jumpStartFuel = fuel; + bot.jumpFuel = fuel; + bot.jumpApexUnits = jumpArc.apexUnits; + bot.jumpStartX = currentBotX; + bot.jumpStartY = currentBotY; + bot.jumpTargetX = landingX; + bot.jumpTargetY = landingY; + bot.lastJumpAt = now; + bot.z = 0; + bot.speedMag = 0; + setBotMoveVector(bot, 0, 0); + const combatTarget = getPreferredSoloCombatTarget(session, bot); + if (combatTarget) { + bot.facing = getBotFacingAccumulatorTowardTarget( + currentBotX, + currentBotY, + combatTarget.x, + combatTarget.y, + bot.facing, + ); + } connLog.info( - '[world/combat] bot jump start: mechId=%d fuel=%d from=(%d,%d) to=(%d,%d) apex=%d reason=%s', + '[world/combat] bot jump start: slot=%d mechId=%d fuel=%d from=(%d,%d) to=(%d,%d) apex=%d reason=%s', + bot.slot, botMechId, fuel, currentBotX, @@ -4640,73 +5125,78 @@ function startBotJump( jumpArc.apexUnits, reason, ); - sendBotPositionSync(session, capture, 'CMD65_BOT_JUMP_START'); + sendBotPositionSync(session, bot, capture, 'CMD65_BOT_JUMP_START'); return true; } function advanceBotJump( session: ClientSession, + bot: SoloCombatBotActorState, connLog: Logger, capture: CaptureLogger, ): boolean { - if (!session.combatBotJumpActive) { + if (!bot.jumpActive) { return false; } - const startedAt = session.combatBotJumpStartedAt ?? Date.now(); - const durationMs = Math.max(1, session.combatBotJumpDurationMs ?? getJumpMirrorDurationMsForMechId(getBotMechId(session))); - const startX = session.combatBotJumpStartX ?? (session.combatBotX ?? 0); - const startY = session.combatBotJumpStartY ?? (session.combatBotY ?? BOT_AI_SPAWN_DISTANCE); - const targetX = session.combatBotJumpTargetX ?? startX; - const targetY = session.combatBotJumpTargetY ?? startY; - const apexUnits = session.combatBotJumpApexUnits ?? getJumpArcForMechId(getBotMechId(session)).apexUnits; - const startedFuel = session.combatBotJumpStartFuel ?? (session.combatBotJumpFuel ?? JUMP_JET_FUEL_MAX); + const startedAt = bot.jumpStartedAt ?? Date.now(); + const durationMs = Math.max(1, bot.jumpDurationMs ?? getJumpMirrorDurationMsForMechId(bot.mechId)); + const startX = bot.jumpStartX ?? bot.x; + const startY = bot.jumpStartY ?? bot.y; + const targetX = bot.jumpTargetX ?? startX; + const targetY = bot.jumpTargetY ?? startY; + const apexUnits = bot.jumpApexUnits ?? getJumpArcForMechId(bot.mechId).apexUnits; + const startedFuel = bot.jumpStartFuel ?? bot.jumpFuel; const progress = Math.min(1, (Date.now() - startedAt) / durationMs); - const previousBotX = session.combatBotX ?? startX; - const previousBotY = session.combatBotY ?? startY; + const previousBotX = bot.x; + const previousBotY = bot.y; - session.combatBotX = Math.round(startX + ((targetX - startX) * progress)); - session.combatBotY = Math.round(startY + ((targetY - startY) * progress)); + bot.x = Math.round(startX + ((targetX - startX) * progress)); + bot.y = Math.round(startY + ((targetY - startY) * progress)); setBotMoveVector( - session, - (session.combatBotX ?? startX) - previousBotX, - (session.combatBotY ?? startY) - previousBotY, + bot, + bot.x - previousBotX, + bot.y - previousBotY, ); - session.combatBotZ = progress >= 1 + bot.z = progress >= 1 ? 0 : Math.max(COMBAT_WORLD_UNITS_PER_METER, Math.round(apexUnits * 4 * progress * (1 - progress))); - session.combatBotSpeedMag = 0; - session.combatBotFacing = getBotFacingAccumulatorTowardTarget( - session.combatBotX ?? startX, - session.combatBotY ?? startY, - session.combatX ?? 0, - session.combatY ?? 0, - session.combatBotFacing ?? FACING_ACCUMULATOR_NEUTRAL, - ); - session.combatBotJumpFuel = Math.max(0, Math.round(startedFuel * (1 - progress))); + bot.speedMag = 0; + const combatTarget = getPreferredSoloCombatTarget(session, bot); + if (combatTarget) { + bot.facing = getBotFacingAccumulatorTowardTarget( + bot.x, + bot.y, + combatTarget.x, + combatTarget.y, + bot.facing, + ); + } + bot.jumpFuel = Math.max(0, Math.round(startedFuel * (1 - progress))); if (progress >= 1) { - session.combatBotJumpActive = false; - session.combatBotJumpStartedAt = undefined; - session.combatBotJumpDurationMs = undefined; - session.combatBotJumpStartFuel = undefined; - session.combatBotJumpApexUnits = undefined; - session.combatBotJumpStartX = undefined; - session.combatBotJumpStartY = undefined; - session.combatBotJumpTargetX = undefined; - session.combatBotJumpTargetY = undefined; - sendBotPositionSync(session, capture, 'CMD65_BOT_JUMP_LAND'); + bot.jumpActive = false; + bot.jumpStartedAt = undefined; + bot.jumpDurationMs = undefined; + bot.jumpStartFuel = undefined; + bot.jumpApexUnits = undefined; + bot.jumpStartX = undefined; + bot.jumpStartY = undefined; + bot.jumpTargetX = undefined; + bot.jumpTargetY = undefined; + sendBotPositionSync(session, bot, capture, 'CMD65_BOT_JUMP_LAND'); connLog.info( - '[world/combat] bot jump land: mechId=%d fuel=%d at=(%d,%d)', - getBotMechId(session), - session.combatBotJumpFuel ?? 0, - session.combatBotX ?? 0, - session.combatBotY ?? 0, + '[world/combat] bot jump land: slot=%d mechId=%d fuel=%d at=(%d,%d)', + bot.slot, + bot.mechId, + bot.jumpFuel, + bot.x, + bot.y, ); return true; } - sendBotPositionSync(session, capture, 'CMD65_BOT_JUMP_MIRROR'); + sendBotPositionSync(session, bot, capture, 'CMD65_BOT_JUMP_MIRROR'); return true; } @@ -4715,259 +5205,271 @@ function stepBotMovement( connLog: Logger, capture: CaptureLogger, ): void { - const now = Date.now(); - const elapsedMs = clampNumber( - now - (session.combatBotLastMoveAt ?? (now - BOT_AI_TICK_MS)), - 1, - BOT_AI_TICK_MS * 4, - ); - session.combatBotLastMoveAt = now; - const botMechEntry = getMechEntryForId(getBotMechId(session)); - coolBotHeat(session, Math.max(1, botMechEntry?.heatSinks ?? 10)); - const fuelRegenAmount = Math.max( - 1, - Math.round(JUMP_JET_FUEL_REGEN_PER_TICK * elapsedMs / JUMP_JET_FUEL_REGEN_INTERVAL_MS), - ); - regenBotJumpFuelIfGrounded(session, fuelRegenAmount); - if (advanceBotJump(session, connLog, capture)) { + const activeBots = getActiveSoloCombatBots(session); + if (activeBots.length === 0) { + syncLegacySoloCombatBotState(session); return; } - const playerX = session.combatX ?? 0; - const playerY = session.combatY ?? 0; - const currentBotX = session.combatBotX ?? 0; - const currentBotY = session.combatBotY ?? BOT_AI_SPAWN_DISTANCE; - const dx = playerX - currentBotX; - const dy = playerY - currentBotY; - const distanceUnits = Math.hypot(dx, dy); - const distanceMeters = getCombatDisplayDistanceMeters(currentBotX, currentBotY, playerX, playerY); - const rangeProfile = getBotWeaponRangeProfile(session); - const currentRangeFitScore = getWeaponFitScoreForMechAtDistance( - getBotMechId(session), - session.combatBotInternalValues, - distanceMeters, - weaponSlot => isBotWeaponUsableForRangePlanning(session, weaponSlot), - ); - const rangeBufferUnits = BOT_AI_RANGE_BUFFER_METERS * COMBAT_WORLD_UNITS_PER_METER; - const playerRangeProfile = getWeaponRangeProfileForMech( - session.selectedMechId ?? FALLBACK_MECH_ID, - session.combatPlayerInternalValues, - ); - const botHealth = session.botHealth ?? BOT_INITIAL_HEALTH; - const playerHealth = session.playerHealth ?? BOT_INITIAL_HEALTH; - const durabilityDelta = botHealth - playerHealth; - const playerThreatRangeUnits = (playerRangeProfile.hasUsableWeapon ? playerRangeProfile.longestRangeMeters : 0) - * COMBAT_WORLD_UNITS_PER_METER; - const maxSpeedMag = botMechEntry?.maxSpeedMag ?? 0; - const walkSpeedMag = botMechEntry?.walkSpeedMag ?? maxSpeedMag; - const botBehindOnDurability = durabilityDelta < -8; - const botAheadOnDurability = durabilityDelta > 12; - let tacticalPreferredRangeMeters = rangeProfile.preferredRangeMeters; - if (playerRangeProfile.hasUsableWeapon) { - if (rangeProfile.longestRangeMeters >= playerRangeProfile.longestRangeMeters + 120) { + for (const bot of activeBots) { + const difficultyProfile = getConfiguredBotDifficultyProfile(session); + const now = Date.now(); + const elapsedMs = clampNumber( + now - (bot.lastMoveAt ?? (now - BOT_AI_TICK_MS)), + 1, + BOT_AI_TICK_MS * 4, + ); + bot.lastMoveAt = now; + const botMechEntry = getMechEntryForId(bot.mechId); + coolBotHeat(bot, Math.max(1, botMechEntry?.heatSinks ?? 10)); + const fuelRegenAmount = Math.max( + 1, + Math.round(JUMP_JET_FUEL_REGEN_PER_TICK * elapsedMs / JUMP_JET_FUEL_REGEN_INTERVAL_MS), + ); + regenBotJumpFuelIfGrounded(bot, fuelRegenAmount); + if (advanceBotJump(session, bot, connLog, capture)) { + continue; + } + + const target = getPreferredSoloCombatTarget(session, bot); + if (!target) { + bot.speedMag = stepBotGroundedSpeedMagTowardTarget(bot.speedMag, 0, Math.max(1, Math.abs(bot.speedMag)), elapsedMs); + setBotMoveVector(bot, 0, 0); + sendBotPositionSync(session, bot, capture, 'CMD65_BOT_AI_IDLE'); + continue; + } + + const targetX = target.x; + const targetY = target.y; + const currentBotX = bot.x; + const currentBotY = bot.y; + const dx = targetX - currentBotX; + const dy = targetY - currentBotY; + const distanceUnits = Math.hypot(dx, dy); + const distanceMeters = getCombatDisplayDistanceMeters(currentBotX, currentBotY, targetX, targetY); + const rangeProfile = getBotWeaponRangeProfile(bot); + const currentRangeFitScore = getWeaponFitScoreForMechAtDistance( + bot.mechId, + bot.internalValues, + distanceMeters, + weaponSlot => isBotWeaponUsableForRangePlanning(bot, weaponSlot), + ); + const rangeBufferUnits = BOT_AI_RANGE_BUFFER_METERS * COMBAT_WORLD_UNITS_PER_METER; + const targetRangeProfile = getWeaponRangeProfileForMech( + target.mechId, + target.internalValues, + ); + const botHealth = bot.health; + const targetHealth = target.health; + const durabilityDelta = botHealth - targetHealth; + const targetThreatRangeUnits = (targetRangeProfile.hasUsableWeapon ? targetRangeProfile.longestRangeMeters : 0) + * COMBAT_WORLD_UNITS_PER_METER; + const maxSpeedMag = botMechEntry?.maxSpeedMag ?? 0; + const walkSpeedMag = botMechEntry?.walkSpeedMag ?? maxSpeedMag; + const botBehindOnDurability = durabilityDelta < -8; + const botAheadOnDurability = durabilityDelta > 12; + let tacticalPreferredRangeMeters = rangeProfile.preferredRangeMeters; + if (targetRangeProfile.hasUsableWeapon) { + if (rangeProfile.longestRangeMeters >= targetRangeProfile.longestRangeMeters + 120) { + tacticalPreferredRangeMeters += BOT_AI_RANGE_ADVANTAGE_BONUS_METERS; + } else if ( + rangeProfile.shortestRangeMeters <= 270 + && rangeProfile.longestRangeMeters + 60 < targetRangeProfile.longestRangeMeters + ) { + tacticalPreferredRangeMeters -= difficultyProfile.rangePressureBonusMeters; + } + } + if (targetHealth <= difficultyProfile.finisherPushHealthThreshold && rangeProfile.shortestRangeMeters <= 270) { + tacticalPreferredRangeMeters -= difficultyProfile.rangePressureBonusMeters; + } else if (botBehindOnDurability && rangeProfile.longestRangeMeters >= 450) { tacticalPreferredRangeMeters += BOT_AI_RANGE_ADVANTAGE_BONUS_METERS; - } else if ( - rangeProfile.shortestRangeMeters <= 270 - && rangeProfile.longestRangeMeters + 60 < playerRangeProfile.longestRangeMeters - ) { - tacticalPreferredRangeMeters -= BOT_AI_RANGE_PRESSURE_BONUS_METERS; + } else if (botAheadOnDurability && rangeProfile.shortestRangeMeters <= 270) { + tacticalPreferredRangeMeters -= difficultyProfile.rangePressureBonusMeters; } - } - if (playerHealth <= BOT_AI_FINISHER_PUSH_HEALTH_THRESHOLD && rangeProfile.shortestRangeMeters <= 270) { - tacticalPreferredRangeMeters -= BOT_AI_RANGE_PRESSURE_BONUS_METERS; - } else if (botBehindOnDurability && rangeProfile.longestRangeMeters >= 450) { - tacticalPreferredRangeMeters += BOT_AI_RANGE_ADVANTAGE_BONUS_METERS; - } else if (botAheadOnDurability && rangeProfile.shortestRangeMeters <= 270) { - tacticalPreferredRangeMeters -= BOT_AI_RANGE_PRESSURE_BONUS_METERS; - } - tacticalPreferredRangeMeters = clampNumber( - tacticalPreferredRangeMeters, - BOT_AI_MIN_PREFERRED_RANGE_METERS, - BOT_AI_MAX_PREFERRED_RANGE_METERS, - ); - const preferredRangeUnits = tacticalPreferredRangeMeters * COMBAT_WORLD_UNITS_PER_METER; - const minimumRetreatUnits = Math.max( - BOT_AI_MIN_PREFERRED_RANGE_METERS * COMBAT_WORLD_UNITS_PER_METER, - preferredRangeUnits - (rangeBufferUnits * 2), - ); - const jumpTravelUnits = getBotJumpTravelUnits(session); - const canConsiderJump = mechSupportsJumpJets(getBotMechId(session)) && distanceUnits > 0; - const insidePlayerThreatRange = playerThreatRangeUnits > 0 - && distanceUnits <= playerThreatRangeUnits + (BOT_AI_PLAYER_THREAT_BUFFER_METERS * COMBAT_WORLD_UNITS_PER_METER); - const currentFacing = session.combatBotFacing - ?? getBotFacingAccumulatorTowardTarget( + tacticalPreferredRangeMeters = clampNumber( + tacticalPreferredRangeMeters, + BOT_AI_MIN_PREFERRED_RANGE_METERS, + BOT_AI_MAX_PREFERRED_RANGE_METERS, + ); + const preferredRangeUnits = tacticalPreferredRangeMeters * COMBAT_WORLD_UNITS_PER_METER; + const minimumRetreatUnits = Math.max( + BOT_AI_MIN_PREFERRED_RANGE_METERS * COMBAT_WORLD_UNITS_PER_METER, + preferredRangeUnits - (rangeBufferUnits * 2), + ); + const jumpTravelUnits = getBotJumpTravelUnits(bot); + const canConsiderJump = mechSupportsJumpJets(bot.mechId) && distanceUnits > 0; + const insideTargetThreatRange = targetThreatRangeUnits > 0 + && distanceUnits <= targetThreatRangeUnits + (BOT_AI_PLAYER_THREAT_BUFFER_METERS * COMBAT_WORLD_UNITS_PER_METER); + const currentFacing = bot.facing; + const desiredFacing = getBotFacingAccumulatorTowardTarget( currentBotX, currentBotY, - playerX, - playerY, - FACING_ACCUMULATOR_NEUTRAL, + targetX, + targetY, + currentFacing, ); - const desiredFacing = getBotFacingAccumulatorTowardTarget( - currentBotX, - currentBotY, - playerX, - playerY, - currentFacing, - ); - if ( - canConsiderJump - && distanceUnits < minimumRetreatUnits - && (botBehindOnDurability || rangeProfile.longestRangeMeters >= 450) - ) { - const retreatUnits = Math.min( - jumpTravelUnits, - Math.max(rangeBufferUnits * 2, Math.round(preferredRangeUnits - distanceUnits + rangeBufferUnits)), - ); - const landingX = currentBotX - Math.round((dx / distanceUnits) * retreatUnits); - const landingY = currentBotY - Math.round((dy / distanceUnits) * retreatUnits); - const landingRangeFitScore = getWeaponFitScoreForMechAtDistance( - getBotMechId(session), - session.combatBotInternalValues, - (distanceUnits + retreatUnits) / COMBAT_WORLD_UNITS_PER_METER, - weaponSlot => isBotWeaponUsableForRangePlanning(session, weaponSlot), - ); if ( - isMeaningfulJumpFitGain(currentRangeFitScore, landingRangeFitScore) - && startBotJump(session, connLog, capture, landingX, landingY, 'defensive range reset') + canConsiderJump + && distanceUnits < minimumRetreatUnits + && (botBehindOnDurability || rangeProfile.longestRangeMeters >= 450) ) { - return; + const retreatUnits = Math.min( + jumpTravelUnits, + Math.max(rangeBufferUnits * 2, Math.round(preferredRangeUnits - distanceUnits + rangeBufferUnits)), + ); + const landingX = currentBotX - Math.round((dx / distanceUnits) * retreatUnits); + const landingY = currentBotY - Math.round((dy / distanceUnits) * retreatUnits); + const landingRangeFitScore = getWeaponFitScoreForMechAtDistance( + bot.mechId, + bot.internalValues, + (distanceUnits + retreatUnits) / COMBAT_WORLD_UNITS_PER_METER, + weaponSlot => isBotWeaponUsableForRangePlanning(bot, weaponSlot), + ); + if ( + isMeaningfulJumpFitGain(currentRangeFitScore, landingRangeFitScore) + && startBotJump(session, bot, connLog, capture, landingX, landingY, 'defensive range reset') + ) { + continue; + } } - } - if ( - canConsiderJump - && distanceUnits > preferredRangeUnits + (rangeBufferUnits * 3) - && rangeProfile.shortestRangeMeters <= 270 - ) { - const advanceUnits = Math.min( - jumpTravelUnits, - Math.max(rangeBufferUnits * 2, Math.round(distanceUnits - preferredRangeUnits)), - ); - const landingX = currentBotX + Math.round((dx / distanceUnits) * advanceUnits); - const landingY = currentBotY + Math.round((dy / distanceUnits) * advanceUnits); - const landingRangeFitScore = getWeaponFitScoreForMechAtDistance( - getBotMechId(session), - session.combatBotInternalValues, - Math.max(0, (distanceUnits - advanceUnits) / COMBAT_WORLD_UNITS_PER_METER), - weaponSlot => isBotWeaponUsableForRangePlanning(session, weaponSlot), - ); if ( - isMeaningfulJumpFitGain(currentRangeFitScore, landingRangeFitScore) - && startBotJump(session, connLog, capture, landingX, landingY, 'aggressive range close') + canConsiderJump + && distanceUnits > preferredRangeUnits + (rangeBufferUnits * 3) + && rangeProfile.shortestRangeMeters <= 270 ) { - return; + const advanceUnits = Math.min( + jumpTravelUnits, + Math.max(rangeBufferUnits * 2, Math.round(distanceUnits - preferredRangeUnits)), + ); + const landingX = currentBotX + Math.round((dx / distanceUnits) * advanceUnits); + const landingY = currentBotY + Math.round((dy / distanceUnits) * advanceUnits); + const landingRangeFitScore = getWeaponFitScoreForMechAtDistance( + bot.mechId, + bot.internalValues, + Math.max(0, (distanceUnits - advanceUnits) / COMBAT_WORLD_UNITS_PER_METER), + weaponSlot => isBotWeaponUsableForRangePlanning(bot, weaponSlot), + ); + if ( + isMeaningfulJumpFitGain(currentRangeFitScore, landingRangeFitScore) + && startBotJump(session, bot, connLog, capture, landingX, landingY, 'aggressive range close') + ) { + continue; + } } - } - let nextBotX = currentBotX; - let nextBotY = currentBotY; - const currentSpeedMag = session.combatBotSpeedMag ?? 0; - let nextSpeedMag = currentSpeedMag; - let moveVectorX = 0; - let moveVectorY = 0; - let desiredMotion: -1 | 0 | 1 = 0; - let speedScale = 0; - - if (distanceUnits > preferredRangeUnits + rangeBufferUnits) { - desiredMotion = 1; - speedScale = 1; - } else if (distanceUnits < minimumRetreatUnits) { - desiredMotion = -1; - speedScale = insidePlayerThreatRange ? 1 : 0.8; - } else if (insidePlayerThreatRange) { - if (botBehindOnDurability || rangeProfile.longestRangeMeters >= playerRangeProfile.longestRangeMeters + 90) { - desiredMotion = -1; - speedScale = botBehindOnDurability ? 1 : 0.85; - } else if ( - playerHealth <= BOT_AI_FINISHER_PUSH_HEALTH_THRESHOLD - && rangeProfile.shortestRangeMeters <= 270 - ) { - desiredMotion = 1; - speedScale = 0.8; - } else if (rangeProfile.shortestRangeMeters <= 270) { + let nextBotX = currentBotX; + let nextBotY = currentBotY; + const currentSpeedMag = bot.speedMag; + let nextSpeedMag = currentSpeedMag; + let moveVectorX = 0; + let moveVectorY = 0; + let desiredMotion: -1 | 0 | 1 = 0; + let speedScale = 0; + + if (distanceUnits > preferredRangeUnits + rangeBufferUnits) { desiredMotion = 1; - speedScale = 0.55; - } else if (!botAheadOnDurability) { + speedScale = 1; + } else if (distanceUnits < minimumRetreatUnits) { desiredMotion = -1; + speedScale = insideTargetThreatRange ? 1 : 0.8; + } else if (insideTargetThreatRange) { + if (botBehindOnDurability || rangeProfile.longestRangeMeters >= targetRangeProfile.longestRangeMeters + 90) { + desiredMotion = -1; + speedScale = botBehindOnDurability ? 1 : 0.85; + } else if ( + targetHealth <= difficultyProfile.finisherPushHealthThreshold + && rangeProfile.shortestRangeMeters <= 270 + ) { + desiredMotion = 1; + speedScale = 0.8; + } else if (rangeProfile.shortestRangeMeters <= 270) { + desiredMotion = 1; + speedScale = 0.55; + } else if (!botAheadOnDurability) { + desiredMotion = -1; + speedScale = 0.45; + } + } else if (distanceUnits > 0 && rangeProfile.shortestRangeMeters <= 270) { + desiredMotion = 1; speedScale = 0.45; } - } else if (distanceUnits > 0 && rangeProfile.shortestRangeMeters <= 270) { - desiredMotion = 1; - speedScale = 0.45; - } - let nextFacing = stepFacingAccumulatorToward( - currentFacing, - desiredFacing, - getBotTurnStepUnits(elapsedMs), - ); - const remainingAimDelta = getBotAimDeltaToTarget(currentBotX, currentBotY, playerX, playerY, nextFacing); - const canAdvanceOnHeading = Math.abs(remainingAimDelta) <= BOT_FORWARD_ALIGNMENT_LIMIT_UNITS; - const canReverseOnHeading = Math.abs(remainingAimDelta) <= BOT_TORSO_AIM_LIMIT_UNITS; - if ( - (desiredMotion > 0 && !canAdvanceOnHeading) - || (desiredMotion < 0 && !canReverseOnHeading) - ) { - desiredMotion = 0; - speedScale = 0; - nextFacing = stepFacingAccumulatorToward(currentFacing, desiredFacing, getBotTurnStepUnits(elapsedMs)); - } - - let targetSpeedMag = 0; - let movementSpeedCap = 0; - let desiredRangeDelta = Number.POSITIVE_INFINITY; - if (maxSpeedMag > 0 && desiredMotion !== 0 && speedScale > 0) { - movementSpeedCap = desiredMotion < 0 - ? Math.max(1, Math.abs(walkSpeedMag)) - : Math.max(1, Math.abs(maxSpeedMag)); - targetSpeedMag = Math.max( - COMBAT_WORLD_UNITS_PER_METER / 10, - Math.round(movementSpeedCap * speedScale), - ) * desiredMotion; - desiredRangeDelta = desiredMotion > 0 - ? distanceUnits > preferredRangeUnits + rangeBufferUnits - ? Math.max(COMBAT_WORLD_UNITS_PER_METER, Math.round(distanceUnits - preferredRangeUnits)) - : Number.POSITIVE_INFINITY - : distanceUnits < minimumRetreatUnits - ? Math.max(COMBAT_WORLD_UNITS_PER_METER, Math.round(preferredRangeUnits - distanceUnits)) - : Number.POSITIVE_INFINITY; - } - - nextSpeedMag = stepBotGroundedSpeedMagTowardTarget( - currentSpeedMag, - targetSpeedMag, - Math.max(Math.abs(targetSpeedMag), Math.abs(currentSpeedMag), movementSpeedCap), - elapsedMs, - ); - const averageSpeedMag = (currentSpeedMag + nextSpeedMag) / 2; - if (nextFacing !== currentFacing || averageSpeedMag !== 0) { - const requestedStepUnits = Math.abs(Math.round( - signedSpeedMagToMetersPerSecond(averageSpeedMag) - * (elapsedMs / 1000) - * COMBAT_WORLD_UNITS_PER_METER, - )); - if (requestedStepUnits > 0) { - const stepUnits = Math.min( - requestedStepUnits, - desiredRangeDelta, - ); - const forwardUnit = getBotForwardUnitVector(nextFacing); - const motionSign = averageSpeedMag < 0 ? -1 : 1; - moveVectorX = Math.round(forwardUnit.x * stepUnits * motionSign); - moveVectorY = Math.round(forwardUnit.y * stepUnits * motionSign); - nextBotX += moveVectorX; - nextBotY += moveVectorY; + let nextFacing = stepFacingAccumulatorToward( + currentFacing, + desiredFacing, + getBotTurnStepUnits(elapsedMs), + ); + const remainingAimDelta = getBotAimDeltaToTarget(currentBotX, currentBotY, targetX, targetY, nextFacing); + const canAdvanceOnHeading = Math.abs(remainingAimDelta) <= BOT_FORWARD_ALIGNMENT_LIMIT_UNITS; + const canReverseOnHeading = Math.abs(remainingAimDelta) <= BOT_TORSO_AIM_LIMIT_UNITS; + if ( + (desiredMotion > 0 && !canAdvanceOnHeading) + || (desiredMotion < 0 && !canReverseOnHeading) + ) { + desiredMotion = 0; + speedScale = 0; + nextFacing = stepFacingAccumulatorToward(currentFacing, desiredFacing, getBotTurnStepUnits(elapsedMs)); + } + + let targetSpeedMag = 0; + let movementSpeedCap = 0; + let desiredRangeDelta = Number.POSITIVE_INFINITY; + if (maxSpeedMag > 0 && desiredMotion !== 0 && speedScale > 0) { + movementSpeedCap = desiredMotion < 0 + ? Math.max(1, Math.abs(walkSpeedMag)) + : Math.max(1, Math.abs(maxSpeedMag)); + targetSpeedMag = Math.max( + COMBAT_WORLD_UNITS_PER_METER / 10, + Math.round(movementSpeedCap * speedScale), + ) * desiredMotion; + desiredRangeDelta = desiredMotion > 0 + ? distanceUnits > preferredRangeUnits + rangeBufferUnits + ? Math.max(COMBAT_WORLD_UNITS_PER_METER, Math.round(distanceUnits - preferredRangeUnits)) + : Number.POSITIVE_INFINITY + : distanceUnits < minimumRetreatUnits + ? Math.max(COMBAT_WORLD_UNITS_PER_METER, Math.round(preferredRangeUnits - distanceUnits)) + : Number.POSITIVE_INFINITY; + } + + nextSpeedMag = stepBotGroundedSpeedMagTowardTarget( + currentSpeedMag, + targetSpeedMag, + Math.max(Math.abs(targetSpeedMag), Math.abs(currentSpeedMag), movementSpeedCap), + elapsedMs, + ); + const averageSpeedMag = (currentSpeedMag + nextSpeedMag) / 2; + if (nextFacing !== currentFacing || averageSpeedMag !== 0) { + const requestedStepUnits = Math.abs(Math.round( + signedSpeedMagToMetersPerSecond(averageSpeedMag) + * (elapsedMs / 1000) + * COMBAT_WORLD_UNITS_PER_METER, + )); + if (requestedStepUnits > 0) { + const stepUnits = Math.min( + requestedStepUnits, + desiredRangeDelta, + ); + const forwardUnit = getBotForwardUnitVector(nextFacing); + const motionSign = averageSpeedMag < 0 ? -1 : 1; + moveVectorX = Math.round(forwardUnit.x * stepUnits * motionSign); + moveVectorY = Math.round(forwardUnit.y * stepUnits * motionSign); + nextBotX += moveVectorX; + nextBotY += moveVectorY; + } } + + bot.x = nextBotX; + bot.y = nextBotY; + setBotMoveVector(bot, moveVectorX, moveVectorY); + bot.z = 0; + bot.facing = nextFacing; + bot.speedMag = nextSpeedMag; + sendBotPositionSync(session, bot, capture, 'CMD65_BOT_AI_POSITION'); } - session.combatBotX = nextBotX; - session.combatBotY = nextBotY; - setBotMoveVector(session, moveVectorX, moveVectorY); - session.combatBotZ = 0; - session.combatBotFacing = nextFacing; - session.combatBotSpeedMag = nextSpeedMag; - sendBotPositionSync(session, capture, 'CMD65_BOT_AI_POSITION'); + syncLegacySoloCombatBotState(session); } function stepBotWeaponFire( @@ -4979,21 +5481,17 @@ function stepBotWeaponFire( if (session.socket.destroyed || !session.socket.writable || session.phase !== 'combat') { return; } - if (session.combatResultCode !== undefined || (session.botHealth ?? 0) <= 0) { + if (session.combatResultCode !== undefined) { return; } - - const botMechId = getBotMechId(session); - const botMechEntry = getMechEntryForId(botMechId); - if (!botMechEntry) { + const activeBots = getActiveSoloCombatBots(session); + if (activeBots.length === 0) { + syncLegacySoloCombatBotState(session); return; } - - const playerArmorValues = [...(session.combatPlayerArmorValues ?? DEFAULT_BOT_ARMOR_VALUES)]; - const playerInternalValues = [...(session.combatPlayerInternalValues ?? DEFAULT_BOT_INTERNAL_VALUES)]; - const playerCriticalStateBytes = [...(session.combatPlayerCriticalStateBytes ?? createCriticalStateBytes(getMechEntryForSession(session)?.extraCritCount))]; - let playerHeadArmor = session.combatPlayerHeadArmor ?? HEAD_ARMOR_VALUE; - if (isActorDestroyed(playerInternalValues)) { + const playerInternalValuesNow = session.combatPlayerInternalValues ?? DEFAULT_BOT_INTERNAL_VALUES; + if (isActorDestroyed(playerInternalValuesNow)) { + syncLegacySoloCombatBotState(session); queueCombatResultTransition( players, session, @@ -5006,300 +5504,367 @@ function stepBotWeaponFire( return; } - const botX = session.combatBotX ?? 0; - const botY = session.combatBotY ?? BOT_AI_SPAWN_DISTANCE; - const botZ = session.combatBotZ ?? 0; - const targetX = session.combatX ?? 0; - const targetY = session.combatY ?? 0; - const targetZ = session.combatJumpAltitude ?? 0; - const botFacing = session.combatBotFacing - ?? getBotFacingAccumulatorTowardTarget( - botX, - botY, - targetX, - targetY, - FACING_ACCUMULATOR_NEUTRAL, - ); - const rawYaw = getBotAimDeltaToTarget(botX, botY, targetX, targetY, botFacing); - const rawPitch = getBotPitchToTargetRaw(botX, botY, botZ, targetX, targetY, targetZ); - if (Math.abs(rawYaw) > BOT_TORSO_AIM_LIMIT_UNITS || Math.abs(rawPitch) > BOT_TORSO_AIM_LIMIT_UNITS) { - maybeLogBotAimLimit(session, connLog, botX, botY, botZ, targetX, targetY, targetZ, rawYaw, rawPitch); - return; - } - const distanceMeters = getCombatDisplayDistanceMeters(botX, botY, targetX, targetY); - const now = Date.now(); - const currentHeat = session.combatBotHeat ?? 0; - const heatSinks = Math.max(1, botMechEntry.heatSinks ?? getBotHeatSinkCount(session)); - const availableShots: BotVolleyCandidateShot[] = []; - - for (let weaponSlot = 0; weaponSlot < botMechEntry.weaponTypeIds.length; weaponSlot += 1) { - const mountGate = getWeaponMountGateForMech(botMechId, session.combatBotInternalValues, weaponSlot); - if (!mountGate.allowed) continue; - - const weaponName = getWeaponNameForMechSlot(botMechId, weaponSlot); - const weaponSpec = getWeaponSpecForMechSlot(botMechId, weaponSlot); - const cooldownGate = getWeaponCooldownGateForState( - session.combatBotWeaponReadyAtBySlot, - weaponSlot, - weaponName, - weaponSpec?.cooldownMs, - now, - ); - if (!cooldownGate.allowed) continue; + const destroyedBotSlots = new Set(); + for (const bot of activeBots) { + const difficultyProfile = getConfiguredBotDifficultyProfile(session); + const botMechId = bot.mechId; + const botMechEntry = getMechEntryForId(botMechId); + if (!botMechEntry || bot.health <= 0) { + continue; + } - const rangeGate = getShotMaxRangeGateForMechSlot( - botMechId, - weaponSlot, - botX, - botY, - targetX, - targetY, - ); - if (!rangeGate.allowed) continue; - - const ammoGate = peekBotWeaponAmmo(session, weaponSlot); - if (!ammoGate.allowed) continue; - - const toHitEstimate = estimateCombatToHit({ - attackerX: botX, - attackerY: botY, - attackerFacing: botFacing, - attackerSpeedMag: session.combatBotSpeedMag, - attackerMaxSpeedMag: botMechEntry.maxSpeedMag, - attackerAirborne: session.combatBotJumpActive === true, - targetX, - targetY, - targetFacing: getCombatCmd65Facing(session), - targetSpeedMag: session.combatSpeedMag, - targetMaxSpeedMag: session.combatMaxSpeedMag, - targetAirborne: (session.combatJumpAltitude ?? 0) > 0, - targetMoveVectorX: session.combatMoveVectorX, - targetMoveVectorY: session.combatMoveVectorY, - distanceMeters, - weaponSpec, - maxRangeMeters: rangeGate.maxRangeMeters, - }); - const damage = weaponSpec?.damage ?? BOT_FALLBACK_WEAPON_DAMAGE; - const heat = weaponSpec?.heat ?? 0; - const expectedDamage = damage * toHitEstimate.chance; + const target = getPreferredSoloCombatTarget(session, bot); + if (!target) { + continue; + } - availableShots.push({ - weaponSlot, - damage, - heat, - weaponName, - weaponSpec, - cooldownMs: cooldownGate.cooldownMs, - maxRangeMeters: rangeGate.maxRangeMeters, - rangeBand: toHitEstimate.rangeBand, - efficiency: damage / Math.max(1, heat), - hitChance: toHitEstimate.chance, - expectedDamage, - expectedEfficiency: expectedDamage / Math.max(1, heat), - }); - } + const botX = bot.x; + const botY = bot.y; + const botZ = bot.z; + const targetX = target.x; + const targetY = target.y; + const targetZ = target.z; + const botFacing = bot.facing; + const rawYaw = getBotAimDeltaToTarget(botX, botY, targetX, targetY, botFacing); + const rawPitch = getBotPitchToTargetRaw(botX, botY, botZ, targetX, targetY, targetZ); + if (Math.abs(rawYaw) > BOT_TORSO_AIM_LIMIT_UNITS || Math.abs(rawPitch) > BOT_TORSO_AIM_LIMIT_UNITS) { + maybeLogBotAimLimit(bot, connLog, botX, botY, botZ, targetX, targetY, targetZ, rawYaw, rawPitch); + continue; + } + const distanceMeters = getCombatDisplayDistanceMeters(botX, botY, targetX, targetY); + const now = Date.now(); + const currentHeat = bot.heat; + const heatSinks = Math.max(1, botMechEntry.heatSinks ?? getBotHeatSinkCount(bot)); + const availableShots: BotVolleyCandidateShot[] = []; + + for (let weaponSlot = 0; weaponSlot < botMechEntry.weaponTypeIds.length; weaponSlot += 1) { + const mountGate = getWeaponMountGateForMech(botMechId, bot.internalValues, weaponSlot); + if (!mountGate.allowed) continue; + + const weaponName = getWeaponNameForMechSlot(botMechId, weaponSlot); + const weaponSpec = getWeaponSpecForMechSlot(botMechId, weaponSlot); + const cooldownGate = getWeaponCooldownGateForState( + bot.weaponReadyAtBySlot, + weaponSlot, + weaponName, + weaponSpec?.cooldownMs, + now, + ); + if (!cooldownGate.allowed) continue; - if (availableShots.length === 0) { - return; - } + const rangeGate = getShotMaxRangeGateForMechSlot( + botMechId, + weaponSlot, + botX, + botY, + targetX, + targetY, + ); + if (!rangeGate.allowed) continue; + + const ammoGate = peekBotWeaponAmmo(bot, weaponSlot); + if (!ammoGate.allowed) continue; + + const toHitEstimate = estimateCombatToHit({ + attackerX: botX, + attackerY: botY, + attackerFacing: botFacing, + attackerSpeedMag: bot.speedMag, + attackerMaxSpeedMag: botMechEntry.maxSpeedMag, + attackerAirborne: bot.jumpActive === true, + targetX, + targetY, + targetFacing: target.facing, + targetSpeedMag: target.speedMag, + targetMaxSpeedMag: target.maxSpeedMag, + targetAirborne: target.airborne, + targetMoveVectorX: target.moveVectorX, + targetMoveVectorY: target.moveVectorY, + distanceMeters, + weaponSpec, + maxRangeMeters: rangeGate.maxRangeMeters, + }, difficultyProfile); + const damage = weaponSpec?.damage ?? BOT_FALLBACK_WEAPON_DAMAGE; + const heat = weaponSpec?.heat ?? 0; + const expectedDamage = damage * toHitEstimate.chance; + + availableShots.push({ + weaponSlot, + damage, + heat, + weaponName, + weaponSpec, + cooldownMs: cooldownGate.cooldownMs, + maxRangeMeters: rangeGate.maxRangeMeters, + rangeBand: toHitEstimate.rangeBand, + efficiency: damage / Math.max(1, heat), + hitChance: toHitEstimate.chance, + expectedDamage, + expectedEfficiency: expectedDamage / Math.max(1, heat), + }); + } - const presets = buildBotTicPresets(availableShots, currentHeat, heatSinks); - const selectedPreset = chooseBotTicPreset( - presets, - distanceMeters, - session.playerHealth ?? BOT_INITIAL_HEALTH, - currentHeat, - heatSinks, - ); - if (!selectedPreset || selectedPreset.shots.length === 0) { - return; - } + if (availableShots.length === 0) { + continue; + } - const volley: BotVolleyCandidateShot[] = []; - let firedHeat = 0; - for (const shot of selectedPreset.shots) { - const rangeGate = getShotMaxRangeGateForMechSlot( - botMechId, - shot.weaponSlot, - botX, - botY, - targetX, - targetY, + const presets = buildBotTicPresets(availableShots, currentHeat, heatSinks); + const selectedPreset = chooseBotTicPreset( + presets, + distanceMeters, + target.health, + currentHeat, + heatSinks, + difficultyProfile, ); - if (!rangeGate.allowed) continue; - const ammoGate = consumeBotWeaponAmmo(session, shot.weaponSlot); - if (!ammoGate.allowed) continue; - markBotWeaponSlotFired(session, shot.weaponSlot, shot.cooldownMs, now); - volley.push({ - ...shot, - maxRangeMeters: rangeGate.maxRangeMeters ?? shot.maxRangeMeters, - }); - firedHeat += shot.heat; - } - - if (volley.length === 0) { - return; - } - session.combatBotHeat = currentHeat + firedHeat; - - send(session.socket, buildCmd71ResetEffectStatePacket(nextSeq(session)), capture, 'CMD71_BOT_RESET'); - const shotSummaries: string[] = []; - let totalDamageUpdates = 0; + if (!selectedPreset || selectedPreset.shots.length === 0) { + continue; + } - for (const shot of volley) { - const hitRoll = resolveCombatToHitRoll({ - attackerX: botX, - attackerY: botY, - attackerFacing: botFacing, - attackerSpeedMag: session.combatBotSpeedMag, - attackerMaxSpeedMag: botMechEntry.maxSpeedMag, - attackerAirborne: session.combatBotJumpActive === true, - targetX, - targetY, - targetFacing: getCombatCmd65Facing(session), - targetSpeedMag: session.combatSpeedMag, - targetMaxSpeedMag: session.combatMaxSpeedMag, - targetAirborne: (session.combatJumpAltitude ?? 0) > 0, - targetMoveVectorX: session.combatMoveVectorX, - targetMoveVectorY: session.combatMoveVectorY, - distanceMeters, - weaponSpec: shot.weaponSpec, - maxRangeMeters: shot.maxRangeMeters, - }); - if (!hitRoll.hit) { - const missImpact = getMissImpactPoint( + const volley: BotVolleyCandidateShot[] = []; + let firedHeat = 0; + for (const shot of selectedPreset.shots) { + const rangeGate = getShotMaxRangeGateForMechSlot( + botMechId, + shot.weaponSlot, botX, botY, targetX, targetY, - session.combatJumpAltitude ?? 0, + ); + if (!rangeGate.allowed) continue; + const ammoGate = consumeBotWeaponAmmo(bot, shot.weaponSlot); + if (!ammoGate.allowed) continue; + markBotWeaponSlotFired(bot, shot.weaponSlot, shot.cooldownMs, now); + volley.push({ + ...shot, + maxRangeMeters: rangeGate.maxRangeMeters ?? shot.maxRangeMeters, + }); + firedHeat += shot.heat; + } + + if (volley.length === 0) { + continue; + } + bot.heat = currentHeat + firedHeat; + + let targetArmorValues = target.kind === 'player' + ? [...(session.combatPlayerArmorValues ?? DEFAULT_BOT_ARMOR_VALUES)] + : target.bot?.armorValues ?? [...DEFAULT_BOT_ARMOR_VALUES]; + let targetInternalValues = target.kind === 'player' + ? [...(session.combatPlayerInternalValues ?? DEFAULT_BOT_INTERNAL_VALUES)] + : target.bot?.internalValues ?? [...DEFAULT_BOT_INTERNAL_VALUES]; + let targetCriticalStateBytes = target.kind === 'player' + ? [...(session.combatPlayerCriticalStateBytes ?? createCriticalStateBytes(getMechEntryForSession(session)?.extraCritCount))] + : target.bot?.criticalStateBytes ?? createCriticalStateBytes(getMechEntryForId(target.mechId)?.extraCritCount); + let targetHeadArmor = target.kind === 'player' + ? (session.combatPlayerHeadArmor ?? HEAD_ARMOR_VALUE) + : (target.bot?.headArmor ?? HEAD_ARMOR_VALUE); + let currentTargetHealth = target.health; + + send(session.socket, buildCmd71ResetEffectStatePacket(nextSeq(session)), capture, 'CMD71_BOT_RESET'); + const shotSummaries: string[] = []; + let totalDamageUpdates = 0; + + for (const shot of volley) { + const hitRoll = resolveCombatToHitRoll({ + attackerX: botX, + attackerY: botY, + attackerFacing: botFacing, + attackerSpeedMag: bot.speedMag, + attackerMaxSpeedMag: botMechEntry.maxSpeedMag, + attackerAirborne: bot.jumpActive === true, + targetX, + targetY, + targetFacing: target.facing, + targetSpeedMag: target.speedMag, + targetMaxSpeedMag: target.maxSpeedMag, + targetAirborne: target.airborne, + targetMoveVectorX: target.moveVectorX, + targetMoveVectorY: target.moveVectorY, + distanceMeters, + weaponSpec: shot.weaponSpec, + maxRangeMeters: shot.maxRangeMeters, + }, difficultyProfile); + if (!hitRoll.hit) { + const missImpact = getMissImpactPoint( + botX, + botY, + targetX, + targetY, + targetZ, + distanceMeters, + ); + send( + session.socket, + buildCmd68ProjectileSpawnPacket( + { + sourceSlot: bot.slot, + weaponSlot: shot.weaponSlot, + targetRaw: 0, + targetAttach: 0, + angleSeedA: 0, + angleSeedB: 0, + impactX: missImpact.impactX, + impactY: missImpact.impactY, + impactZ: missImpact.impactZ, + }, + nextSeq(session), + ), + capture, + 'CMD68_BOT_PROJECTILE_MISS', + ); + shotSummaries.push( + `${shot.weaponSlot}:${shot.weaponName ?? 'unknown'}:miss:target=${target.kind === 'player' ? 'player' : `slot=${target.slot}`}:chance=${Math.round(hitRoll.chance * 100)}:roll=${Math.round(hitRoll.roll * 100)}:band=${hitRoll.rangeBand}:cross=${Math.round(hitRoll.crossingFactor * 100)}:impact=${missImpact.impactX}/${missImpact.impactY}/${missImpact.impactZ}`, + ); + continue; + } + + const hitSection = chooseBotAttackSection( + bot, + shot.weaponSlot, + targetArmorValues, + targetInternalValues, + targetHeadArmor, distanceMeters, + currentTargetHealth, ); + const targetAttach = findRepresentativeCombatAttachmentIdForSection(target.mechId, hitSection); + const previousInternalValues = [...targetInternalValues]; send( session.socket, buildCmd68ProjectileSpawnPacket( { - sourceSlot: 1, + sourceSlot: bot.slot, weaponSlot: shot.weaponSlot, - targetRaw: 0, - targetAttach: 0, + targetRaw: target.kind === 'player' ? 10 : target.slot + 1, + targetAttach: targetAttach === undefined ? 0 : targetAttach + 1, angleSeedA: 0, angleSeedB: 0, - impactX: missImpact.impactX, - impactY: missImpact.impactY, - impactZ: missImpact.impactZ, + impactX: targetX, + impactY: targetY, + impactZ: targetZ, }, nextSeq(session), ), capture, - 'CMD68_BOT_PROJECTILE_MISS', + 'CMD68_BOT_PROJECTILE', ); - shotSummaries.push( - `${shot.weaponSlot}:${shot.weaponName ?? 'unknown'}:miss:chance=${Math.round(hitRoll.chance * 100)}:roll=${Math.round(hitRoll.roll * 100)}:band=${hitRoll.rangeBand}:cross=${Math.round(hitRoll.crossingFactor * 100)}:impact=${missImpact.impactX}/${missImpact.impactY}/${missImpact.impactZ}`, + const damageResult = applyDamageToSection( + targetArmorValues, + targetInternalValues, + hitSection, + shot.damage, + targetHeadArmor, ); - continue; - } + const postDamageUpdates = collectPostDamageStateUpdates( + target.mechId, + targetCriticalStateBytes, + previousInternalValues, + targetInternalValues, + hitSection.internalIndex === 7 && damageResult.updates.some(update => update.damageCode === 0x27), + ); + targetHeadArmor = damageResult.headArmor; + const allUpdates = [...damageResult.updates, ...postDamageUpdates.updates]; + for (const update of allUpdates) { + send( + session.socket, + target.kind === 'player' + ? buildCmd67LocalDamagePacket(update.damageCode, update.damageValue, nextSeq(session)) + : buildCmd66ActorDamagePacket(target.slot, update.damageCode, update.damageValue, nextSeq(session)), + capture, + target.kind === 'player' ? 'CMD67_BOT_LOCAL_DAMAGE' : 'CMD66_BOT_REMOTE_DAMAGE', + ); + } + if (postDamageUpdates.newlyDestroyedLegs.length > 0 && !isActorDestroyed(targetInternalValues)) { + sendCombatLegLossCollapse( + session, + target.kind === 'player' ? 0 : target.slot, + connLog, + `${postDamageUpdates.newlyDestroyedLegs.map(leg => leg.label).join('+')} destroyed`, + target.kind === 'player' ? 'CMD70_BOT_PLAYER_LEG_COLLAPSE' : 'CMD70_BOT_REMOTE_LEG_COLLAPSE', + capture, + session.combatLegLossTransitionMode ?? 'collapse-only', + ); + } - const hitSection = chooseBotAttackSection( - session, - shot.weaponSlot, - playerArmorValues, - playerInternalValues, - playerHeadArmor, - distanceMeters, - ); - const targetAttach = findRepresentativeCombatAttachmentIdForSection(session.selectedMechId, hitSection); - const previousInternalValues = [...playerInternalValues]; - send( - session.socket, - buildCmd68ProjectileSpawnPacket( - { - sourceSlot: 1, - weaponSlot: shot.weaponSlot, - targetRaw: 10, - targetAttach: targetAttach === undefined ? 0 : targetAttach + 1, - angleSeedA: 0, - angleSeedB: 0, - impactX: targetX, - impactY: targetY, - impactZ: session.combatJumpAltitude ?? 0, - }, - nextSeq(session), - ), - capture, - 'CMD68_BOT_PROJECTILE', - ); - const damageResult = applyDamageToSection( - playerArmorValues, - playerInternalValues, - hitSection, - shot.damage, - playerHeadArmor, - ); - const postDamageUpdates = collectPostDamageStateUpdates( - session.selectedMechId, - playerCriticalStateBytes, - previousInternalValues, - playerInternalValues, - hitSection.internalIndex === 7 && damageResult.updates.some(update => update.damageCode === 0x27), - ); - playerHeadArmor = damageResult.headArmor; - const allUpdates = [...damageResult.updates, ...postDamageUpdates.updates]; - for (const update of allUpdates) { - send( - session.socket, - buildCmd67LocalDamagePacket(update.damageCode, update.damageValue, nextSeq(session)), - capture, - 'CMD67_BOT_LOCAL_DAMAGE', + currentTargetHealth = getCombatDurability(targetArmorValues, targetInternalValues) + targetHeadArmor; + totalDamageUpdates += allUpdates.length; + shotSummaries.push( + `${shot.weaponSlot}:${shot.weaponName ?? 'unknown'}:${shot.damage}:${hitSection.label}:slot=${bot.slot}:target=${target.kind === 'player' ? 'player' : target.slot}:attach=${targetAttach ?? 'n/a'}:chance=${Math.round(hitRoll.chance * 100)}:roll=${Math.round(hitRoll.roll * 100)}:band=${hitRoll.rangeBand}:cross=${Math.round(hitRoll.crossingFactor * 100)}:health=${currentTargetHealth}:updates=${allUpdates.map(update => `0x${update.damageCode.toString(16)}=${update.damageValue}`).join('/') || 'none'}`, ); } - if (postDamageUpdates.newlyDestroyedLegs.length > 0 && !isActorDestroyed(playerInternalValues)) { - sendCombatLegLossCollapse( + + if (target.kind === 'player') { + session.combatPlayerArmorValues = targetArmorValues; + session.combatPlayerInternalValues = targetInternalValues; + session.combatPlayerCriticalStateBytes = targetCriticalStateBytes; + session.combatPlayerHeadArmor = targetHeadArmor; + session.playerHealth = currentTargetHealth; + } else if (target.bot) { + target.bot.armorValues = targetArmorValues; + target.bot.internalValues = targetInternalValues; + target.bot.criticalStateBytes = targetCriticalStateBytes; + target.bot.headArmor = targetHeadArmor; + target.bot.health = currentTargetHealth; + if (isActorDestroyed(targetInternalValues)) { + target.bot.health = 0; + destroyedBotSlots.add(target.bot.slot); + } + } + + send(session.socket, buildCmd71ResetEffectStatePacket(nextSeq(session)), capture, 'CMD71_BOT_CLOSE'); + connLog.info( + '[world/combat] bot volley: slot=%d side=%d mechId=%d target=%s tic=%s distance=%dm weapons=%s heat=%s expected=%.1f targetHealth=%d updates=%d shots=[%s]', + bot.slot, + bot.side, + botMechId, + target.kind === 'player' ? 'player' : `slot=${target.slot}/side=${target.side}`, + selectedPreset.name, + Math.round(distanceMeters), + volley.map(shot => shot.weaponSlot).join('/'), + `${Math.round(currentHeat * 10) / 10}->${Math.round(bot.heat * 10) / 10} risk=${Math.round(selectedPreset.overheatRisk * 100)}`, + selectedPreset.totalExpectedDamage, + currentTargetHealth, + totalDamageUpdates, + shotSummaries.join(','), + ); + + if (target.kind === 'player' && isActorDestroyed(targetInternalValues)) { + session.playerHealth = 0; + syncLegacySoloCombatBotState(session); + queueCombatResultTransition( + players, session, - 0, connLog, - `${postDamageUpdates.newlyDestroyedLegs.map(leg => leg.label).join('+')} destroyed`, - 'CMD70_BOT_PLAYER_LEG_COLLAPSE', capture, - session.combatLegLossTransitionMode ?? 'collapse-only', + COMBAT_RESULT_LOSS, + (targetInternalValues[7] ?? 0) <= 0 ? 'head destroyed' : 'center torso destroyed', + PLAYER_RESULT_DELAY_MS, ); + return; } - - totalDamageUpdates += allUpdates.length; - shotSummaries.push( - `${shot.weaponSlot}:${shot.weaponName ?? 'unknown'}:${shot.damage}:${hitSection.label}:attach=${targetAttach ?? 'n/a'}:chance=${Math.round(hitRoll.chance * 100)}:roll=${Math.round(hitRoll.roll * 100)}:band=${hitRoll.rangeBand}:cross=${Math.round(hitRoll.crossingFactor * 100)}:health=${getCombatDurability(playerArmorValues, playerInternalValues) + playerHeadArmor}:updates=${allUpdates.map(update => `0x${update.damageCode.toString(16)}=${update.damageValue}`).join('/') || 'none'}`, - ); } - session.combatPlayerArmorValues = playerArmorValues; - session.combatPlayerInternalValues = playerInternalValues; - session.combatPlayerCriticalStateBytes = playerCriticalStateBytes; - session.combatPlayerHeadArmor = playerHeadArmor; - session.playerHealth = getCombatDurability(playerArmorValues, playerInternalValues) + playerHeadArmor; - send(session.socket, buildCmd71ResetEffectStatePacket(nextSeq(session)), capture, 'CMD71_BOT_CLOSE'); - connLog.info( - '[world/combat] bot volley: mechId=%d tic=%s distance=%dm weapons=%s heat=%s expected=%.1f playerHealth=%d updates=%d shots=[%s]', - botMechId, - selectedPreset.name, - Math.round(distanceMeters), - volley.map(shot => shot.weaponSlot).join('/'), - `${Math.round(currentHeat * 10) / 10}->${Math.round((session.combatBotHeat ?? 0) * 10) / 10} risk=${Math.round(selectedPreset.overheatRisk * 100)}`, - selectedPreset.totalExpectedDamage, - session.playerHealth, - totalDamageUpdates, - shotSummaries.join(','), - ); - - if (isActorDestroyed(playerInternalValues)) { - session.playerHealth = 0; + syncLegacySoloCombatBotState(session); + for (const slot of destroyedBotSlots) { + const bot = getSoloCombatBotBySlot(session, slot); + if (!bot) { + continue; + } + sendBotDeathTransition(session, bot, connLog, capture, 'destroyed by bot fire'); + } + if (getActiveHostileSoloCombatBots(session).length === 0) { + session.botHealth = 0; + stopBotCombatActions(session); queueCombatResultTransition( players, session, connLog, capture, - COMBAT_RESULT_LOSS, - (playerInternalValues[7] ?? 0) <= 0 ? 'head destroyed' : 'center torso destroyed', - PLAYER_RESULT_DELAY_MS, + COMBAT_RESULT_VICTORY, + 'all hostile solo-combat bots structurally destroyed', + BOT_RESULT_DELAY_MS, ); } } @@ -5349,37 +5914,38 @@ function stopBotCombatActions(session: ClientSession): void { function sendBotDeathTransition( session: ClientSession, + bot: SoloCombatBotActorState, connLog: Logger, capture: CaptureLogger, reason: string, ): void { - if (session.botDeathTimer !== undefined) { - clearTimeout(session.botDeathTimer); - session.botDeathTimer = undefined; + if (bot.deathTimer !== undefined) { + clearTimeout(bot.deathTimer); + bot.deathTimer = undefined; } // Retail v1.23 death flow uses Cmd70 subcommand 8 to start the collapse and // then subcommand 0 to advance the actor through the destruction tail. - connLog.info('[world/combat] bot destroyed — sending collapse transition (%s)', reason); + connLog.info('[world/combat] bot destroyed — slot=%d mechId=%d sending collapse transition (%s)', bot.slot, bot.mechId, reason); send( session.socket, - buildCmd70ActorTransitionPacket(1, 8, nextSeq(session)), + buildCmd70ActorTransitionPacket(bot.slot, 8, nextSeq(session)), capture, 'CMD70_BOT_COLLAPSE', ); - session.botDeathTimer = setTimeout(() => { - session.botDeathTimer = undefined; + bot.deathTimer = setTimeout(() => { + bot.deathTimer = undefined; if (session.socket.destroyed || !session.socket.writable || session.phase !== 'combat') return; - connLog.info('[world/combat] bot death-tail advance after collapse (%s)', reason); + connLog.info('[world/combat] bot death-tail advance: slot=%d mechId=%d (%s)', bot.slot, bot.mechId, reason); send( session.socket, - buildCmd70ActorTransitionPacket(1, 0, nextSeq(session)), + buildCmd70ActorTransitionPacket(bot.slot, 0, nextSeq(session)), capture, 'CMD70_BOT_DEATH_ADVANCE', ); }, 1200); - session.botDeathTimer.unref(); + bot.deathTimer.unref(); } function queueCombatResultTransition( @@ -5467,6 +6033,7 @@ function queueCombatResultTransition( */ export function stopCombatTimers(session: ClientSession): void { stopBotCombatActions(session); + clearSoloCombatBotDeathTimers(session); if (session.botDeathTimer !== undefined) { clearTimeout(session.botDeathTimer); session.botDeathTimer = undefined; @@ -5516,6 +6083,7 @@ export function resetCombatState(session: ClientSession): void { stopCombatTimers(session); session.combatInitialized = false; session.phase = 'world'; + session.combatBotActors = undefined; session.botHealth = undefined; session.playerHealth = undefined; session.combatX = undefined; @@ -6788,13 +7356,16 @@ function startArenaCombatSession( const remoteMechEntry = WORLD_MECH_BY_ID.get(remoteMechId); const remoteSlot = remoteIndex + 1; const remoteCallsign = getDisplayName(remote); + const remoteIsTeammate = + participant.worldArenaSide !== undefined + && participant.worldArenaSide === remote.worldArenaSide; sendToWorldSession( participant, buildCmd64RemoteActorPacket( { slot: remoteSlot, - actorTypeByte: 0, + actorTypeByte: getCombatRemoteActorTypeByte(remoteIsTeammate), identity0: remoteCallsign.substring(0, 11), identity1: remoteCallsign.substring(0, 31), identity2: remoteMechEntry?.typeString ?? '', @@ -8195,6 +8766,19 @@ export function sendCombatBootstrapSequence( session.combatLastJumpLandAt = undefined; session.combatLastJumpLandAltitude = undefined; session.botHealth = BOT_INITIAL_HEALTH; + const verificationMode = session.combatVerificationMode; + if (!hasConfiguredSoloCombatHostileBots(session, verificationMode)) { + send( + socket, + buildCmd3BroadcastPacket( + 'Solo bot combat needs at least one bot on an enemy side. Use /botside first.', + nextSeq(session), + ), + capture, + 'CMD3_BOTSIDE_HOSTILE_REQUIRED', + ); + return; + } notifyRoomDeparture(players, session, connLog); @@ -8214,6 +8798,13 @@ export function sendCombatBootstrapSequence( session.combatBootstrapTimer = undefined; if (session.socket.destroyed || !session.socket.writable || session.phase !== 'combat') return; + const botCount = getConfiguredSoloCombatBotCount(session, verificationMode); + const playerSide = getConfiguredSoloCombatPlayerSide(session); + const botSides = Array.from({ length: botCount }, (_, index) => getConfiguredBotSide(session, index, verificationMode)); + const soloSpawnLayout = getSoloCombatSpawnLayout(botSides, playerSide); + session.combatX = soloSpawnLayout.playerSpawn.x; + session.combatY = soloSpawnLayout.playerSpawn.y; + // 2. Cmd72 — local bootstrap (combat CRC seed applied by buildGamePacket). const cmd72 = buildCmd72LocalBootstrapPacket( { @@ -8234,10 +8825,10 @@ export function sendCombatBootstrapSequence( identity3: session.allegiance ?? '', identity4: '', statusByte: 0, - initialX: 0, - initialY: 0, + initialX: session.combatX ?? 0, + initialY: session.combatY ?? 0, extraType2Values: [], - remainingActorCount: 1, + remainingActorCount: botCount, unknownType1Raw: MOTION_NEUTRAL, mech: { mechId, @@ -8262,49 +8853,20 @@ export function sendCombatBootstrapSequence( send(socket, cmd72, capture, 'CMD72_COMBAT_BOOTSTRAP'); session.combatStartAt = Date.now(); - // 3. Cmd64 — add remote bot actor at slot 1. - const botMechId = session.combatBotMechId ?? mechId; - const botMechEntry = WORLD_MECH_BY_ID.get(botMechId); - const botCriticalStateBytes = createCriticalStateBytes(botMechEntry?.extraCritCount); - session.combatBotArmorValues = [...(botMechEntry?.armorLikeMaxValues ?? DEFAULT_BOT_ARMOR_VALUES)]; - session.combatBotInternalValues = botMechEntry !== undefined - ? mechInternalStateBytes(botMechEntry.tonnage) - : [...DEFAULT_BOT_INTERNAL_VALUES]; - session.combatBotCriticalStateBytes = botCriticalStateBytes; - session.combatBotHeadArmor = HEAD_ARMOR_VALUE; - session.combatBotX = 0; - session.combatBotY = BOT_AI_SPAWN_DISTANCE; - session.combatBotZ = 0; - session.combatBotFacing = getBotFacingAccumulatorTowardTarget( - session.combatBotX, - session.combatBotY, - session.combatX ?? 0, - session.combatY ?? 0, - FACING_ACCUMULATOR_NEUTRAL, - ); - session.combatBotSpeedMag = 0; - session.combatBotLastMoveAt = undefined; - session.combatBotLastAimLimitLogAt = undefined; - session.combatBotMoveVectorX = 0; - session.combatBotMoveVectorY = 0; - session.combatBotWeaponReadyAtBySlot = []; - session.combatBotAmmoStateValues = getInitialCombatAmmoStateValues(botMechEntry?.ammoBinTypeIds); - session.combatBotHeat = 0; - session.combatBotJumpActive = false; - session.combatBotJumpFuel = JUMP_JET_FUEL_MAX; - session.combatBotJumpStartedAt = undefined; - session.combatBotJumpDurationMs = undefined; - session.combatBotJumpStartFuel = undefined; - session.combatBotJumpApexUnits = undefined; - session.combatBotJumpStartX = undefined; - session.combatBotJumpStartY = undefined; - session.combatBotJumpTargetX = undefined; - session.combatBotJumpTargetY = undefined; - session.combatBotLastJumpAt = undefined; - session.botHealth = getCombatDurability( - session.combatBotArmorValues, - session.combatBotInternalValues, - ) + (session.combatBotHeadArmor ?? 0); + const botSpawnPoints = soloSpawnLayout.botSpawnPoints; + const botActors = Array.from({ length: botCount }, (_, index) => { + const spawn = botSpawnPoints[index] ?? { x: 0, y: BOT_AI_SPAWN_DISTANCE }; + return createSoloCombatBotActor( + session, + index + 1, + getConfiguredBotLoadoutId(session, index), + botSides[index] ?? getConfiguredSoloCombatDefaultEnemySide(session), + spawn.x, + spawn.y, + ); + }); + session.combatBotActors = botActors; + syncLegacySoloCombatBotState(session); session.combatPlayerArmorValues = [...(mechEntry?.armorLikeMaxValues ?? DEFAULT_BOT_ARMOR_VALUES)]; session.combatPlayerInternalValues = mechEntry !== undefined ? mechInternalStateBytes(mechEntry.tonnage) @@ -8316,22 +8878,27 @@ export function sendCombatBootstrapSequence( session.combatPlayerInternalValues, ) + (session.combatPlayerHeadArmor ?? 0); session.combatRetaliationCursor = 0; - const cmd64 = buildCmd64RemoteActorPacket( - { - slot: 1, - actorTypeByte: 0, - identity0: 'Opponent', - identity1: 'Opponent', - identity2: botMechEntry?.typeString ?? '', - identity3: '', - identity4: '', - statusByte: 0, - mechId: botMechId, - }, - nextSeq(session), - ); - send(socket, cmd64, capture, 'CMD64_BOT_ACTOR'); - connLog.info('[world] bot actor: mech_id=%d type=%s', botMechId, botMechEntry?.typeString ?? '?'); + for (const bot of botActors) { + const botMechEntry = WORLD_MECH_BY_ID.get(bot.mechId); + const botLabel = getSoloCombatBotSlotLabel(bot.slot, bot.side, playerSide); + const botIsTeammate = bot.side === playerSide; + const cmd64 = buildCmd64RemoteActorPacket( + { + slot: bot.slot, + actorTypeByte: getCombatRemoteActorTypeByte(botIsTeammate), + identity0: botLabel.substring(0, 11), + identity1: botLabel.substring(0, 31), + identity2: botMechEntry?.typeString ?? '', + identity3: '', + identity4: '', + statusByte: 0, + mechId: bot.mechId, + }, + nextSeq(session), + ); + send(socket, cmd64, capture, 'CMD64_BOT_ACTOR'); + connLog.info('[world] bot actor: slot=%d side=%d mech_id=%d type=%s x=%d y=%d', bot.slot, bot.side, bot.mechId, botMechEntry?.typeString ?? '?', bot.x, bot.y); + } const cmd65 = buildCmd65PositionSyncPacket( { slot: 0, x: 0, y: 0, z: 0, facing: 0, throttle: 0, legVel: 0, speedMag: 0 }, @@ -8339,7 +8906,9 @@ export function sendCombatBootstrapSequence( ); send(socket, cmd65, capture, 'CMD65_INITIAL_POSITION'); - sendBotPositionSync(session, capture, 'CMD65_BOT_POSITION'); + for (const bot of botActors) { + sendBotPositionSync(session, bot, capture, 'CMD65_BOT_POSITION'); + } const cmd62 = buildCmd62CombatStartPacket(nextSeq(session)); send(socket, cmd62, capture, 'CMD62_COMBAT_START'); @@ -8355,7 +8924,6 @@ export function sendCombatBootstrapSequence( }, JUMP_JET_FUEL_REGEN_INTERVAL_MS); session.combatJumpFuelRegenTimer.unref(); - const verificationMode = session.combatVerificationMode; if (verificationMode === 'headtest') { // Keep scripted retaliation only for the explicit headtest verifier. startForcedRetaliationVerification( @@ -8414,7 +8982,11 @@ export function sendCombatBootstrapSequence( setTimeout(() => { if (session.socket.destroyed || !session.socket.writable) return; connLog.info('[world/combat] scripted verification: autowin'); - session.botHealth = 0; + const primaryBot = getSoloCombatBotBySlot(session, 1); + if (primaryBot) { + primaryBot.health = 0; + } + syncLegacySoloCombatBotState(session); send( session.socket, buildCmd66ActorDamagePacket(1, 1, 999, nextSeq(session)), @@ -8422,7 +8994,9 @@ export function sendCombatBootstrapSequence( 'CMD66_VERIFY_AUTOWIN', ); stopBotCombatActions(session); - sendBotDeathTransition(session, connLog, capture, 'verify-autowin'); + if (primaryBot) { + sendBotDeathTransition(session, primaryBot, connLog, capture, 'verify-autowin'); + } queueCombatResultTransition( players, session, @@ -8914,6 +9488,41 @@ function handleDuelTextCommand( return true; } +const WORLD_COMMAND_HELP_MAX_LINES = 10; + +const WORLD_COMMAND_HELP_LINES = [ + 'Available chat commands:', + '/map | /travel - open the Solaris travel map', + '/mech | /mechbay | /mechs - open the mech picker', + '/bots | /botcount <1-7> | /botdifficulty <1-5> (/botdiff)', + '/botside - set bot side/team', + '/botpick [slot] | /botmech | /botmech ', + '/fight | /fightrestart - start or restart non-duel combat', + '/ready | /unready | /notready - arena ready-room status', + '/duel | /acceptduel | /declineduel', + '/duelstatus | /duelterms | /duelcancel - duel status and stakes', +] as const; + +if (WORLD_COMMAND_HELP_LINES.length > WORLD_COMMAND_HELP_MAX_LINES) { + throw new Error( + `WORLD_COMMAND_HELP_LINES exceeds the ${WORLD_COMMAND_HELP_MAX_LINES}-line client limit.`, + ); +} + +export function sendWorldCommandHelp( + session: ClientSession, + capture: CaptureLogger, +): void { + for (const line of WORLD_COMMAND_HELP_LINES) { + send( + session.socket, + buildCmd3BroadcastPacket(line, nextSeq(session)), + capture, + 'CMD3_HELP', + ); + } +} + export function handleWorldTextCommand( players: PlayerRegistry, session: ClientSession, @@ -8928,6 +9537,11 @@ export function handleWorldTextCommand( return; } + if (lower === '/help') { + sendWorldCommandHelp(session, capture); + return; + } + if (lower === '/map' || lower === '/travel') { sendSolarisTravelMap(session, connLog, capture); return; @@ -9052,42 +9666,160 @@ export function handleBotMechTextCommand( } = {}, ): boolean { const clean = text.replace(/\x1b/g, '?').trim(); - const botmechMatch = clean.match(/^\/botmech\s+(\d+)$/i); - if (!botmechMatch) { - return false; - } - - const requestedId = parseInt(botmechMatch[1], 10); - const mechEntry = WORLD_MECH_BY_ID.get(requestedId); - if (!mechEntry) { - connLog.warn('[world] /botmech: unknown mech_id=%d', requestedId); + const sendLocalNotice = (message: string, label: string): void => { if (!options.suppressBroadcast) { send( session.socket, buildCmd3BroadcastPacket( - `Unknown mech_id ${requestedId}. Use /mechs to browse available mechs.`, + message, nextSeq(session), ), capture, - 'CMD3_BOTMECH_UNKNOWN', + label, + ); + } + }; + + if (/^\/bots$/i.test(clean)) { + sendLocalNotice(buildBotConfigurationSummary(session), 'CMD3_BOT_CONFIG_SUMMARY'); + return true; + } + + const botCountMatch = clean.match(/^\/botcount\s+(\d+)$/i); + if (botCountMatch) { + const requestedCount = parseInt(botCountMatch[1], 10); + if (!Number.isFinite(requestedCount) || requestedCount < 1 || requestedCount > MAX_SINGLE_PLAYER_BOT_OPPONENTS) { + sendLocalNotice( + `Bot count must be between 1 and ${MAX_SINGLE_PLAYER_BOT_OPPONENTS}.`, + 'CMD3_BOTCOUNT_INVALID', ); + return true; } + session.combatBotOpponentCount = requestedCount; + worldResumeRegistry.save(session); + connLog.info('[world] /botcount: configured %d bot opponents', requestedCount); + sendLocalNotice( + `${buildBotConfigurationSummary(session)} Use /botside , /botpick , /botmech , or /botdifficulty <1-5> to change bot setup.`, + 'CMD3_BOTCOUNT_ACK', + ); return true; } - session.combatBotMechId = requestedId; - connLog.info('[world] /botmech: bot mech set to %s (id=%d)', mechEntry.typeString, requestedId); - if (!options.suppressBroadcast) { - send( - session.socket, - buildCmd3BroadcastPacket( - `Bot mech set to ${mechEntry.typeString} (id=${requestedId}). Use /fight or /fightrestart.`, - nextSeq(session), - ), - capture, - 'CMD3_BOTMECH_ACK', + const botDifficultyMatch = clean.match(/^\/bot(?:difficulty|diff)\s+(\d+)$/i); + if (botDifficultyMatch) { + const requestedDifficulty = parseInt(botDifficultyMatch[1], 10); + if (!Number.isFinite(requestedDifficulty) || requestedDifficulty < 1 || requestedDifficulty > 5) { + sendLocalNotice('Bot difficulty must be between 1 and 5.', 'CMD3_BOTDIFFICULTY_INVALID'); + return true; + } + session.combatBotDifficultyLevel = clampBotDifficultyLevel(requestedDifficulty); + worldResumeRegistry.save(session); + connLog.info('[world] /botdifficulty: configured bot difficulty=%d', session.combatBotDifficultyLevel); + sendLocalNotice( + `Bot difficulty set to ${getConfiguredBotDifficultyLevel(session)}/5. ${buildBotConfigurationSummary(session)}`, + 'CMD3_BOTDIFFICULTY_ACK', + ); + return true; + } + + const botSideMatch = clean.match(/^\/bot(?:side|team)\s+(\d+)\s+([^\s]+)$/i); + if (botSideMatch) { + const requestedSlot = parseInt(botSideMatch[1], 10); + if (!Number.isFinite(requestedSlot) || requestedSlot < 1 || requestedSlot > MAX_SINGLE_PLAYER_BOT_OPPONENTS) { + sendLocalNotice( + `Bot slot must be between 1 and ${MAX_SINGLE_PLAYER_BOT_OPPONENTS}.`, + 'CMD3_BOTSIDE_INVALID_SLOT', + ); + return true; + } + + const requestedSideToken = botSideMatch[2].toLowerCase(); + let requestedSide: number | undefined; + if (requestedSideToken === 'player' || requestedSideToken === 'ally' || requestedSideToken === 'friendly') { + requestedSide = getConfiguredSoloCombatPlayerSide(session); + } else if (requestedSideToken === 'enemy' || requestedSideToken === 'opponent' || requestedSideToken === 'hostile') { + requestedSide = getConfiguredSoloCombatDefaultEnemySide(session); + } else if (/^\d+$/.test(requestedSideToken)) { + requestedSide = parseInt(requestedSideToken, 10); + } + + if (!Number.isFinite(requestedSide) || requestedSide === undefined || requestedSide < MIN_SOLO_COMBAT_SIDE || requestedSide > MAX_SOLO_COMBAT_SIDE) { + sendLocalNotice( + `Bot side must be player, enemy, or a side between ${MIN_SOLO_COMBAT_SIDE} and ${MAX_SOLO_COMBAT_SIDE}.`, + 'CMD3_BOTSIDE_INVALID_SIDE', + ); + return true; + } + + setConfiguredBotSide(session, requestedSlot - 1, requestedSide); + worldResumeRegistry.save(session); + connLog.info('[world] /botside: slot=%d configured side=%d', requestedSlot, requestedSide); + sendLocalNotice( + `Bot ${requestedSlot} side set to ${getConfiguredBotSideSummaryLabel(session, requestedSlot - 1)}. ${buildBotConfigurationSummary(session)}`, + 'CMD3_BOTSIDE_ACK', + ); + return true; + } + + const botPickMatch = clean.match(/^\/botpick(?:\s+(\d+))?$/i); + if (botPickMatch) { + if (session.phase !== 'world') { + sendLocalNotice('Bot mech picker is only available in world mode.', 'CMD3_BOTPICK_INVALID_PHASE'); + return true; + } + const requestedSlot = parseInt(botPickMatch[1] ?? '1', 10); + if (!Number.isFinite(requestedSlot) || requestedSlot < 1 || requestedSlot > MAX_SINGLE_PLAYER_BOT_OPPONENTS) { + sendLocalNotice( + `Bot slot must be between 1 and ${MAX_SINGLE_PLAYER_BOT_OPPONENTS}.`, + 'CMD3_BOTPICK_INVALID_SLOT', + ); + return true; + } + session.combatBotOpponentCount = Math.max(getConfiguredBotOpponentCount(session), requestedSlot); + worldResumeRegistry.save(session); + connLog.info('[world] /botpick: opening mech picker for bot slot=%d', requestedSlot); + sendMechClassPicker(session, connLog, capture, { target: 'bot', botIndex: requestedSlot - 1 }); + return true; + } + + const botMechSlotMatch = clean.match(/^\/botmech\s+(\d+)\s+(\d+)$/i); + const botMechSingleMatch = clean.match(/^\/botmech\s+(\d+)$/i); + if (!botMechSlotMatch && !botMechSingleMatch) { + return false; + } + + const requestedSlot = botMechSlotMatch ? parseInt(botMechSlotMatch[1], 10) : 1; + const requestedId = parseInt((botMechSlotMatch ? botMechSlotMatch[2] : botMechSingleMatch?.[1]) ?? '0', 10); + if (!Number.isFinite(requestedSlot) || requestedSlot < 1 || requestedSlot > MAX_SINGLE_PLAYER_BOT_OPPONENTS) { + sendLocalNotice( + `Bot slot must be between 1 and ${MAX_SINGLE_PLAYER_BOT_OPPONENTS}.`, + 'CMD3_BOTMECH_INVALID_SLOT', + ); + return true; + } + + const mechEntry = WORLD_MECH_BY_ID.get(requestedId); + if (!mechEntry) { + connLog.warn('[world] /botmech: unknown mech_id=%d slot=%d', requestedId, requestedSlot); + sendLocalNotice( + `Unknown mech_id ${requestedId}. Use /mechs to browse available mechs.`, + 'CMD3_BOTMECH_UNKNOWN', ); + return true; } + + setConfiguredBotLoadoutId(session, requestedSlot - 1, requestedId); + worldResumeRegistry.save(session); + connLog.info( + '[world] /botmech: slot=%d bot mech set to %s (id=%d)', + requestedSlot, + mechEntry.typeString, + requestedId, + ); + sendLocalNotice( + `Bot ${requestedSlot} mech set to ${mechEntry.typeString} (id=${requestedId}). ${buildBotConfigurationSummary(session)}`, + 'CMD3_BOTMECH_ACK', + ); return true; } @@ -9557,6 +10289,8 @@ export function handleCombatWeaponFireFrame( return; } + const combatSession = players.getCombatSession(session.combatSessionId); + const blockedFriendlyShotSummaries: string[] = []; const fireableShots = []; for (const shot of shots) { const mountGate = getWeaponMountGate(session, shot.weaponSlot); @@ -9577,10 +10311,41 @@ export function handleCombatWeaponFireFrame( cooldownGate.weaponName ?? 'unknown', cooldownGate.remainingMs ?? 0, cooldownGate.cooldownMs ?? 0, - ); + ); continue; } + if (combatSession?.mode === 'arena' && combatSession.state === 'active') { + const target = getCombatTargetParticipantForViewerSlot(players, combatSession, session, shot.targetSlot); + const friendlyTarget = !!target + && !target.socket.destroyed + && target.phase === 'combat' + && target.combatSessionId === combatSession.id + && target.combatResultCode === undefined + && (target.playerHealth ?? 1) > 0 + && session.worldArenaSide !== undefined + && session.worldArenaSide === target.worldArenaSide; + if (friendlyTarget) { + const { damage: shotDamage, weaponName } = getShotDamage(session, shot.weaponSlot); + blockedFriendlyShotSummaries.push( + `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:friendly-fire-blocked:${getDisplayName(target)}`, + ); + continue; + } + } else { + const targetBot = shot.targetSlot <= 0 ? undefined : getSoloCombatBotBySlot(session, shot.targetSlot); + const friendlyTarget = !!targetBot + && targetBot.health > 0 + && targetBot.side === getConfiguredSoloCombatPlayerSide(session); + if (friendlyTarget) { + const { damage: shotDamage, weaponName } = getShotDamage(session, shot.weaponSlot); + blockedFriendlyShotSummaries.push( + `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:friendly-fire-blocked:slot=${targetBot.slot}`, + ); + continue; + } + } + const ammoGate = consumeWeaponAmmo(session, shot.weaponSlot); if (!ammoGate.allowed) { connLog.info( @@ -9616,9 +10381,18 @@ export function handleCombatWeaponFireFrame( fireableShots.length, ); } - if (fireableShots.length === 0) return; + if (fireableShots.length === 0) { + if (blockedFriendlyShotSummaries.length > 0) { + connLog.info( + '[world/combat] cmd10 friendly fire blocked: firePath=%s records=%d shots=[%s]', + firePath, + blockedFriendlyShotSummaries.length, + blockedFriendlyShotSummaries.join(','), + ); + } + return; + } - const combatSession = players.getCombatSession(session.combatSessionId); if (combatSession?.mode === 'duel') { const duelPeer = getActiveDuelPeer(players, session); if (!duelPeer) { @@ -9638,7 +10412,7 @@ export function handleCombatWeaponFireFrame( let duelPeerHeadArmor = duelPeer.combatPlayerHeadArmor ?? HEAD_ARMOR_VALUE; send(session.socket, buildCmd71ResetEffectStatePacket(nextSeq(session)), capture, 'CMD71_RESET'); sendToWorldSession(duelPeer, buildCmd71ResetEffectStatePacket(nextSeq(duelPeer)), 'CMD71_DUEL_REMOTE_RESET'); - const shotSummaries: string[] = []; + const shotSummaries = [...blockedFriendlyShotSummaries]; let totalDamageUpdates = 0; for (const shot of fireableShots) { @@ -9805,7 +10579,7 @@ export function handleCombatWeaponFireFrame( sendToWorldSession(viewer, buildCmd71ResetEffectStatePacket(nextSeq(viewer)), 'CMD71_ARENA_RESET'); } - const shotSummaries: string[] = []; + const shotSummaries = [...blockedFriendlyShotSummaries]; let totalDamageUpdates = 0; const eliminatedParticipants = new Map(); @@ -9989,35 +10763,35 @@ export function handleCombatWeaponFireFrame( return; } - if (session.botHealth === undefined) { - session.botHealth = getCombatDurability( - session.combatBotArmorValues ?? DEFAULT_BOT_ARMOR_VALUES, - session.combatBotInternalValues ?? DEFAULT_BOT_INTERNAL_VALUES, - ) + (session.combatBotHeadArmor ?? HEAD_ARMOR_VALUE); - } - if (session.botHealth <= 0) { - connLog.debug('[world/combat] cmd-10 shot ignored — bot already destroyed'); + const activeBots = getActiveSoloCombatBots(session); + if (activeBots.length === 0) { + connLog.debug('[world/combat] cmd-10 shot ignored — all solo-combat bots already destroyed'); return; } + const difficultyProfile = getConfiguredBotDifficultyProfile(session); + const playerSide = getConfiguredSoloCombatPlayerSide(session); - const botArmorValues = [...(session.combatBotArmorValues ?? DEFAULT_BOT_ARMOR_VALUES)]; - const botInternalValues = [...(session.combatBotInternalValues ?? DEFAULT_BOT_INTERNAL_VALUES)]; - const botCriticalStateBytes = [...(session.combatBotCriticalStateBytes ?? createCriticalStateBytes(WORLD_MECH_BY_ID.get(session.combatBotMechId ?? session.selectedMechId ?? FALLBACK_MECH_ID)?.extraCritCount))]; - let botHeadArmor = session.combatBotHeadArmor ?? HEAD_ARMOR_VALUE; - const botMechId = session.combatBotMechId ?? session.selectedMechId; - const botMechEntry = getMechEntryForId(botMechId); - const botModelId = getCombatModelIdForMechId(botMechId); - const botX = session.combatBotX ?? 0; - const botY = session.combatBotY ?? BOT_AI_SPAWN_DISTANCE; - const botZ = session.combatBotZ ?? 0; - const botFacing = session.combatBotFacing ?? 0; send(session.socket, buildCmd71ResetEffectStatePacket(nextSeq(session)), capture, 'CMD71_RESET'); const shotSummaries: string[] = []; let totalDamageUpdates = 0; + const eliminatedBotSlots = new Set(); for (const shot of fireableShots) { const { damage: shotDamage, weaponName, weaponSpec } = getShotDamage(session, shot.weaponSlot); - const rangeGate = getShotMaxRangeGate(session, shot.weaponSlot, botX, botY); + const targetBot = shot.targetSlot <= 0 ? undefined : getSoloCombatBotBySlot(session, shot.targetSlot); + const targetActive = !!targetBot && targetBot.health > 0 && !eliminatedBotSlots.has(targetBot.slot); + const friendlyTarget = targetActive && targetBot.side === playerSide; + const targetX = targetBot?.x ?? 0; + const targetY = targetBot?.y ?? 0; + const targetZ = targetBot?.z ?? 0; + + if (friendlyTarget) { + shotSummaries.push( + `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:friendly-fire-blocked:slot=${targetBot.slot}`, + ); + continue; + } + send( session.socket, buildCmd68ProjectileSpawnPacket( @@ -10037,12 +10811,25 @@ export function handleCombatWeaponFireFrame( capture, 'CMD68_PROJECTILE', ); + + if (!targetActive || !targetBot) { + shotSummaries.push( + `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:no-active-target:${shot.targetSlot}/${shot.targetAttach}`, + ); + continue; + } + + const rangeGate = getShotMaxRangeGate(session, shot.weaponSlot, targetX, targetY); if (!rangeGate.allowed) { shotSummaries.push( - `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:out-of-range:${Math.round(rangeGate.distanceMeters ?? -1)}/${rangeGate.maxRangeMeters ?? 'n/a'}m`, + `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:slot=${targetBot.slot}:out-of-range:${Math.round(rangeGate.distanceMeters ?? -1)}/${rangeGate.maxRangeMeters ?? 'n/a'}m`, ); continue; } + + const botMechId = targetBot.mechId; + const botMechEntry = getMechEntryForId(botMechId); + const botModelId = getCombatModelIdForMechId(botMechId); const hitRoll = resolveCombatToHitRoll({ attackerX: session.combatX ?? 0, attackerY: session.combatY ?? 0, @@ -10050,42 +10837,43 @@ export function handleCombatWeaponFireFrame( attackerSpeedMag: session.combatSpeedMag, attackerMaxSpeedMag: session.combatMaxSpeedMag, attackerAirborne: (session.combatJumpAltitude ?? 0) > 0, - targetX: botX, - targetY: botY, - targetFacing: botFacing, - targetSpeedMag: session.combatBotSpeedMag, + targetX, + targetY, + targetFacing: targetBot.facing, + targetSpeedMag: targetBot.speedMag, targetMaxSpeedMag: botMechEntry?.maxSpeedMag, - targetAirborne: session.combatBotJumpActive === true || botZ > 0, - targetMoveVectorX: session.combatBotMoveVectorX, - targetMoveVectorY: session.combatBotMoveVectorY, - distanceMeters: rangeGate.distanceMeters ?? getCombatDisplayDistanceMeters(session.combatX ?? 0, session.combatY ?? 0, botX, botY), + targetAirborne: targetBot.jumpActive === true || targetZ > 0, + targetMoveVectorX: targetBot.moveVectorX, + targetMoveVectorY: targetBot.moveVectorY, + distanceMeters: rangeGate.distanceMeters ?? getCombatDisplayDistanceMeters(session.combatX ?? 0, session.combatY ?? 0, targetX, targetY), weaponSpec, maxRangeMeters: rangeGate.maxRangeMeters, - }); + }, difficultyProfile); if (!hitRoll.hit) { shotSummaries.push( - `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:miss:${shot.targetSlot}/${shot.targetAttach}:chance=${Math.round(hitRoll.chance * 100)}:roll=${Math.round(hitRoll.roll * 100)}:band=${hitRoll.rangeBand}:cross=${Math.round(hitRoll.crossingFactor * 100)}:bot=${botX}/${botY}`, + `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:slot=${targetBot.slot}:miss:${shot.targetSlot}/${shot.targetAttach}:chance=${Math.round(hitRoll.chance * 100)}:roll=${Math.round(hitRoll.roll * 100)}:band=${hitRoll.rangeBand}:cross=${Math.round(hitRoll.crossingFactor * 100)}:bot=${targetX}/${targetY}`, ); continue; } + const impactContext = buildTargetImpactContext( shot.impactXRaw - COORD_BIAS, shot.impactYRaw - COORD_BIAS, shot.impactZ, - botX, - botY, - botZ, - botFacing, + targetX, + targetY, + targetZ, + targetBot.facing, ); const hitSection = resolveEffectiveHitSection( botMechId, shot.targetAttach, shot.impactZ, - botArmorValues, - botInternalValues, + targetBot.armorValues, + targetBot.internalValues, impactContext, ); - const previousInternalValues = [...botInternalValues]; + const previousInternalValues = [...targetBot.internalValues]; const damageResult = applyWeaponDamage( weaponSpec, shotDamage, @@ -10094,31 +10882,31 @@ export function handleCombatWeaponFireFrame( shot.targetAttach, shot.angleSeedA, shot.angleSeedB, - botArmorValues, - botInternalValues, - botHeadArmor, + targetBot.armorValues, + targetBot.internalValues, + targetBot.headArmor, ); const postDamageUpdates = collectPostDamageStateUpdates( botMechId, - botCriticalStateBytes, + targetBot.criticalStateBytes, previousInternalValues, - botInternalValues, + targetBot.internalValues, damageResult.headInternalDamaged, ); - botHeadArmor = damageResult.headArmor; + targetBot.headArmor = damageResult.headArmor; const allUpdates = [...damageResult.updates, ...postDamageUpdates.updates]; for (const update of allUpdates) { send( session.socket, - buildCmd66ActorDamagePacket(1, update.damageCode, update.damageValue, nextSeq(session)), + buildCmd66ActorDamagePacket(targetBot.slot, update.damageCode, update.damageValue, nextSeq(session)), capture, 'CMD66_BOT_DAMAGE', ); } - if (postDamageUpdates.newlyDestroyedLegs.length > 0 && !isActorDestroyed(botInternalValues)) { + if (postDamageUpdates.newlyDestroyedLegs.length > 0 && !isActorDestroyed(targetBot.internalValues)) { sendCombatLegLossCollapse( session, - 1, + targetBot.slot, connLog, `${postDamageUpdates.newlyDestroyedLegs.map(leg => leg.label).join('+')} destroyed`, 'CMD70_BOT_LEG_COLLAPSE', @@ -10127,42 +10915,48 @@ export function handleCombatWeaponFireFrame( ); } + targetBot.health = getCombatDurability(targetBot.armorValues, targetBot.internalValues) + targetBot.headArmor; totalDamageUpdates += allUpdates.length; shotSummaries.push( - `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:${summarizeHitSections(damageResult.hitSections)}:${shot.targetSlot}/${shot.targetAttach}:chance=${Math.round(hitRoll.chance * 100)}:roll=${Math.round(hitRoll.roll * 100)}:cross=${Math.round(hitRoll.crossingFactor * 100)}:bot=${botX}/${botY}:headArmor=${botHeadArmor}:updates=${allUpdates.map(update => `0x${update.damageCode.toString(16)}=${update.damageValue}`).join('/') || 'none'}${getModel13AttachProbeSuffix(botMechId, shot.targetAttach, impactContext)}`, + `${shot.weaponSlot}:${weaponName ?? 'unknown'}:${shotDamage}:${summarizeHitSections(damageResult.hitSections)}:slot=${targetBot.slot}:mech=${botMechId}:model=${botModelId ?? 'n/a'}:${shot.targetSlot}/${shot.targetAttach}:chance=${Math.round(hitRoll.chance * 100)}:roll=${Math.round(hitRoll.roll * 100)}:cross=${Math.round(hitRoll.crossingFactor * 100)}:bot=${targetX}/${targetY}:headArmor=${targetBot.headArmor}:updates=${allUpdates.map(update => `0x${update.damageCode.toString(16)}=${update.damageValue}`).join('/') || 'none'}${getModel13AttachProbeSuffix(botMechId, shot.targetAttach, impactContext)}`, ); + + if (isActorDestroyed(targetBot.internalValues)) { + targetBot.health = 0; + eliminatedBotSlots.add(targetBot.slot); + } } - session.combatBotArmorValues = botArmorValues; - session.combatBotInternalValues = botInternalValues; - session.combatBotCriticalStateBytes = botCriticalStateBytes; - session.combatBotHeadArmor = botHeadArmor; - session.botHealth = getCombatDurability(botArmorValues, botInternalValues); - session.botHealth += botHeadArmor; + syncLegacySoloCombatBotState(session); connLog.info( - '[world/combat] cmd10 weapon fire accepted: firePath=%s records=%d weaponSlots=%s botMechId=%s botModelId=%s botHealth=%d updates=%d shots=[%s]', + '[world/combat] cmd10 weapon fire accepted: firePath=%s records=%d weaponSlots=%s botHealth=%d updates=%d shots=[%s]', firePath, fireableShots.length, fireableShots.map(shot => shot.weaponSlot).join('/'), - botMechId ?? 'n/a', - botModelId ?? 'n/a', - session.botHealth, + session.botHealth ?? 0, totalDamageUpdates, shotSummaries.join(','), ); send(session.socket, buildCmd71ResetEffectStatePacket(nextSeq(session)), capture, 'CMD71_CLOSE'); - if (isActorDestroyed(botInternalValues)) { + for (const slot of eliminatedBotSlots) { + const bot = getSoloCombatBotBySlot(session, slot); + if (!bot) { + continue; + } + sendBotDeathTransition(session, bot, connLog, capture, 'fatal-damage'); + } + + if (getActiveHostileSoloCombatBots(session).length === 0) { session.botHealth = 0; stopBotCombatActions(session); - sendBotDeathTransition(session, connLog, capture, 'fatal-damage'); queueCombatResultTransition( players, session, connLog, capture, COMBAT_RESULT_VICTORY, - 'bot structurally destroyed', + 'all hostile solo-combat bots structurally destroyed', BOT_RESULT_DELAY_MS, ); } @@ -10363,16 +11157,33 @@ export function handleMechPickerCmd7( ): boolean { const clearMechPickerState = () => { session.mechPickerStep = undefined; + session.mechPickerTarget = undefined; + session.mechPickerTargetBotIndex = undefined; session.mechPickerClass = undefined; session.mechPickerChassis = undefined; session.mechPickerChassisPage = undefined; }; const step = session.mechPickerStep; + const pickerTarget = session.mechPickerTarget ?? 'player'; + const pickerBotIndex = Math.max(0, Math.min( + MAX_SINGLE_PLAYER_BOT_OPPONENTS - 1, + session.mechPickerTargetBotIndex ?? 0, + )); if (step === 'class' && listId === MECH_CLASS_LIST_ID) { if (selection <= 0) { clearMechPickerState(); - sendSceneRefresh(players, session, connLog, capture, 'Mech selection cancelled.'); + if (pickerTarget === 'bot') { + sendSceneRefresh( + players, + session, + connLog, + capture, + `Bot ${pickerBotIndex + 1} mech selection cancelled.`, + ); + } else { + sendSceneRefresh(players, session, connLog, capture, 'Mech selection cancelled.'); + } return true; } const classIndex = selection - 1; @@ -10383,7 +11194,12 @@ export function handleMechPickerCmd7( if (step === 'chassis' && listId === MECH_CHASSIS_LIST_ID) { if (selection <= 0) { - sendMechClassPicker(session, connLog, capture); + sendMechClassPicker( + session, + connLog, + capture, + pickerTarget === 'bot' ? { target: 'bot', botIndex: pickerBotIndex } : undefined, + ); return true; } const classIndex = session.mechPickerClass ?? 0; @@ -10391,7 +11207,12 @@ export function handleMechPickerCmd7( const visible = chassisList.slice(0, 20); const chassis = visible[selection - 1]; if (!chassis) { - sendMechClassPicker(session, connLog, capture); + sendMechClassPicker( + session, + connLog, + capture, + pickerTarget === 'bot' ? { target: 'bot', botIndex: pickerBotIndex } : undefined, + ); return true; } sendMechVariantPicker(session, chassis, connLog, capture); @@ -10413,7 +11234,36 @@ export function handleMechPickerCmd7( capture, 'CMD3_MECH_SELECT_ERR', ); - sendMechClassPicker(session, connLog, capture); + sendMechClassPicker( + session, + connLog, + capture, + pickerTarget === 'bot' ? { target: 'bot', botIndex: pickerBotIndex } : undefined, + ); + return true; + } + + if (pickerTarget === 'bot') { + setConfiguredBotLoadoutId(session, pickerBotIndex, chosen.id); + clearMechPickerState(); + worldResumeRegistry.save(session); + connLog.info( + '[world] bot mech selected: callsign="%s" slot=%d mech_id=%d typeString=%s', + getDisplayName(session), + pickerBotIndex + 1, + chosen.id, + chosen.typeString, + ); + send( + session.socket, + buildCmd3BroadcastPacket( + `Bot ${pickerBotIndex + 1} mech selected: ${chosen.typeString}. ${buildBotConfigurationSummary(session)}`, + nextSeq(session), + ), + capture, + 'CMD3_BOTMECH_SELECTED', + ); + send(session.socket, buildCmd5CursorNormalPacket(nextSeq(session)), capture, 'CMD5_NORMAL'); return true; } diff --git a/src/world/world-scene.ts b/src/world/world-scene.ts index 73496be..70b654e 100644 --- a/src/world/world-scene.ts +++ b/src/world/world-scene.ts @@ -1012,8 +1012,16 @@ export function sendMechClassPicker( session: ClientSession, connLog: Logger, capture: CaptureLogger, + options: { + target?: 'player' | 'bot'; + botIndex?: number; + } = {}, ): void { session.mechPickerStep = 'class'; + session.mechPickerTarget = options.target ?? 'player'; + session.mechPickerTargetBotIndex = session.mechPickerTarget === 'bot' + ? Math.max(0, Math.min(6, options.botIndex ?? 0)) + : undefined; session.mechPickerClass = undefined; session.mechPickerChassis = undefined; session.mechPickerChassisPage = undefined; @@ -1039,7 +1047,13 @@ export function sendMechClassPicker( ammoBinTypeIds: [], }; }); - connLog.info('[world] sending mech class picker'); + connLog.info( + '[world] sending mech class picker: target=%s%s', + session.mechPickerTarget, + session.mechPickerTarget === 'bot' + ? ` index=${(session.mechPickerTargetBotIndex ?? 0) + 1}` + : '', + ); send( session.socket, buildMechListPacket(entries, MECH_CLASS_LIST_ID, MECH_CLASS_FOOTER, nextSeq(session)), From ad3a2420ab835d31536f962ff8a5e1de3b3bc525 Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 17:38:35 -0400 Subject: [PATCH 09/14] =?UTF-8?q?feat(api):=20M6=20arena=20queue=20?= =?UTF-8?q?=E2=80=94=20GET/POST/DELETE=20/arena/queue=20+=20PATCH=20/arena?= =?UTF-8?q?/ready?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/world/arena-queue.ts: in-memory ArenaQueue - join(): derives mechId from DB caller, resets ready=false on mech change - leave(), setReady(), isAllReady(), recordLaunch() - pendingMatch persists after launch for WS reconnect recovery - clearPendingMatch() called on new queue cycle (new player joins) - api.ts: 4 new endpoints - GET /arena/queue: returns slots + pendingMatch snapshot - POST /arena/queue (X-Username): derives mech from character DB; clears stale pendingMatch - DELETE /arena/queue (X-Username): removes player from queue - PATCH /arena/ready (X-Username + {ready:bool}): sets readiness; launches when all ready - solo (1 player) or pvp (2+); broadcasts arena_match_launch; records pendingMatch - CORS updated to include DELETE - ws_broadcaster.ts: send arena_queue_update + arena_match_launch snapshots on new WS connect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 155 ++++++++++++++++++++++++++++++++---- src/world/arena-queue.ts | 122 ++++++++++++++++++++++++++++ src/world/ws_broadcaster.ts | 18 ++++- 3 files changed, 278 insertions(+), 17 deletions(-) create mode 100644 src/world/arena-queue.ts diff --git a/src/api.ts b/src/api.ts index 80dc9bf..8ada6f7 100644 --- a/src/api.ts +++ b/src/api.ts @@ -5,21 +5,35 @@ * Godot 4 client. The ARIES TCP protocol (ports 2000/2001) is unaffected. * * Endpoints: - * GET /health → { ok: true, version, name } - * GET /mechs → { ok: true, mechs: MechApiEntry[] } - * GET /world/rooms → { ok: true, rooms: WorldRoom[], source_available: boolean } - * POST /world/travel → { ok: true, room: WorldRoom | null } - * Body: { roomId: number } - * Header: X-Username (authenticated display name) - * GET /world/presence → { ok: true, rooms: Array<{ roomId, occupants: string[] }> } - * POST /world/chat → { ok: true } - * Body: { roomId: number, text: string (max 200 chars) } - * Header: X-Username - * Broadcasts room_chat WebSocket event to all clients - * PATCH /world/mech/select → { ok: true, mechId, typeString, slot } - * Body: { mechId: number } - * Header: X-Username - * WS /ws → real-time push: presence_update, room_chat events + * GET /health → { ok: true, version, name } + * GET /mechs → { ok: true, mechs: MechApiEntry[] } + * GET /world/rooms → { ok: true, rooms: WorldRoom[], source_available: boolean } + * POST /world/travel → { ok: true, room: WorldRoom | null } + * Body: { roomId: number } + * Header: X-Username (authenticated display name) + * GET /world/presence → { ok: true, rooms: Array<{ roomId, occupants: string[] }> } + * POST /world/chat → { ok: true } + * Body: { roomId: number, text: string (max 200 chars) } + * Header: X-Username + * Broadcasts room_chat WebSocket event to all clients + * PATCH /world/mech/select → { ok: true, mechId, typeString, slot } + * Body: { mechId: number } + * Header: X-Username + * GET /arena/queue → { ok: true, slots: ArenaSlot[], pendingMatch } + * POST /arena/queue → { ok: true, slot: ArenaSlot } + * Header: X-Username + * Derives mech from character DB; clears any pendingMatch + * Broadcasts arena_queue_update + * DELETE /arena/queue → { ok: true } + * Header: X-Username + * Broadcasts arena_queue_update + * PATCH /arena/ready → { ok: true, launched?: boolean, arenaId?: string } + * Body: { ready: boolean } + * Header: X-Username + * Broadcasts arena_queue_update; if all ready: + * broadcasts arena_match_launch + records pendingMatch + * WS /ws → real-time push: presence_update, room_chat, + * arena_queue_update, arena_match_launch events */ import * as http from 'http'; @@ -31,6 +45,8 @@ import { MECH_STATS } from './data/mech-stats.js'; import { findCharacterByDisplayName, updateCharacterMech } from './db/characters.js'; import { presenceStore } from './world/presence.js'; import { wsBroadcaster } from './world/ws_broadcaster.js'; +import { arenaQueue } from './world/arena-queue.js'; +import { randomUUID } from 'crypto'; const _pkg = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf8'), @@ -38,7 +54,7 @@ const _pkg = JSON.parse( function setCors(res: http.ServerResponse): void { res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, OPTIONS'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Username'); } @@ -255,6 +271,113 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se return; } + if (req.method === 'GET' && pathname === '/arena/queue') { + jsonOk(res, { + ok: true, + slots: arenaQueue.getAll(), + pendingMatch: arenaQueue.pendingMatch, + }); + return; + } + + if (req.method === 'POST' && pathname === '/arena/queue') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { + jsonError(res, 400, 'X-Username header required'); + return; + } + const character = await findCharacterByDisplayName(username); + if (!character) { + jsonError(res, 404, 'character not found'); + return; + } + const mechId: number = character.mech_id ?? -1; + if (mechId < 0) { + jsonError(res, 400, 'No mech selected — visit the Mech Bay first'); + return; + } + const mechEntry = WORLD_MECHS.find((m) => m.id === mechId); + const typeString = mechEntry?.typeString ?? ''; + // Clear any stale pending match when a new queue cycle begins. + arenaQueue.clearPendingMatch(); + const slot = arenaQueue.join(username, mechId, typeString); + apiLog.info('%s joined arena queue (mech=%s id=%d)', username, typeString, mechId); + wsBroadcaster.broadcast('arena_queue_update', { slots: arenaQueue.getAll() }); + jsonOk(res, { ok: true, slot }); + return; + } + + if (req.method === 'DELETE' && pathname === '/arena/queue') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { + jsonError(res, 400, 'X-Username header required'); + return; + } + const removed = arenaQueue.leave(username); + if (removed) { + apiLog.info('%s left arena queue', username); + wsBroadcaster.broadcast('arena_queue_update', { slots: arenaQueue.getAll() }); + } + jsonOk(res, { ok: true }); + return; + } + + if (req.method === 'PATCH' && pathname === '/arena/ready') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { + jsonError(res, 400, 'X-Username header required'); + return; + } + let body: string; + try { + body = await readBody(req); + } catch { + jsonError(res, 400, 'failed to read request body'); + return; + } + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch { + jsonError(res, 400, 'invalid JSON body'); + return; + } + const ready = + parsed !== null && + typeof parsed === 'object' && + typeof (parsed as Record).ready === 'boolean' + ? ((parsed as Record).ready as boolean) + : null; + if (ready === null) { + jsonError(res, 400, 'ready must be a boolean'); + return; + } + const slot = arenaQueue.setReady(username, ready); + if (!slot) { + jsonError(res, 404, 'not in arena queue — join first'); + return; + } + apiLog.info('%s arena ready=%s', username, ready); + wsBroadcaster.broadcast('arena_queue_update', { slots: arenaQueue.getAll() }); + + if (arenaQueue.isAllReady()) { + const arenaId = randomUUID(); + const match = arenaQueue.recordLaunch(arenaId); + apiLog.info('arena match launching: id=%s players=%d', arenaId, match.slots.length); + wsBroadcaster.broadcast('arena_match_launch', { + arenaId: match.arenaId, + slots: match.slots, + launchedAt: match.launchedAt, + mode: match.slots.length === 1 ? 'solo' : 'pvp', + }); + jsonOk(res, { ok: true, launched: true, arenaId: match.arenaId }); + return; + } + + jsonOk(res, { ok: true, launched: false }); + return; + } + res.writeHead(404); res.end(); }); diff --git a/src/world/arena-queue.ts b/src/world/arena-queue.ts new file mode 100644 index 0000000..11cd0b8 --- /dev/null +++ b/src/world/arena-queue.ts @@ -0,0 +1,122 @@ +/** + * In-memory arena staging queue. + * + * Tracks which players are waiting for an arena match, their selected mech, + * and their ready status. Not persisted to DB; clears on server restart. + * + * The queue uses the player's display_name (X-Username) as the identifier, + * consistent with the rest of the mpbt-server REST API. + * + * Launch semantics: + * - Any queue size ≥ 1 may launch when all slots are ready. + * - A single-player launch is a solo-vs-bot practice match. + * - A multi-player launch is a PvP match. + * - After launch, pendingMatch is set so late-joining WS clients can + * recover the launch event; the queue is cleared. + */ + +export interface ArenaSlot { + username: string; + mechId: number; + typeString: string; + joinedAt: number; + ready: boolean; +} + +export interface PendingMatch { + arenaId: string; + slots: ArenaSlot[]; + launchedAt: number; +} + +class ArenaQueue { + private _slots: ArenaSlot[] = []; + private _pendingMatch: PendingMatch | null = null; + + /** + * Join the queue. If the player is already in the queue, their mech is + * updated and ready is reset to false (mech change voids readiness). + */ + join(username: string, mechId: number, typeString: string): ArenaSlot { + const existing = this._slots.find((s) => s.username === username); + if (existing) { + if (existing.mechId !== mechId || existing.typeString !== typeString) { + existing.mechId = mechId; + existing.typeString = typeString; + existing.ready = false; + } + return existing; + } + const slot: ArenaSlot = { + username, + mechId, + typeString, + joinedAt: Date.now(), + ready: false, + }; + this._slots.push(slot); + return slot; + } + + /** Remove the player from the queue. Returns true if they were queued. */ + leave(username: string): boolean { + const idx = this._slots.findIndex((s) => s.username === username); + if (idx < 0) return false; + this._slots.splice(idx, 1); + return true; + } + + /** + * Set the ready state for a player. + * Returns the updated slot, or null if the player is not in the queue. + */ + setReady(username: string, ready: boolean): ArenaSlot | null { + const slot = this._slots.find((s) => s.username === username); + if (!slot) return null; + slot.ready = ready; + return slot; + } + + getAll(): ArenaSlot[] { + return [...this._slots]; + } + + getSlot(username: string): ArenaSlot | undefined { + return this._slots.find((s) => s.username === username); + } + + /** True when there is at least one queued player and all are ready. */ + isAllReady(): boolean { + return this._slots.length > 0 && this._slots.every((s) => s.ready); + } + + /** + * Record a pending match launch. Clears the queue so players can re-queue + * after the match; pendingMatch is retained for WS reconnect recovery. + */ + recordLaunch(arenaId: string): PendingMatch { + const match: PendingMatch = { + arenaId, + slots: [...this._slots], + launchedAt: Date.now(), + }; + this._pendingMatch = match; + this._slots = []; + return match; + } + + /** + * The most recently launched match, if any. Cleared when a new player + * joins (i.e. after the match ends and players return to queue). + */ + get pendingMatch(): PendingMatch | null { + return this._pendingMatch; + } + + /** Clear a stale pending match (e.g. when a new queue cycle begins). */ + clearPendingMatch(): void { + this._pendingMatch = null; + } +} + +export const arenaQueue = new ArenaQueue(); diff --git a/src/world/ws_broadcaster.ts b/src/world/ws_broadcaster.ts index 9471f99..8ecb205 100644 --- a/src/world/ws_broadcaster.ts +++ b/src/world/ws_broadcaster.ts @@ -11,6 +11,7 @@ import { WebSocketServer, WebSocket } from 'ws'; import * as http from 'http'; import { presenceStore } from './presence.js'; +import { arenaQueue } from './arena-queue.js'; export class WsBroadcaster { private readonly _wss = new WebSocketServer({ noServer: true }); @@ -32,10 +33,25 @@ export class WsBroadcaster { }); this._wss.on('connection', (ws) => { - // Send immediate presence snapshot so the client has current state. + // Send immediate snapshots so late-joiners/re-connectors get current state. ws.send( JSON.stringify({ type: 'presence_update', rooms: presenceStore.getAll() }), ); + ws.send( + JSON.stringify({ type: 'arena_queue_update', slots: arenaQueue.getAll() }), + ); + const pm = arenaQueue.pendingMatch; + if (pm) { + ws.send( + JSON.stringify({ + type: 'arena_match_launch', + arenaId: pm.arenaId, + slots: pm.slots, + launchedAt: pm.launchedAt, + mode: pm.slots.length === 1 ? 'solo' : 'pvp', + }), + ); + } }); } From c11652fbc770cb9f05086363b6ca271e266cafa1 Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 20:30:40 -0400 Subject: [PATCH 10/14] =?UTF-8?q?feat(api):=20M7=20WS=20combat=20protocol?= =?UTF-8?q?=20=E2=80=94=20combat-ws=20session=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CombatWsManager (src/world/combat-ws.ts): - startSession(): creates actors (human + bot), pre-join 30s timeout - _handleJoin(): slot validation, WS registration, tick start on all-joined - _tick(): bot AI (pursuit+fire at 10Hz), heat regen, snapshot broadcast - _handleFire(): cooldown 1500ms, heat gate, range 500m, bearing ±45° check - _handleLeave()/WS-close: surrender → _endSession() with 10s GC - Extend WsBroadcaster: registerMessageHandler() + incoming WS message routing - Extend api.ts: combatWsManager.init() + startSession() on arena launch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 6 +- src/world/combat-ws.ts | 431 ++++++++++++++++++++++++++++++++++++ src/world/ws_broadcaster.ts | 18 ++ 3 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 src/world/combat-ws.ts diff --git a/src/api.ts b/src/api.ts index 8ada6f7..54c5906 100644 --- a/src/api.ts +++ b/src/api.ts @@ -46,6 +46,7 @@ import { findCharacterByDisplayName, updateCharacterMech } from './db/characters import { presenceStore } from './world/presence.js'; import { wsBroadcaster } from './world/ws_broadcaster.js'; import { arenaQueue } from './world/arena-queue.js'; +import { combatWsManager } from './world/combat-ws.js'; import { randomUUID } from 'crypto'; const _pkg = JSON.parse( @@ -364,12 +365,14 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se const arenaId = randomUUID(); const match = arenaQueue.recordLaunch(arenaId); apiLog.info('arena match launching: id=%s players=%d', arenaId, match.slots.length); + const mode = match.slots.length === 1 ? 'solo' : 'pvp'; wsBroadcaster.broadcast('arena_match_launch', { arenaId: match.arenaId, slots: match.slots, launchedAt: match.launchedAt, - mode: match.slots.length === 1 ? 'solo' : 'pvp', + mode, }); + combatWsManager.startSession(match.arenaId, mode, match.slots); jsonOk(res, { ok: true, launched: true, arenaId: match.arenaId }); return; } @@ -383,6 +386,7 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se }); wsBroadcaster.attach(server); + combatWsManager.init(wsBroadcaster); server.on('error', (err: Error) => { apiLog.error('HTTP server error: %s', err.message); diff --git a/src/world/combat-ws.ts b/src/world/combat-ws.ts new file mode 100644 index 0000000..6828589 --- /dev/null +++ b/src/world/combat-ws.ts @@ -0,0 +1,431 @@ +/** + * CombatWsManager — manages in-memory combat sessions for the Godot client's + * modern JSON-over-WebSocket combat protocol. + * + * Sessions are created when the arena queue fires all-ready (via api.ts) and + * cleaned up when the match ends, times out, or all players disconnect. + * + * Security: + * - combat_join validates the username is in the session's slot list. + * - First WS to claim a slot wins; subsequent claims with an open WS are + * rejected, preventing socket hijacking. + * - Server derives winner/loser from session state, never from the payload. + * + * Bot AI (solo mode): + * - Pursues the human player at BOT_SPEED_FRACTION of max speed. + * - Fires every BOT_FIRE_INTERVAL_MS. + * - Stops pursuing when either actor health ≤ 0. + * + * Fire validation (human player only): + * - Cooldown: min PLAYER_FIRE_COOLDOWN_MS between shots. + * - Heat gate: blocked when heat ≥ HEAT_OVERHEAT_THRESHOLD. + * - Range: target must be within MAX_FIRE_RANGE_M. + * - Arc: target must be within ±45° of the attacker's heading. + * + * Coordinate convention (matches Godot 4 with default mech orientation): + * - Forward vector from heading h = (-sin(h), 0, -cos(h)). + * - Bot heading is set so that its forward vector aims at the player. + */ + +import { WebSocket } from 'ws'; +import { Logger } from '../util/logger.js'; +import type { WsBroadcaster } from './ws_broadcaster.js'; +import type { ArenaSlot } from './arena-queue.js'; + +const log = new Logger('combat-ws'); + +// ─── Tuning constants ───────────────────────────────────────────────────────── + +const TICK_MS = 100; +const BOT_FIRE_INTERVAL_MS = 3000; +const HEAT_REGEN_PER_TICK = 1.5; +const WEAPON_HEAT = 10; +const HEAT_OVERHEAT_THRESHOLD = 80; +const BOT_DAMAGE = 8; +const PLAYER_DAMAGE = 10; +const SPAWN_Z = 100; // ±Z from origin (m) +const BOT_MAX_SPEED_KPH = 64.8; // Locust default +const BOT_SPEED_FRACTION = 0.5; +const BOT_MIN_ENGAGE_DIST = 20; // stop closing in within 20 m +const PLAYER_FIRE_COOLDOWN_MS = 1500; +const MAX_FIRE_RANGE_M = 500; +const FIRE_BEARING_COS = Math.cos(Math.PI / 4); // ±45 ° +const PRE_JOIN_TIMEOUT_MS = 30_000; +const SESSION_GC_DELAY_MS = 10_000; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface CombatActor { + username: string; + isBot: boolean; + ws: WebSocket | null; // null for bots; set on combat_join for humans + x: number; + z: number; + heading: number; // radians, Y-axis rotation + health: number; // 0–100 + heat: number; // 0–100 + maxSpeedKph: number; + typeString: string; + lastFireTime: number; + claimed: boolean; // true once a WS has joined for this slot +} + +interface CombatSession { + arenaId: string; + mode: 'solo' | 'pvp'; + actors: Map; + tick: number; + state: 'waiting' | 'active' | 'ended'; + tickTimer: ReturnType | null; + preJoinTimer: ReturnType | null; + botLastFireTime: number; + humanJoinedCount: number; + expectedHumanCount: number; +} + +// ─── Manager ────────────────────────────────────────────────────────────────── + +export class CombatWsManager { + private readonly _sessions = new Map(); + + init(broadcaster: WsBroadcaster): void { + broadcaster.registerMessageHandler((ws, raw) => this._handleRaw(ws, raw)); + } + + /** + * Create a new combat session immediately after arena_match_launch. + * Slots contain all human players; the bot is added automatically for solo. + */ + startSession(arenaId: string, mode: 'solo' | 'pvp', slots: ArenaSlot[]): void { + if (this._sessions.has(arenaId)) { + log.warn('startSession called for existing arenaId=%s — ignored', arenaId); + return; + } + + const actors = new Map(); + + slots.forEach((slot, i) => { + actors.set(slot.username, { + username: slot.username, + isBot: false, + ws: null, + x: 0, + z: i === 0 ? -SPAWN_Z : SPAWN_Z, + heading: i === 0 ? 0 : Math.PI, // face each other + health: 100, + heat: 0, + maxSpeedKph: 64.8, // TODO: derive from mech stats + typeString: slot.typeString, + lastFireTime: 0, + claimed: false, + }); + }); + + if (mode === 'solo') { + actors.set('BOT', { + username: 'BOT', + isBot: true, + ws: null, + x: 0, + z: SPAWN_Z, + heading: Math.PI, // faces toward player spawn (z = -SPAWN_Z) + health: 100, + heat: 0, + maxSpeedKph: BOT_MAX_SPEED_KPH, + typeString: 'Locust', + lastFireTime: 0, + claimed: true, + }); + } + + const preJoinTimer = setTimeout(() => { + const session = this._sessions.get(arenaId); + if (!session || session.state !== 'waiting') return; + log.warn('arena %s: pre-join timeout — cancelling session', arenaId); + this._endSession(session, null, null); + }, PRE_JOIN_TIMEOUT_MS); + + const session: CombatSession = { + arenaId, + mode, + actors, + tick: 0, + state: 'waiting', + tickTimer: null, + preJoinTimer, + botLastFireTime: Date.now() + 2000, // 2 s grace before first bot shot + humanJoinedCount: 0, + expectedHumanCount: slots.length, + }; + + this._sessions.set(arenaId, session); + log.info('combat session created: arenaId=%s mode=%s players=%d', arenaId, mode, slots.length); + } + + // ─── Incoming message dispatch ───────────────────────────────────────────── + + private _handleRaw(ws: WebSocket, raw: string): void { + let msg: unknown; + try { + msg = JSON.parse(raw); + } catch { + return; + } + if (msg === null || typeof msg !== 'object') return; + + const m = msg as Record; + const type = typeof m['type'] === 'string' ? m['type'] : ''; + + if (!['combat_join', 'combat_input', 'combat_fire', 'combat_leave'].includes(type)) return; + + const arenaId = typeof m['arenaId'] === 'string' ? m['arenaId'] : ''; + if (!arenaId) return; + + // Username is validated against the slot list — not used for routing. + const username = typeof m['username'] === 'string' ? m['username'] : ''; + if (!username) return; + + switch (type) { + case 'combat_join': this._handleJoin(ws, arenaId, username); break; + case 'combat_input': this._handleInput(arenaId, username, m); break; + case 'combat_fire': this._handleFire(arenaId, username); break; + case 'combat_leave': this._handleLeave(arenaId, username); break; + } + } + + private _handleJoin(ws: WebSocket, arenaId: string, username: string): void { + const session = this._sessions.get(arenaId); + if (!session) { + log.warn('combat_join: unknown session arenaId=%s username=%s', arenaId, username); + return; + } + if (session.state === 'ended') return; + + const actor = session.actors.get(username); + if (!actor || actor.isBot) { + log.warn('combat_join: username=%s not a human slot in arenaId=%s', username, arenaId); + return; + } + + // Reject if another open connection already holds this slot. + if (actor.claimed && actor.ws && actor.ws.readyState === WebSocket.OPEN) { + log.warn('combat_join: slot already claimed username=%s arenaId=%s', username, arenaId); + return; + } + + actor.ws = ws; + actor.claimed = true; + + // Disconnect = surrender. + ws.once('close', () => { + log.info('WS closed during combat: username=%s arenaId=%s', username, arenaId); + this._handleLeave(arenaId, username); + }); + + session.humanJoinedCount++; + log.info('combat_join: username=%s arenaId=%s (%d/%d)', + username, arenaId, session.humanJoinedCount, session.expectedHumanCount); + + // Send immediate snapshot so client can initialise from server state. + this._sendToActor(actor, 'combat_snapshot', this._buildSnapshot(session)); + + // Start the tick loop when all expected humans have joined. + if (session.humanJoinedCount >= session.expectedHumanCount && session.state === 'waiting') { + if (session.preJoinTimer) { + clearTimeout(session.preJoinTimer); + session.preJoinTimer = null; + } + session.state = 'active'; + session.tickTimer = setInterval(() => this._tick(session), TICK_MS); + log.info('combat session active: arenaId=%s', arenaId); + } + } + + private _handleInput(arenaId: string, username: string, m: Record): void { + const session = this._sessions.get(arenaId); + if (!session || session.state !== 'active') return; + + const actor = session.actors.get(username); + if (!actor || actor.isBot) return; + + const x = typeof m['x'] === 'number' ? m['x'] : null; + const z = typeof m['z'] === 'number' ? m['z'] : null; + const heading = typeof m['heading'] === 'number' ? m['heading'] : null; + + if (x !== null) actor.x = x; + if (z !== null) actor.z = z; + if (heading !== null) actor.heading = heading; + } + + private _handleFire(arenaId: string, username: string): void { + const session = this._sessions.get(arenaId); + if (!session || session.state !== 'active') return; + + const attacker = session.actors.get(username); + if (!attacker || attacker.isBot) return; + + const now = Date.now(); + if (now - attacker.lastFireTime < PLAYER_FIRE_COOLDOWN_MS) return; + if (attacker.heat >= HEAT_OVERHEAT_THRESHOLD) return; + + // Any other actor is the target. + const target = Array.from(session.actors.values()).find(a => a.username !== username); + if (!target) return; + + const dx = target.x - attacker.x; + const dz = target.z - attacker.z; + const dist = Math.hypot(dx, dz); + + if (dist > MAX_FIRE_RANGE_M) return; + + // Forward vector from heading h = (-sin(h), 0, -cos(h)). + const fwdX = -Math.sin(attacker.heading); + const fwdZ = -Math.cos(attacker.heading); + const dot = (fwdX * dx + fwdZ * dz) / (dist || 1); + if (dot < FIRE_BEARING_COS) return; + + attacker.lastFireTime = now; + attacker.heat = Math.min(100, attacker.heat + WEAPON_HEAT); + target.health = Math.max(0, target.health - PLAYER_DAMAGE); + + this._broadcastToSession(session, 'combat_hit', { + arenaId, + attacker: username, + target: target.username, + damage: PLAYER_DAMAGE, + health: target.health, + }); + + if (target.health <= 0) { + this._endSession(session, username, target.username); + } + } + + private _handleLeave(arenaId: string, username: string): void { + const session = this._sessions.get(arenaId); + if (!session || session.state === 'ended') return; + + log.info('combat_leave/surrender: username=%s arenaId=%s', username, arenaId); + + // Leaving player loses; the other human (or BOT) wins. + const winner = Array.from(session.actors.values()) + .find(a => a.username !== username && !a.isBot)?.username ?? 'BOT'; + + this._endSession(session, winner, username); + } + + // ─── Tick loop ───────────────────────────────────────────────────────────── + + private _tick(session: CombatSession): void { + if (session.state !== 'active') return; + + const botActor = session.actors.get('BOT'); + const humanActors = Array.from(session.actors.values()).filter(a => !a.isBot); + const primaryHuman = humanActors[0]; + + if (botActor && primaryHuman) { + const dx = primaryHuman.x - botActor.x; + const dz = primaryHuman.z - botActor.z; + const dist = Math.hypot(dx, dz); + + if (dist > BOT_MIN_ENGAGE_DIST) { + const speed = (botActor.maxSpeedKph * BOT_SPEED_FRACTION / 3.6) * (TICK_MS / 1000); + // Set heading so that forward vector (-sin(h), 0, -cos(h)) = normalize(dx, dz). + botActor.heading = Math.atan2(-dx, -dz); + botActor.x += (dx / dist) * speed; + botActor.z += (dz / dist) * speed; + } + + const now = Date.now(); + if (now - session.botLastFireTime >= BOT_FIRE_INTERVAL_MS) { + session.botLastFireTime = now; + primaryHuman.health = Math.max(0, primaryHuman.health - BOT_DAMAGE); + + this._broadcastToSession(session, 'combat_hit', { + arenaId: session.arenaId, + attacker: 'BOT', + target: primaryHuman.username, + damage: BOT_DAMAGE, + health: primaryHuman.health, + }); + + if (primaryHuman.health <= 0) { + this._endSession(session, 'BOT', primaryHuman.username); + return; + } + } + } + + for (const actor of session.actors.values()) { + actor.heat = Math.max(0, actor.heat - HEAT_REGEN_PER_TICK); + } + + this._broadcastToSession(session, 'combat_snapshot', this._buildSnapshot(session)); + session.tick++; + } + + // ─── Helpers ─────────────────────────────────────────────────────────────── + + private _buildSnapshot(session: CombatSession): object { + return { + arenaId: session.arenaId, + tick: session.tick, + actors: Array.from(session.actors.values()).map(a => ({ + username: a.username, + x: a.x, + z: a.z, + heading: a.heading, + health: a.health, + heat: a.heat, + isBot: a.isBot, + typeString: a.typeString, + })), + }; + } + + private _endSession( + session: CombatSession, + winner: string | null, + loser: string | null, + ): void { + if (session.state === 'ended') return; + session.state = 'ended'; + + if (session.tickTimer) { + clearInterval(session.tickTimer); + session.tickTimer = null; + } + if (session.preJoinTimer) { + clearTimeout(session.preJoinTimer); + session.preJoinTimer = null; + } + + log.info('combat ended: arenaId=%s winner=%s loser=%s', session.arenaId, winner, loser); + + this._broadcastToSession(session, 'combat_end', { + arenaId: session.arenaId, + winner, + loser, + mode: session.mode, + }); + + setTimeout(() => this._sessions.delete(session.arenaId), SESSION_GC_DELAY_MS); + } + + private _broadcastToSession(session: CombatSession, type: string, data: object): void { + const msg = JSON.stringify({ type, ...data }); + for (const actor of session.actors.values()) { + if (!actor.isBot && actor.ws && actor.ws.readyState === WebSocket.OPEN) { + actor.ws.send(msg); + } + } + } + + private _sendToActor(actor: CombatActor, type: string, data: object): void { + if (actor.ws && actor.ws.readyState === WebSocket.OPEN) { + actor.ws.send(JSON.stringify({ type, ...data })); + } + } +} + +export const combatWsManager = new CombatWsManager(); diff --git a/src/world/ws_broadcaster.ts b/src/world/ws_broadcaster.ts index 8ecb205..a785723 100644 --- a/src/world/ws_broadcaster.ts +++ b/src/world/ws_broadcaster.ts @@ -16,6 +16,16 @@ import { arenaQueue } from './arena-queue.js'; export class WsBroadcaster { private readonly _wss = new WebSocketServer({ noServer: true }); private _attached = false; + private readonly _messageHandlers: Array<(ws: WebSocket, raw: string) => void> = []; + + /** + * Register a handler that is called for every incoming WebSocket message. + * Multiple handlers may be registered; each receives the same (ws, raw) pair. + * Handlers must not throw — errors are swallowed to protect the WS loop. + */ + registerMessageHandler(handler: (ws: WebSocket, raw: string) => void): void { + this._messageHandlers.push(handler); + } attach(server: http.Server): void { if (this._attached) return; @@ -52,6 +62,14 @@ export class WsBroadcaster { }), ); } + + // Route incoming messages to registered handlers. + ws.on('message', (data) => { + const raw = Buffer.isBuffer(data) ? data.toString('utf8') : String(data); + for (const handler of this._messageHandlers) { + try { handler(ws, raw); } catch { /* handler errors must not crash the WS loop */ } + } + }); }); } From 51c38452f639f0096084e42937577121b7d61984 Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Tue, 28 Apr 2026 20:44:32 -0400 Subject: [PATCH 11/14] feat(api): M8 standings + character endpoint + PvP result persistence - GET /standings returns top-50 Solaris VII leaderboard via computeSolarisStandings() - GET /world/character returns cbills/allegiance/mechId/mechSlot for X-Username header - combat-ws.ts: persist PvP duel results to duel_results on match end - Only persists when session was active (prevents pre-start forfeit pollution) - createDuelResult ON CONFLICT idempotency; settleDuelStakeTransfer only on fresh insert - Stake capped to loser's actual balance (Math.min(250, loserChar.cbills)) - mechId added to CombatActor interface Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index 54c5906..2b425bc 100644 --- a/src/api.ts +++ b/src/api.ts @@ -19,6 +19,9 @@ * PATCH /world/mech/select → { ok: true, mechId, typeString, slot } * Body: { mechId: number } * Header: X-Username + * GET /standings → { ok: true, standings: SolarisStanding[] (top 50) } + * GET /world/character → { ok: true, cbills, allegiance, mechId, mechSlot } + * Header: X-Username * GET /arena/queue → { ok: true, slots: ArenaSlot[], pendingMatch } * POST /arena/queue → { ok: true, slot: ArenaSlot } * Header: X-Username @@ -42,7 +45,9 @@ import { Logger } from './util/logger.js'; import { loadSolarisRooms } from './data/maps.js'; import { WORLD_MECHS } from './world/world-data.js'; import { MECH_STATS } from './data/mech-stats.js'; -import { findCharacterByDisplayName, updateCharacterMech } from './db/characters.js'; +import { findCharacterByDisplayName, updateCharacterMech, listCharacters } from './db/characters.js'; +import { listAllDuelResults } from './db/duel-results.js'; +import { computeSolarisStandings } from './world/solaris-rankings.js'; import { presenceStore } from './world/presence.js'; import { wsBroadcaster } from './world/ws_broadcaster.js'; import { arenaQueue } from './world/arena-queue.js'; @@ -272,6 +277,49 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se return; } + if (req.method === 'GET' && pathname === '/standings') { + const [results, chars] = await Promise.all([ + listAllDuelResults(), + listCharacters(), + ]); + const standings = computeSolarisStandings(results, chars).slice(0, 50); + jsonOk(res, { + ok: true, + standings: standings.map(s => ({ + overallRank: s.overallRank, + displayName: s.displayName, + allegiance: s.allegiance, + wins: s.wins, + losses: s.losses, + ratioText: s.ratioText, + tierLabel: s.tierLabel, + score: s.score, + })), + }); + return; + } + + if (req.method === 'GET' && pathname === '/world/character') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { + jsonError(res, 400, 'X-Username header required'); + return; + } + const character = await findCharacterByDisplayName(username); + if (!character) { + jsonError(res, 404, 'character not found'); + return; + } + jsonOk(res, { + ok: true, + cbills: character.cbills, + allegiance: character.allegiance, + mechId: character.mech_id, + mechSlot: character.mech_slot, + }); + return; + } + if (req.method === 'GET' && pathname === '/arena/queue') { jsonOk(res, { ok: true, From 9c371362745d2c0845a6d8298a72cefd0fc9a6eb Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Wed, 29 Apr 2026 00:35:23 -0400 Subject: [PATCH 12/14] =?UTF-8?q?feat(comstar):=20M13=20ComStar=20backend?= =?UTF-8?q?=20=E2=80=94=20comstar=5Fmodern=20table,=20DB=20helpers,=205=20?= =?UTF-8?q?REST=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comstar_modern table (separate from ARIES messages table) - DB helpers: sendComstarModern, listInbox, countUnread, markReadById, softDelete - REST: GET /comstar/unread, GET /comstar, POST /comstar, PATCH /comstar/:id/read, DELETE /comstar/:id - WS broadcast sends only { to: displayName } — no private content leaked Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/api.ts | 112 ++++++++++++++++++++++++++++++++++++- src/db/comstar_modern.ts | 117 +++++++++++++++++++++++++++++++++++++++ src/db/schema.sql | 20 +++++++ 3 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 src/db/comstar_modern.ts diff --git a/src/api.ts b/src/api.ts index 2b425bc..03137c1 100644 --- a/src/api.ts +++ b/src/api.ts @@ -35,8 +35,21 @@ * Header: X-Username * Broadcasts arena_queue_update; if all ready: * broadcasts arena_match_launch + records pendingMatch + * GET /comstar → { ok: true, messages: ComstarApiMessage[], unreadCount } + * Header: X-Username + * GET /comstar/unread → { ok: true, count: number } + * Header: X-Username + * POST /comstar → { ok: true } + * Body: { to, subject (max 100), body (max 1000) } + * Header: X-Username + * Broadcasts comstar_new_message { to } to all WS clients + * PATCH /comstar/:id/read → { ok: true } or 404 + * Header: X-Username + * DELETE /comstar/:id → { ok: true } or 404 + * Header: X-Username * WS /ws → real-time push: presence_update, room_chat, - * arena_queue_update, arena_match_launch events + * arena_queue_update, arena_match_launch, + * comstar_new_message events */ import * as http from 'http'; @@ -53,6 +66,13 @@ import { wsBroadcaster } from './world/ws_broadcaster.js'; import { arenaQueue } from './world/arena-queue.js'; import { combatWsManager } from './world/combat-ws.js'; import { randomUUID } from 'crypto'; +import { + listInbox, + countUnread, + sendComstarModern, + markReadById, + softDelete, +} from './db/comstar_modern.js'; const _pkg = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf8'), @@ -429,6 +449,96 @@ export function startApiServer(log: Logger, host: string, port: number): http.Se return; } + // ── ComStar ────────────────────────────────────────────────────────────── + + if (req.method === 'GET' && pathname === '/comstar/unread') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { jsonError(res, 400, 'X-Username header required'); return; } + const character = await findCharacterByDisplayName(username); + if (!character) { jsonError(res, 404, 'character not found'); return; } + const count = await countUnread(character.account_id); + jsonOk(res, { ok: true, count }); + return; + } + + if (req.method === 'GET' && pathname === '/comstar') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { jsonError(res, 400, 'X-Username header required'); return; } + const character = await findCharacterByDisplayName(username); + if (!character) { jsonError(res, 404, 'character not found'); return; } + const rows = await listInbox(character.account_id); + const unreadCount = rows.filter((r) => r.read_at === null).length; + jsonOk(res, { + ok: true, + unreadCount, + messages: rows.map((r) => ({ + id: r.id, + from: r.from_name, + subject: r.subject, + body: r.body, + sentAt: r.sent_at.toISOString(), + readAt: r.read_at?.toISOString() ?? null, + })), + }); + return; + } + + if (req.method === 'POST' && pathname === '/comstar') { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { jsonError(res, 400, 'X-Username header required'); return; } + let rawBody: string; + try { rawBody = await readBody(req); } catch { jsonError(res, 400, 'failed to read body'); return; } + let parsed: unknown; + try { parsed = JSON.parse(rawBody); } catch { jsonError(res, 400, 'invalid JSON body'); return; } + const p = parsed as Record; + const to = typeof p?.to === 'string' ? (p.to as string).trim() : ''; + const subject = typeof p?.subject === 'string' ? (p.subject as string).trim() : ''; + const body = typeof p?.body === 'string' ? (p.body as string).trim() : ''; + if (!to) { jsonError(res, 400, 'to is required'); return; } + if (subject.length > 100) { jsonError(res, 400, 'subject must be 100 characters or fewer'); return; } + if (!body) { jsonError(res, 400, 'body is required'); return; } + if (body.length > 1000) { jsonError(res, 400, 'body must be 1000 characters or fewer'); return; } + if (to.toLowerCase() === username.toLowerCase()) { jsonError(res, 400, 'cannot send to yourself'); return; } + const sender = await findCharacterByDisplayName(username); + if (!sender) { jsonError(res, 404, 'sender character not found'); return; } + const recipient = await findCharacterByDisplayName(to); + if (!recipient) { jsonError(res, 404, 'recipient not found'); return; } + const msg = await sendComstarModern(sender.account_id, recipient.account_id, username, subject, body); + if (!msg) { jsonError(res, 500, 'failed to send message'); return; } + wsBroadcaster.broadcast('comstar_new_message', { to: recipient.display_name }); + apiLog.info('comstar: %s → %s subject=%s', username, to, subject.slice(0, 40)); + jsonOk(res, { ok: true }); + return; + } + + { + const comstarReadMatch = /^\/comstar\/(\d+)\/read$/.exec(pathname); + if (req.method === 'PATCH' && comstarReadMatch) { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { jsonError(res, 400, 'X-Username header required'); return; } + const character = await findCharacterByDisplayName(username); + if (!character) { jsonError(res, 404, 'character not found'); return; } + const updated = await markReadById(Number(comstarReadMatch[1]), character.account_id); + if (!updated) { jsonError(res, 404, 'message not found'); return; } + jsonOk(res, { ok: true }); + return; + } + } + + { + const comstarDeleteMatch = /^\/comstar\/(\d+)$/.exec(pathname); + if (req.method === 'DELETE' && comstarDeleteMatch) { + const username = (req.headers['x-username'] ?? '') as string; + if (!username) { jsonError(res, 400, 'X-Username header required'); return; } + const character = await findCharacterByDisplayName(username); + if (!character) { jsonError(res, 404, 'character not found'); return; } + const deleted = await softDelete(Number(comstarDeleteMatch[1]), character.account_id); + if (!deleted) { jsonError(res, 404, 'message not found'); return; } + jsonOk(res, { ok: true }); + return; + } + } + res.writeHead(404); res.end(); }); diff --git a/src/db/comstar_modern.ts b/src/db/comstar_modern.ts new file mode 100644 index 0000000..9e80730 --- /dev/null +++ b/src/db/comstar_modern.ts @@ -0,0 +1,117 @@ +/** + * ComStar modern-client message persistence. + * + * Uses the separate `comstar_modern` table so there is zero overlap with the + * ARIES-oriented `messages` table consumed by the retail client adapter. + */ + +import { pool } from './client.js'; + +export interface ComstarModernRow { + id: number; + from_account_id: number; + to_account_id: number; + from_name: string; + subject: string; + body: string; + sent_at: Date; + read_at: Date | null; + deleted_at: Date | null; +} + +/** Maximum inbox depth returned per request. */ +export const COMSTAR_INBOX_LIMIT = 50; + +/** + * Insert a new message. Returns the inserted row or null when the sender or + * recipient account is invalid (FK constraint violation). + */ +export async function sendComstarModern( + fromAccountId: number, + toAccountId: number, + fromName: string, + subject: string, + body: string, +): Promise { + try { + const res = await pool.query( + `INSERT INTO comstar_modern + (from_account_id, to_account_id, from_name, subject, body) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, from_account_id, to_account_id, from_name, + subject, body, sent_at, read_at, deleted_at`, + [fromAccountId, toAccountId, fromName, subject, body], + ); + return res.rows[0] ?? null; + } catch { + return null; + } +} + +/** + * List the inbox for a recipient — non-deleted rows, newest-first, capped at + * COMSTAR_INBOX_LIMIT. + */ +export async function listInbox(toAccountId: number): Promise { + const res = await pool.query( + `SELECT id, from_account_id, to_account_id, from_name, + subject, body, sent_at, read_at, deleted_at + FROM comstar_modern + WHERE to_account_id = $1 AND deleted_at IS NULL + ORDER BY sent_at DESC, id DESC + LIMIT $2`, + [toAccountId, COMSTAR_INBOX_LIMIT], + ); + return res.rows; +} + +/** + * Count unread (non-deleted) messages for a recipient. + */ +export async function countUnread(toAccountId: number): Promise { + const res = await pool.query<{ count: string }>( + `SELECT COUNT(*)::text AS count + FROM comstar_modern + WHERE to_account_id = $1 + AND read_at IS NULL + AND deleted_at IS NULL`, + [toAccountId], + ); + return Number(res.rows[0]?.count ?? '0'); +} + +/** + * Mark a message as read. Only updates if the caller is the recipient. + * Returns true when a row was updated, false when not found / wrong owner. + */ +export async function markReadById( + messageId: number, + toAccountId: number, +): Promise { + const res = await pool.query( + `UPDATE comstar_modern + SET read_at = COALESCE(read_at, now()) + WHERE id = $1 AND to_account_id = $2 AND deleted_at IS NULL + RETURNING id`, + [messageId, toAccountId], + ); + return (res.rowCount ?? 0) > 0; +} + +/** + * Soft-delete a message. Only the recipient may delete. + * Returns true when a row was updated, false when not found / wrong owner. + */ +export async function softDelete( + messageId: number, + toAccountId: number, +): Promise { + const res = await pool.query( + `UPDATE comstar_modern + SET deleted_at = COALESCE(deleted_at, now()) + WHERE id = $1 AND to_account_id = $2 + RETURNING id`, + [messageId, toAccountId], + ); + return (res.rowCount ?? 0) > 0; +} diff --git a/src/db/schema.sql b/src/db/schema.sql index f252952..0812e87 100644 --- a/src/db/schema.sql +++ b/src/db/schema.sql @@ -142,3 +142,23 @@ CREATE INDEX IF NOT EXISTS duel_results_winner_account_idx CREATE INDEX IF NOT EXISTS duel_results_loser_account_idx ON duel_results (loser_account_id, completed_at DESC); + +-- comstar_modern: private messages sent between players via the modern REST API. +-- Separate from the legacy `messages` table (ARIES/retail client) to avoid any +-- interference with ARIES delivery semantics. +CREATE TABLE IF NOT EXISTS comstar_modern ( + id SERIAL PRIMARY KEY, + from_account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + to_account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + from_name VARCHAR(64) NOT NULL, -- display_name snapshot at send time + subject VARCHAR(100) NOT NULL DEFAULT '', + body TEXT NOT NULL, + sent_at TIMESTAMPTZ NOT NULL DEFAULT now(), + read_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ +); + +-- Fast inbox lookup (all non-deleted messages for a recipient, newest first). +CREATE INDEX IF NOT EXISTS comstar_modern_inbox_idx + ON comstar_modern (to_account_id, sent_at DESC) + WHERE deleted_at IS NULL; From 58a526beff346451655652dd7a33e78eb9ffbd9c Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Wed, 29 Apr 2026 23:08:06 -0400 Subject: [PATCH 13/14] Persist duel results and document isolated map rooms Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/world/combat-ws.ts | 78 ++++++++++++++++++++++++++++++++++++++++++ world-map.json | 5 ++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/world/combat-ws.ts b/src/world/combat-ws.ts index 6828589..f234b71 100644 --- a/src/world/combat-ws.ts +++ b/src/world/combat-ws.ts @@ -31,6 +31,11 @@ import { WebSocket } from 'ws'; import { Logger } from '../util/logger.js'; import type { WsBroadcaster } from './ws_broadcaster.js'; import type { ArenaSlot } from './arena-queue.js'; +import { + findCharacterByDisplayName, + settleDuelStakeTransfer, +} from '../db/characters.js'; +import { createDuelResult } from '../db/duel-results.js'; const log = new Logger('combat-ws'); @@ -66,6 +71,7 @@ interface CombatActor { heat: number; // 0–100 maxSpeedKph: number; typeString: string; + mechId: number; // 0 for BOT lastFireTime: number; claimed: boolean; // true once a WS has joined for this slot } @@ -116,6 +122,7 @@ export class CombatWsManager { heat: 0, maxSpeedKph: 64.8, // TODO: derive from mech stats typeString: slot.typeString, + mechId: slot.mechId, lastFireTime: 0, claimed: false, }); @@ -133,6 +140,7 @@ export class CombatWsManager { heat: 0, maxSpeedKph: BOT_MAX_SPEED_KPH, typeString: 'Locust', + mechId: 0, lastFireTime: 0, claimed: true, }); @@ -389,6 +397,8 @@ export class CombatWsManager { loser: string | null, ): void { if (session.state === 'ended') return; + + const wasActive = session.state === 'active'; session.state = 'ended'; if (session.tickTimer) { @@ -409,9 +419,77 @@ export class CombatWsManager { mode: session.mode, }); + // Only persist ranked results for PvP matches that reached active state. + if (wasActive && session.mode === 'pvp' && winner && loser && winner !== 'BOT' && loser !== 'BOT') { + this._persistResult(session, winner, loser).catch(err => + log.error('failed to persist combat result arenaId=%s: %s', session.arenaId, err), + ); + } + setTimeout(() => this._sessions.delete(session.arenaId), SESSION_GC_DELAY_MS); } + private async _persistResult( + session: CombatSession, + winner: string, + loser: string, + ): Promise { + const [winnerChar, loserChar] = await Promise.all([ + findCharacterByDisplayName(winner), + findCharacterByDisplayName(loser), + ]); + if (!winnerChar || !loserChar) { + log.warn('_persistResult: character not found for arenaId=%s winner=%s loser=%s', session.arenaId, winner, loser); + return; + } + + const winnerActor = session.actors.get(winner); + const loserActor = session.actors.get(loser); + if (!winnerActor || !loserActor) return; + + const STAKE_CB = 250; + const safeStake = Math.min(STAKE_CB, loserChar.cbills); + + // ON CONFLICT DO NOTHING makes the insert idempotent; only settle cbills + // when the row is freshly inserted to avoid double-transfers on replay. + const inserted = await createDuelResult({ + combatSessionId: session.arenaId, + worldMapRoomId: 0, + roomName: 'Arena', + winnerAccountId: winnerChar.account_id, + loserAccountId: loserChar.account_id, + winnerDisplayName: winner, + loserDisplayName: loser, + winnerComstarId: 100000 + winnerChar.account_id, + loserComstarId: 100000 + loserChar.account_id, + winnerMechId: winnerActor.mechId, + loserMechId: loserActor.mechId, + winnerStakeCb: safeStake, + loserStakeCb: safeStake, + winnerRemainingHealth: winnerActor.health, + winnerMaxHealth: 100, + loserRemainingHealth: 0, + loserMaxHealth: 100, + resultReason: 'combat_ws_defeat', + }); + + if (!inserted) { + // Row already existed — skip settlement to prevent double transfer. + log.warn('_persistResult: duplicate insert skipped for arenaId=%s', session.arenaId); + return; + } + + if (safeStake > 0) { + try { + await settleDuelStakeTransfer(winnerChar.account_id, loserChar.account_id, safeStake); + log.info('cbills settled: arenaId=%s winner=%s +%d loser=%s -%d', + session.arenaId, winner, safeStake, loser, safeStake); + } catch (err) { + log.error('cbills settlement failed arenaId=%s: %s', session.arenaId, err); + } + } + } + private _broadcastToSession(session: CombatSession, type: string, data: object): void { const msg = JSON.stringify({ type, ...data }); for (const actor of session.actors.values()) { diff --git a/world-map.json b/world-map.json index ca32807..df07372 100644 --- a/world-map.json +++ b/world-map.json @@ -1,5 +1,8 @@ { "_comment": "Reconstructed from SOLARIS.MAP binary and retail in-room screenshots. Synthetic IDs 10001-10027 = sub-rooms; 10028-10062 = stub/terminal nodes. Do not edit manually.", + "_exitNotes": [ + "Any room whose exits.north, exits.south, exits.east, and exits.west are all null is effectively an island; sending a player there will leave them stuck unless some other non-exit travel path moves them out." + ], "_sources": [ "SOLARIS.MAP", "Solaris_Bitmap screenshots" @@ -2209,4 +2212,4 @@ "_screenshot": null } ] -} \ No newline at end of file +} From 792461412161e27d3cb8f7c6fd281483f5472a24 Mon Sep 17 00:00:00 2001 From: Mike Babcock Date: Wed, 29 Apr 2026 23:15:31 -0400 Subject: [PATCH 14/14] Remove generated server logs from branch tip Keep the new client API/WebSocket merge clean by excluding local runtime log artifacts before promoting the branch to master. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mpbt-server.err.log | 2 - mpbt-server.out.log | 1728 ------------------------------------------- 2 files changed, 1730 deletions(-) delete mode 100644 mpbt-server.err.log delete mode 100644 mpbt-server.out.log diff --git a/mpbt-server.err.log b/mpbt-server.err.log deleted file mode 100644 index a6435bb..0000000 --- a/mpbt-server.err.log +++ /dev/null @@ -1,2 +0,0 @@ -[world] loaded 32 rooms from SOLARIS.MAP -[world] loaded world-map.json (183 rooms) diff --git a/mpbt-server.out.log b/mpbt-server.out.log deleted file mode 100644 index 700a74c..0000000 --- a/mpbt-server.out.log +++ /dev/null @@ -1,1728 +0,0 @@ -2026-04-27T09:46:00.836Z INFO [server] Loaded 161 mechs from mechdata/ -2026-04-27T09:46:00.840Z INFO [server] [world] ══════════════════════════════════════════════ -2026-04-27T09:46:00.841Z INFO [server] [world] Game World Server (M3) -2026-04-27T09:46:00.841Z INFO [server] [world] Listening on 0.0.0.0:2001 -2026-04-27T09:46:00.841Z INFO [server] [world] CRC seed: 0x0A5C25 (RPS / MMW path) -2026-04-27T09:46:00.842Z INFO [server] [world] ══════════════════════════════════════════════ -2026-04-27T09:46:00.842Z INFO [server] ═══════════════════════════════════════════════════════ -2026-04-27T09:46:00.842Z INFO [server] MPBT ARIES Server Emulator -2026-04-27T09:46:00.842Z INFO [server] Listening on 0.0.0.0:2000 -2026-04-27T09:46:00.844Z INFO [server] Hostname: Moozeo -2026-04-27T09:46:00.845Z INFO [server] Protocol: ARIES binary (12-byte header, confirmed by RE) -2026-04-27T09:46:00.845Z INFO [server] play.pcgi server=127.0.0.1:2000 -2026-04-27T09:46:00.845Z INFO [server] Log level: DEBUG → logs/server.log -2026-04-27T09:46:00.845Z INFO [server] Packet captures: enabled → captures/ -2026-04-27T09:46:00.846Z INFO [server] ═══════════════════════════════════════════════════════ -2026-04-27T09:46:20.218Z INFO [server:fdbf716d] Client connected from 192.168.1.109:62916 (session fdbf716d-c4e4-4f0a-8e73-242c1907ee6c) -2026-04-27T09:46:20.219Z INFO [server:fdbf716d] Sending LOGIN_REQUEST (0x16) — 12 bytes -2026-04-27T09:46:20.222Z DEBUG [server:fdbf716d] recv 346 bytes (total=346, phase=auth) -2026-04-27T09:46:20.222Z DEBUG [server:fdbf716d] pkt type=0x15 tag=0x0 payloadLen=334 -2026-04-27T09:46:20.223Z DEBUG [server:fdbf716d] [rx] -00000000 4d 6f 6f 73 65 00 00 00 00 00 00 00 00 00 00 00 |Moose...........| -00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 45 6e 67 69 6e |Kesmai CommEngin| -00000080 65 20 33 2e 32 39 00 00 00 00 00 00 00 00 00 00 |e 3.29..........| -00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000e0 00 00 00 00 00 00 00 00 67 00 00 00 00 00 00 00 |........g.......| -000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 42 |..............9B| -00000140 00 00 00 09 31 32 4d 50 42 54 34 75 2e 00 |....12MPBT4u..| -2026-04-27T09:46:20.224Z DEBUG [server:fdbf716d] [auth] login payload len=334 -00000000 4d 6f 6f 73 65 00 00 00 00 00 00 00 00 00 00 00 |Moose...........| -00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 45 6e 67 69 6e |Kesmai CommEngin| -00000080 65 20 33 2e 32 39 00 00 00 00 00 00 00 00 00 00 |e 3.29..........| -00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000e0 00 00 00 00 00 00 00 00 67 00 00 00 00 00 00 00 |........g.......| -000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 42 |..............9B| -00000140 00 00 00 09 31 32 4d 50 42 54 34 75 2e 00 |....12MPBT4u..| -2026-04-27T09:46:20.225Z INFO [server:fdbf716d] [auth] login from "Moose" ver="Kesmai CommEngine 3.29" service="g" pwLen=9 -2026-04-27T09:46:20.226Z INFO [server:fdbf716d] [login] credentials received: user="Moose" service="g" clientVer="Kesmai CommEngine 3.29" -2026-04-27T09:46:20.564Z INFO [server:fdbf716d] [login] authenticated: user="Moose" (id=15) -2026-04-27T09:46:20.565Z INFO [server:fdbf716d] [login] sending SYNC ack — 12 bytes -2026-04-27T09:46:20.565Z INFO [server:fdbf716d] [login] sending WELCOME escape — 45 bytes -2026-04-27T09:46:20.575Z DEBUG [server:fdbf716d] recv 23 bytes (total=369, phase=lobby) -2026-04-27T09:46:20.576Z DEBUG [server:fdbf716d] pkt type=0x00 tag=0x0 payloadLen=11 -2026-04-27T09:46:20.576Z DEBUG [server:fdbf716d] [rx] -00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| -2026-04-27T09:46:20.577Z DEBUG [server:fdbf716d] [game] rx type-0 len=11 -00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| -2026-04-27T09:46:20.578Z DEBUG [server:fdbf716d] [game] client seq=0 cmd=3 phase=lobby -2026-04-27T09:46:20.583Z INFO [server:fdbf716d] [game] character found: displayName="Moose" allegiance=Marik mech_id=98 mech_slot=36 → REDIRECT to world -2026-04-27T09:46:20.583Z INFO [server:fdbf716d] [game] recorded world launch: displayName="Moose" allegiance=Marik mech=CPLT-C1 (id=98) -2026-04-27T09:46:20.584Z INFO [server:fdbf716d] [game] sending REDIRECT → 192.168.1.109:2001 -2026-04-27T09:46:20.585Z INFO [server:fdbf716d] Client disconnected (phase=closing, bytes=369) -2026-04-27T09:46:20.591Z INFO [server:f38959b6] [world] client connected from 192.168.1.109:62918 (session f38959b6-6677-4338-b7ea-b9602eb33eb0) -2026-04-27T09:46:20.591Z INFO [server:f38959b6] [world] sending LOGIN_REQUEST (12 bytes) -2026-04-27T09:46:20.592Z DEBUG [server:f38959b6] [world] recv 337 bytes (total=337, phase=auth) -2026-04-27T09:46:20.593Z DEBUG [server:f38959b6] [world] pkt type=0x15 tag=0x0 payloadLen=325 -2026-04-27T09:46:20.593Z DEBUG [server:f38959b6] [world][rx] -00000000 4d 6f 6f 73 65 00 00 00 00 00 00 00 00 00 00 00 |Moose...........| -00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 45 6e 67 69 6e |Kesmai CommEngin| -00000080 65 20 33 2e 32 39 00 00 00 00 00 00 00 00 00 00 |e 3.29..........| -00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000e0 00 00 00 00 00 00 00 00 31 39 32 2e 31 36 38 2e |........192.168.| -000000f0 31 2e 31 30 39 3a 32 30 30 31 00 00 00 00 00 00 |1.109:2001......| -00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 42 |..............9B| -00000140 00 00 00 00 00 |.....| -2026-04-27T09:46:20.594Z DEBUG [server:f38959b6] [auth] login payload len=325 -00000000 4d 6f 6f 73 65 00 00 00 00 00 00 00 00 00 00 00 |Moose...........| -00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 45 6e 67 69 6e |Kesmai CommEngin| -00000080 65 20 33 2e 32 39 00 00 00 00 00 00 00 00 00 00 |e 3.29..........| -00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000e0 00 00 00 00 00 00 00 00 31 39 32 2e 31 36 38 2e |........192.168.| -000000f0 31 2e 31 30 39 3a 32 30 30 31 00 00 00 00 00 00 |1.109:2001......| -00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 42 |..............9B| -00000140 00 00 00 00 00 |.....| -2026-04-27T09:46:20.595Z INFO [server:f38959b6] [auth] login from "Moose" ver="Kesmai CommEngine 3.29" service="192.168.1.109:2001" pwLen=0 -2026-04-27T09:46:20.595Z INFO [server:f38959b6] [world-login] launch record found: displayName="Moose" allegiance=Marik mech=CPLT-C1 (id=98 slot=36 rosterId=100015 room=146) -2026-04-27T09:46:20.595Z INFO [server:f38959b6] [world-login] accepted: user="Moose" displayName="Moose" allegiance=Marik service="192.168.1.109:2001" -2026-04-27T09:46:20.596Z INFO [server:f38959b6] [world-login] sending SYNC ack -2026-04-27T09:46:20.596Z INFO [server:f38959b6] [world-login] sending WELCOME escape (45 bytes) -2026-04-27T09:46:20.598Z DEBUG [server:f38959b6] [world] recv 23 bytes (total=360, phase=world) -2026-04-27T09:46:20.598Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=11 -2026-04-27T09:46:20.598Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| -2026-04-27T09:46:20.599Z DEBUG [server:f38959b6] [world] rx type-0 len=11 -00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| -2026-04-27T09:46:20.599Z DEBUG [server:f38959b6] [world] client seq=0 cmd=3 -2026-04-27T09:46:20.600Z INFO [server:f38959b6] [world] cmd-3 (client-ready) → sending world init sequence -2026-04-27T09:46:20.600Z INFO [server:f38959b6] [world] sending Cmd4 SceneInit (logicalRoom=146 sceneRoom=146 header="Solaris Starport" detail="Click on the stone arch icon to the east for a tutorial if you are new to Solaris City and need help. Click on the tram icon to the south if you know your way around and you're ready to go find a fight. Remember players with [MPBT] in front of their names are the game staff. They will help to teach you the game. Other players that want to train you or that want you to join their group may not have the best intentions.") -2026-04-27T09:46:20.601Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) -2026-04-27T09:46:20.602Z INFO [server:f38959b6] [world] world init sequence complete -2026-04-27T09:46:20.603Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" -2026-04-27T09:46:29.537Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=380, phase=world) -2026-04-27T09:46:29.537Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:46:29.537Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 26 2c 2c 65 4c 1b |.!&,,eL.| -2026-04-27T09:46:29.537Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 26 2c 2c 65 4c 1b |.!&,,eL.| -2026-04-27T09:46:29.538Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 -2026-04-27T09:46:29.538Z INFO [server:f38959b6] [world] cmd-5 scene action: type=11 -2026-04-27T09:46:29.538Z INFO [server:f38959b6] [world] sending Cmd43 Solaris travel map: logicalRoomId=146 sceneRoomId=146 -2026-04-27T09:46:30.480Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=406, phase=world) -2026-04-27T09:46:30.480Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:46:30.481Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2b 23 3d 21 21 21 22 74 41 5f 45 1b |.!+#=!!!"tA_E.| -2026-04-27T09:46:30.481Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 2b 23 3d 21 21 21 22 74 41 5f 45 1b |.!+#=!!!"tA_E.| -2026-04-27T09:46:30.481Z DEBUG [server:f38959b6] [world] client seq=0 cmd=10 -2026-04-27T09:46:30.482Z INFO [server:f38959b6] [world] cmd-10 map reply: context=198 selection=168 selectedRoomId=167 -2026-04-27T09:46:30.482Z INFO [server:f38959b6] [world] sending arena ready-room menu: arena=167 options=1 -2026-04-27T09:46:32.609Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=432, phase=world) -2026-04-27T09:46:32.701Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:46:32.701Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 71 21 21 21 21 22 43 31 4b 1b |.!(,q!!!!"C1K.| -2026-04-27T09:46:32.701Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 71 21 21 21 21 22 43 31 4b 1b |.!(,q!!!!"C1K.| -2026-04-27T09:46:32.701Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:46:32.702Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1015 selection=1 -2026-04-27T09:46:32.703Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" -2026-04-27T09:46:32.703Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" -2026-04-27T09:46:32.704Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) -2026-04-27T09:46:32.704Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" -2026-04-27T09:46:40.170Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=458, phase=world) -2026-04-27T09:46:40.170Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:46:40.170Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| -2026-04-27T09:46:40.171Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| -2026-04-27T09:46:40.171Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:46:50.597Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:46:50.598Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=470, phase=world) -2026-04-27T09:46:50.599Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:46:50.599Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:46:54.593Z DEBUG [server:f38959b6] [world] recv 31 bytes (total=501, phase=world) -2026-04-27T09:46:54.593Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=19 -2026-04-27T09:46:54.594Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2b 2f 62 6f 74 64 69 66 66 20 33 51 |.!%!+/botdiff 3Q| -00000010 54 50 1b |TP.| -2026-04-27T09:46:54.594Z DEBUG [server:f38959b6] [world] rx type-0 len=19 -00000000 1b 21 25 21 2b 2f 62 6f 74 64 69 66 66 20 33 51 |.!%!+/botdiff 3Q| -00000010 54 50 1b |TP.| -2026-04-27T09:46:54.594Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:46:54.595Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botdiff 3 -2026-04-27T09:46:54.596Z INFO [server:f38959b6] [world] /botdifficulty: configured bot difficulty=3 -2026-04-27T09:47:02.713Z DEBUG [server:f38959b6] [world] recv 32 bytes (total=533, phase=world) -2026-04-27T09:47:02.713Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=20 -2026-04-27T09:47:02.714Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2c 2f 62 6f 74 63 6f 75 6e 74 20 37 |.!%!,/botcount 7| -00000010 40 56 4f 1b |@VO.| -2026-04-27T09:47:02.714Z DEBUG [server:f38959b6] [world] rx type-0 len=20 -00000000 1b 21 25 21 2c 2f 62 6f 74 63 6f 75 6e 74 20 37 |.!%!,/botcount 7| -00000010 40 56 4f 1b |@VO.| -2026-04-27T09:47:02.714Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:47:02.715Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botcount 7 -2026-04-27T09:47:02.715Z INFO [server:f38959b6] [world] /botcount: configured 7 bot opponents -2026-04-27T09:47:20.599Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:47:20.600Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=545, phase=world) -2026-04-27T09:47:20.600Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:47:20.601Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:47:23.368Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=565, phase=world) -2026-04-27T09:47:23.368Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:47:23.369Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| -2026-04-27T09:47:23.369Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| -2026-04-27T09:47:23.369Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 -2026-04-27T09:47:23.369Z INFO [server:f38959b6] [world] cmd-5 scene action: type=10 -2026-04-27T09:47:23.370Z INFO [server:f38959b6] [world] sending arena status list (1 entries) -2026-04-27T09:47:27.057Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=591, phase=world) -2026-04-27T09:47:27.058Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:47:27.058Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:47:27.058Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:47:27.059Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:47:27.059Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 -2026-04-27T09:47:27.059Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 -2026-04-27T09:47:28.240Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=611, phase=world) -2026-04-27T09:47:28.240Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:47:28.241Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 26 2a 2c 65 34 1b |.!&*,e4.| -2026-04-27T09:47:28.241Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 26 2a 2c 65 34 1b |.!&*,e4.| -2026-04-27T09:47:28.241Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 -2026-04-27T09:47:28.242Z INFO [server:f38959b6] [world] cmd-5 scene action: type=9 -2026-04-27T09:47:28.242Z INFO [server:f38959b6] [world] sending arena side menu -2026-04-27T09:47:30.922Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=637, phase=world) -2026-04-27T09:47:30.922Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:47:30.923Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 70 21 21 21 21 22 43 2f 75 1b |.!(,p!!!!"C/u.| -2026-04-27T09:47:30.923Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 70 21 21 21 21 22 43 2f 75 1b |.!(,p!!!!"C/u.| -2026-04-27T09:47:30.923Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:47:30.923Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1014 selection=1 -2026-04-27T09:47:30.924Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" -2026-04-27T09:47:30.924Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) -2026-04-27T09:47:30.925Z INFO [server:f38959b6] [world] sending arena status list (1 entries) -2026-04-27T09:47:30.926Z INFO [server:f38959b6] [world] arena side selected: callsign="Moose" side=1 room=167 -2026-04-27T09:47:34.970Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=663, phase=world) -2026-04-27T09:47:34.970Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:47:34.970Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:47:34.971Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:47:34.971Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:47:34.971Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 -2026-04-27T09:47:34.971Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 -2026-04-27T09:47:38.304Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=689, phase=world) -2026-04-27T09:47:38.305Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:47:38.305Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| -2026-04-27T09:47:38.306Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| -2026-04-27T09:47:38.306Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:47:50.601Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:47:50.602Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=701, phase=world) -2026-04-27T09:47:50.603Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:47:50.603Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:48:13.385Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=734, phase=world) -2026-04-27T09:48:13.385Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:48:13.385Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 31 20 |.!%!-/botside 1 | -00000010 31 59 30 42 1b |1Y0B.| -2026-04-27T09:48:13.386Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 31 20 |.!%!-/botside 1 | -00000010 31 59 30 42 1b |1Y0B.| -2026-04-27T09:48:13.386Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:48:13.386Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 1 1 -2026-04-27T09:48:13.387Z INFO [server:f38959b6] [world] /botside: slot=1 configured side=1 -2026-04-27T09:48:20.607Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:48:20.608Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=746, phase=world) -2026-04-27T09:48:20.609Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:48:20.609Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:48:22.855Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=766, phase=world) -2026-04-27T09:48:22.856Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:48:22.857Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 38 23 2c 63 2a 1b |.!8#,c*.| -2026-04-27T09:48:22.857Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 38 23 2c 63 2a 1b |.!8#,c*.| -2026-04-27T09:48:22.857Z DEBUG [server:f38959b6] [world] client seq=0 cmd=23 -2026-04-27T09:48:22.858Z INFO [server:f38959b6] [world] cmd-23 location action: room=167 slot=2 cached=true -> room=2138 -2026-04-27T09:48:22.858Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" -2026-04-27T09:48:22.858Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=2138 sceneRoomId=169 header="Halloran St" -2026-04-27T09:48:22.859Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) -2026-04-27T09:48:22.859Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" -2026-04-27T09:48:23.701Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=786, phase=world) -2026-04-27T09:48:23.701Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:48:23.702Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 38 28 2c 62 2b 1b |.!8(,b+.| -2026-04-27T09:48:23.702Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 38 28 2c 62 2b 1b |.!8(,b+.| -2026-04-27T09:48:23.702Z DEBUG [server:f38959b6] [world] client seq=0 cmd=23 -2026-04-27T09:48:23.703Z INFO [server:f38959b6] [world] cmd-23 location action: room=2138 slot=3 cached=false -> room=167 -2026-04-27T09:48:23.703Z INFO [server:f38959b6] [world] sending arena ready-room menu: arena=167 options=1 -2026-04-27T09:48:25.641Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=812, phase=world) -2026-04-27T09:48:25.642Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:48:25.642Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 71 21 21 21 21 22 43 31 4b 1b |.!(,q!!!!"C1K.| -2026-04-27T09:48:25.642Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 71 21 21 21 21 22 43 31 4b 1b |.!(,q!!!!"C1K.| -2026-04-27T09:48:25.643Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:48:25.643Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1015 selection=1 -2026-04-27T09:48:25.643Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" -2026-04-27T09:48:25.643Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" -2026-04-27T09:48:25.644Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) -2026-04-27T09:48:25.644Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" -2026-04-27T09:48:28.465Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=838, phase=world) -2026-04-27T09:48:28.465Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:48:28.466Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| -2026-04-27T09:48:28.466Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| -2026-04-27T09:48:28.466Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:48:31.208Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=858, phase=world) -2026-04-27T09:48:31.209Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:48:31.209Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| -2026-04-27T09:48:31.209Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| -2026-04-27T09:48:31.210Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 -2026-04-27T09:48:31.210Z INFO [server:f38959b6] [world] cmd-5 scene action: type=10 -2026-04-27T09:48:31.210Z INFO [server:f38959b6] [world] sending arena status list (1 entries) -2026-04-27T09:48:35.945Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=884, phase=world) -2026-04-27T09:48:35.945Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:48:35.946Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:48:35.946Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:48:35.946Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:48:35.947Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 -2026-04-27T09:48:35.947Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 -2026-04-27T09:48:37.408Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=904, phase=world) -2026-04-27T09:48:37.408Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:48:37.408Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 26 2a 2c 65 34 1b |.!&*,e4.| -2026-04-27T09:48:37.409Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 26 2a 2c 65 34 1b |.!&*,e4.| -2026-04-27T09:48:37.409Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 -2026-04-27T09:48:37.409Z INFO [server:f38959b6] [world] cmd-5 scene action: type=9 -2026-04-27T09:48:37.409Z INFO [server:f38959b6] [world] sending arena side menu -2026-04-27T09:48:38.778Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=930, phase=world) -2026-04-27T09:48:38.778Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:48:38.778Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 70 21 21 21 21 22 43 2f 75 1b |.!(,p!!!!"C/u.| -2026-04-27T09:48:38.779Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 70 21 21 21 21 22 43 2f 75 1b |.!(,p!!!!"C/u.| -2026-04-27T09:48:38.779Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:48:38.779Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1014 selection=1 -2026-04-27T09:48:38.779Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" -2026-04-27T09:48:38.780Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) -2026-04-27T09:48:38.780Z INFO [server:f38959b6] [world] sending arena status list (1 entries) -2026-04-27T09:48:38.780Z INFO [server:f38959b6] [world] arena side selected: callsign="Moose" side=1 room=167 -2026-04-27T09:48:40.904Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=950, phase=world) -2026-04-27T09:48:40.904Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:48:40.905Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| -2026-04-27T09:48:40.905Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| -2026-04-27T09:48:40.905Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 -2026-04-27T09:48:40.905Z INFO [server:f38959b6] [world] cmd-5 scene action: type=10 -2026-04-27T09:48:40.906Z INFO [server:f38959b6] [world] sending arena status list (1 entries) -2026-04-27T09:48:42.969Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=976, phase=world) -2026-04-27T09:48:42.970Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:48:42.970Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:48:42.970Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:48:42.971Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:48:42.971Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 -2026-04-27T09:48:42.971Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 -2026-04-27T09:48:45.440Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=1002, phase=world) -2026-04-27T09:48:45.441Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:48:45.441Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| -2026-04-27T09:48:45.442Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 25 21 26 2f 68 65 6c 70 42 2b 4a 1b |.!%!&/helpB+J.| -2026-04-27T09:48:45.442Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:48:47.744Z DEBUG [server:f38959b6] [world] recv 31 bytes (total=1033, phase=world) -2026-04-27T09:48:47.745Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=19 -2026-04-27T09:48:47.745Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2b 2f 62 6f 74 64 69 66 66 20 33 51 |.!%!+/botdiff 3Q| -00000010 54 50 1b |TP.| -2026-04-27T09:48:47.745Z DEBUG [server:f38959b6] [world] rx type-0 len=19 -00000000 1b 21 25 21 2b 2f 62 6f 74 64 69 66 66 20 33 51 |.!%!+/botdiff 3Q| -00000010 54 50 1b |TP.| -2026-04-27T09:48:47.745Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:48:47.746Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botdiff 3 -2026-04-27T09:48:47.746Z INFO [server:f38959b6] [world] /botdifficulty: configured bot difficulty=3 -2026-04-27T09:48:50.614Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:48:50.615Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=1045, phase=world) -2026-04-27T09:48:50.615Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:48:50.615Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:48:56.874Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1078, phase=world) -2026-04-27T09:48:56.874Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:48:56.875Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 31 20 |.!%!-/botside 1 | -00000010 31 59 30 42 1b |1Y0B.| -2026-04-27T09:48:56.875Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 31 20 |.!%!-/botside 1 | -00000010 31 59 30 42 1b |1Y0B.| -2026-04-27T09:48:56.875Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:48:56.875Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 1 1 -2026-04-27T09:48:56.876Z INFO [server:f38959b6] [world] /botside: slot=1 configured side=1 -2026-04-27T09:48:58.921Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1111, phase=world) -2026-04-27T09:48:58.921Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:48:58.921Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 32 20 |.!%!-/botside 2 | -00000010 31 59 30 72 1b |1Y0r.| -2026-04-27T09:48:58.922Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 32 20 |.!%!-/botside 2 | -00000010 31 59 30 72 1b |1Y0r.| -2026-04-27T09:48:58.922Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:48:58.922Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 2 1 -2026-04-27T09:48:58.922Z INFO [server:f38959b6] [world] /botside: slot=2 configured side=1 -2026-04-27T09:49:01.384Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1144, phase=world) -2026-04-27T09:49:01.385Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:49:01.385Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 33 20 |.!%!-/botside 3 | -00000010 31 59 31 4d 1b |1Y1M.| -2026-04-27T09:49:01.386Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 33 20 |.!%!-/botside 3 | -00000010 31 59 31 4d 1b |1Y1M.| -2026-04-27T09:49:01.386Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:49:01.386Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 3 1 -2026-04-27T09:49:01.387Z INFO [server:f38959b6] [world] /botside: slot=3 configured side=1 -2026-04-27T09:49:05.224Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1177, phase=world) -2026-04-27T09:49:05.225Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:49:05.225Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 34 20 |.!%!-/botside 4 | -00000010 31 59 2f 67 1b |1Y/g.| -2026-04-27T09:49:05.225Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 34 20 |.!%!-/botside 4 | -00000010 31 59 2f 67 1b |1Y/g.| -2026-04-27T09:49:05.225Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:49:05.226Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 4 1 -2026-04-27T09:49:05.226Z INFO [server:f38959b6] [world] /botside: slot=4 configured side=1 -2026-04-27T09:49:09.544Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1210, phase=world) -2026-04-27T09:49:09.545Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:49:09.545Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 34 20 |.!%!-/botside 4 | -00000010 32 59 2f 73 1b |2Y/s.| -2026-04-27T09:49:09.546Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 34 20 |.!%!-/botside 4 | -00000010 32 59 2f 73 1b |2Y/s.| -2026-04-27T09:49:09.546Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:49:09.546Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 4 2 -2026-04-27T09:49:09.546Z INFO [server:f38959b6] [world] /botside: slot=4 configured side=2 -2026-04-27T09:49:12.953Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1243, phase=world) -2026-04-27T09:49:12.954Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:49:12.954Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 35 20 |.!%!-/botside 5 | -00000010 32 59 2f 43 1b |2Y/C.| -2026-04-27T09:49:12.954Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 35 20 |.!%!-/botside 5 | -00000010 32 59 2f 43 1b |2Y/C.| -2026-04-27T09:49:12.954Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:49:12.955Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 5 2 -2026-04-27T09:49:12.955Z INFO [server:f38959b6] [world] /botside: slot=5 configured side=2 -2026-04-27T09:49:16.128Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1276, phase=world) -2026-04-27T09:49:16.129Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:49:16.129Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 36 20 |.!%!-/botside 6 | -00000010 32 59 2e 68 1b |2Y.h.| -2026-04-27T09:49:16.129Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 36 20 |.!%!-/botside 6 | -00000010 32 59 2e 68 1b |2Y.h.| -2026-04-27T09:49:16.129Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:49:16.130Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 6 2 -2026-04-27T09:49:16.130Z INFO [server:f38959b6] [world] /botside: slot=6 configured side=2 -2026-04-27T09:49:18.280Z DEBUG [server:f38959b6] [world] recv 33 bytes (total=1309, phase=world) -2026-04-27T09:49:18.281Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=21 -2026-04-27T09:49:18.281Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 37 20 |.!%!-/botside 7 | -00000010 32 59 2f 23 1b |2Y/#.| -2026-04-27T09:49:18.281Z DEBUG [server:f38959b6] [world] rx type-0 len=21 -00000000 1b 21 25 21 2d 2f 62 6f 74 73 69 64 65 20 37 20 |.!%!-/botside 7 | -00000010 32 59 2f 23 1b |2Y/#.| -2026-04-27T09:49:18.281Z DEBUG [server:f38959b6] [world] client seq=0 cmd=4 -2026-04-27T09:49:18.282Z INFO [server:f38959b6] [world] cmd-4 text: Moose: /botside 7 2 -2026-04-27T09:49:18.282Z INFO [server:f38959b6] [world] /botside: slot=7 configured side=2 -2026-04-27T09:49:20.007Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=1329, phase=world) -2026-04-27T09:49:20.008Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:49:20.008Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| -2026-04-27T09:49:20.008Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 26 2b 2c 65 30 1b |.!&+,e0.| -2026-04-27T09:49:20.009Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 -2026-04-27T09:49:20.009Z INFO [server:f38959b6] [world] cmd-5 scene action: type=10 -2026-04-27T09:49:20.009Z INFO [server:f38959b6] [world] sending arena status list (1 entries) -2026-04-27T09:49:20.627Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:49:20.628Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=1341, phase=world) -2026-04-27T09:49:20.628Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:49:20.628Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:49:39.050Z DEBUG [server:f38959b6] [world] recv 26 bytes (total=1367, phase=world) -2026-04-27T09:49:39.050Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=14 -2026-04-27T09:49:39.051Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:49:39.051Z DEBUG [server:f38959b6] [world] rx type-0 len=14 -00000000 1b 21 28 2c 6f 21 21 21 21 21 43 6a 70 1b |.!(,o!!!!!Cjp.| -2026-04-27T09:49:39.051Z DEBUG [server:f38959b6] [world] client seq=0 cmd=7 -2026-04-27T09:49:39.051Z INFO [server:f38959b6] [world] world menu reply (cmd-7): listId=1013 selection=0 -2026-04-27T09:49:39.051Z DEBUG [server:f38959b6] [world] cmd-7 ignored: unsupported listId=1013 -2026-04-27T09:49:43.183Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=1387, phase=world) -2026-04-27T09:49:43.184Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T09:49:43.184Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 26 26 2c 65 64 1b |.!&&,ed.| -2026-04-27T09:49:43.184Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 26 26 2c 65 64 1b |.!&&,ed.| -2026-04-27T09:49:43.185Z DEBUG [server:f38959b6] [world] client seq=0 cmd=5 -2026-04-27T09:49:43.185Z INFO [server:f38959b6] [world] cmd-5 scene action: type=5 -2026-04-27T09:49:43.185Z INFO [server:f38959b6] [world] cmd-5 Fight button: triggering combat bootstrap room=167 -2026-04-27T09:49:43.186Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" -2026-04-27T09:49:43.187Z INFO [server:f38959b6] [world] delaying combat bootstrap by 4000ms so DROP can display -2026-04-27T09:49:43.212Z DEBUG [server:f38959b6] [world] recv 23 bytes (total=1410, phase=combat) -2026-04-27T09:49:43.213Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=11 -2026-04-27T09:49:43.213Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 24 22 3e 24 22 39 55 31 1b |.!$">$"9U1.| -2026-04-27T09:49:43.213Z DEBUG [server:f38959b6] [world] rx type-0 len=11 -00000000 1b 21 24 22 3e 24 22 39 55 31 1b |.!$">$"9U1.| -2026-04-27T09:49:43.214Z DEBUG [server:f38959b6] [world] client seq=0 cmd=3 -2026-04-27T09:49:43.214Z DEBUG [server:f38959b6] [world] duplicate cmd-3 after initialization — ignoring -2026-04-27T09:49:47.198Z INFO [server:f38959b6] [world] sending Cmd72 combat bootstrap (mech_id=98 callsign="Moose" type=CPLT-C1 allegiance=Marik) -2026-04-27T09:49:47.200Z INFO [server:f38959b6] [world] bot actor: slot=1 side=1 mech_id=98 type=CPLT-C1 x=-149400 y=-258768 -2026-04-27T09:49:47.200Z INFO [server:f38959b6] [world] bot actor: slot=2 side=1 mech_id=98 type=CPLT-C1 x=0 y=-298800 -2026-04-27T09:49:47.201Z INFO [server:f38959b6] [world] bot actor: slot=3 side=1 mech_id=98 type=CPLT-C1 x=149400 y=-258768 -2026-04-27T09:49:47.201Z INFO [server:f38959b6] [world] bot actor: slot=4 side=2 mech_id=98 type=CPLT-C1 x=166671 y=249441 -2026-04-27T09:49:47.202Z INFO [server:f38959b6] [world] bot actor: slot=5 side=2 mech_id=98 type=CPLT-C1 x=58527 y=294236 -2026-04-27T09:49:47.202Z INFO [server:f38959b6] [world] bot actor: slot=6 side=2 mech_id=98 type=CPLT-C1 x=-58527 y=294236 -2026-04-27T09:49:47.202Z INFO [server:f38959b6] [world] bot actor: slot=7 side=2 mech_id=98 type=CPLT-C1 x=-166671 y=249441 -2026-04-27T09:49:47.204Z INFO [server:f38959b6] [world] combat entry complete for "Moose" -2026-04-27T09:49:47.246Z DEBUG [server:f38959b6] [world] recv 19 bytes (total=1429, phase=combat) -2026-04-27T09:49:47.247Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=7 -2026-04-27T09:49:47.247Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 34 27 25 21 1b |.!4'%!.| -2026-04-27T09:49:47.247Z DEBUG [server:f38959b6] [world] rx type-0 len=7 -00000000 1b 21 34 27 25 21 1b |.!4'%!.| -2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world] client seq=0 cmd=19 -2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world/combat] inbound combat cmd=19 len=7 — not yet handled -2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1463, phase=combat) -2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:47.248Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4c 50 |.!)KKKKKKKK!!!LP| -00000010 4b 4b 21 2c 26 1b |KK!,&.| -2026-04-27T09:49:47.249Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4c 50 |.!)KKKKKKKK!!!LP| -00000010 4b 4b 21 2c 26 1b |KK!,&.| -2026-04-27T09:49:47.249Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:47.250Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3702 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:48.241Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1497, phase=combat) -2026-04-27T09:49:48.241Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:48.241Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:48.242Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:48.242Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:48.242Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:49.249Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1531, phase=combat) -2026-04-27T09:49:49.250Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:49.250Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:49.251Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:49.251Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:49.251Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:50.256Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1565, phase=combat) -2026-04-27T09:49:50.257Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:50.257Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:50.257Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:50.257Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:50.258Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:51.267Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1599, phase=combat) -2026-04-27T09:49:51.267Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:51.267Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:51.268Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:51.268Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:51.268Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:52.267Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1633, phase=combat) -2026-04-27T09:49:52.267Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:52.268Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:52.268Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:52.268Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:52.268Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:53.266Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1667, phase=combat) -2026-04-27T09:49:53.266Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:53.267Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:53.267Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:53.268Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:53.268Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:54.265Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1701, phase=combat) -2026-04-27T09:49:54.266Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:54.266Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:54.266Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:54.266Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:54.267Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:55.266Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1735, phase=combat) -2026-04-27T09:49:55.267Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:55.267Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:55.267Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:55.268Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:55.268Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:56.268Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1769, phase=combat) -2026-04-27T09:49:56.268Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:56.269Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:56.269Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:56.269Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:56.270Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:57.281Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1803, phase=combat) -2026-04-27T09:49:57.282Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:57.282Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:57.282Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:57.282Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:57.283Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:58.294Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1837, phase=combat) -2026-04-27T09:49:58.294Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:58.294Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:58.295Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 4b 4b 4b 4b 4b 21 21 21 4a 46 |.!)KKKKKKKK!!!JF| -00000010 4b 4b 21 24 6e 1b |KK!$n.| -2026-04-27T09:49:58.295Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:58.295Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=0 y=0 altitude=0 facingRaw=3522 clientSpeed=0 suppressing echo (stopped) -2026-04-27T09:49:59.302Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1871, phase=combat) -2026-04-27T09:49:59.302Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:49:59.303Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 4b 62 4b 4b 4b 4b 21 21 21 4a 4e |.!)KKKbKKKK!!!JN| -00000010 4d 72 35 47 2f 1b |Mr5G/.| -2026-04-27T09:49:59.303Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 4b 62 4b 4b 4b 4b 21 21 21 4a 4e |.!)KKKbKKKK!!!JN| -00000010 4d 72 35 47 2f 1b |Mr5G/.| -2026-04-27T09:49:59.303Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:49:59.304Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=23 y=0 altitude=0 facingRaw=3530 clientSpeed=209 effectiveSpeed=209 -> no echo (trust local key events) -2026-04-27T09:50:00.310Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1905, phase=combat) -2026-04-27T09:50:00.311Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:00.311Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 51 21 4b 4b 4c 4c 21 21 21 4a 67 |.!)KKQ!KKLL!!!Jg| -00000010 53 72 66 31 6c 1b |Srf1l.| -2026-04-27T09:50:00.312Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 51 21 4b 4b 4c 4c 21 21 21 4a 67 |.!)KKQ!KKLL!!!Jg| -00000010 53 72 66 31 6c 1b |Srf1l.| -2026-04-27T09:50:00.312Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:00.312Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=468 y=86 altitude=0 facingRaw=3555 clientSpeed=719 effectiveSpeed=719 -> no echo (trust local key events) -2026-04-27T09:50:01.307Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1939, phase=combat) -2026-04-27T09:50:01.307Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:01.307Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 5c 33 4b 4b 52 61 21 21 21 4b 2b |.!)KK\3KKRa!!!K+| -00000010 5a 21 56 5e 59 1b |Z!V^Y.| -2026-04-27T09:50:01.308Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 5c 33 4b 4b 52 61 21 21 21 4b 2b |.!)KK\3KKRa!!!K+| -00000010 5a 21 56 5e 59 1b |Z!V^Y.| -2026-04-27T09:50:01.308Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:01.308Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=1421 y=617 altitude=0 facingRaw=3580 clientSpeed=1233 effectiveSpeed=1233 -> no echo (trust local key events) -2026-04-27T09:50:02.315Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=1973, phase=combat) -2026-04-27T09:50:02.316Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:02.316Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 67 6d 4b 4b 5f 22 21 21 21 4b 3f |.!)KKgmKK_"!!!K?| -00000010 5c 3e 5c 3a 3b 1b |\>\:;.| -2026-04-27T09:50:02.316Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 67 6d 4b 4b 5f 22 21 21 21 4b 3f |.!)KKgmKK_"!!!K?| -00000010 5c 3e 5c 3a 3b 1b |\>\:;.| -2026-04-27T09:50:02.317Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:02.317Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=2414 y=1659 altitude=0 facingRaw=3600 clientSpeed=1432 effectiveSpeed=1432 -> no echo (trust local key events) -2026-04-27T09:50:03.316Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2007, phase=combat) -2026-04-27T09:50:03.316Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:03.317Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4b 70 5e 4b 4b 6f 53 21 21 21 4b 3f |.!)KKp^KKoS!!!K?| -00000010 5f 26 2f 2b 73 1b |_&/+s.| -2026-04-27T09:50:03.317Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4b 70 5e 4b 4b 6f 53 21 21 21 4b 3f |.!)KKp^KKoS!!!K?| -00000010 5f 26 2f 2b 73 1b |_&/+s.| -2026-04-27T09:50:03.317Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:03.317Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=3164 y=3068 altitude=0 facingRaw=3600 clientSpeed=1663 effectiveSpeed=1663 -> no echo (trust local key events) -2026-04-27T09:50:04.315Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2041, phase=combat) -2026-04-27T09:50:04.316Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:04.316Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 22 61 4b 4c 2d 4f 21 21 21 4b 42 |.!)KL"aKL-O!!!KB| -00000010 60 3a 36 21 50 1b |`:6!P.| -2026-04-27T09:50:04.316Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 22 61 4b 4c 2d 4f 21 21 21 4b 42 |.!)KL"aKL-O!!!KB| -00000010 60 3a 36 21 50 1b |`:6!P.| -2026-04-27T09:50:04.317Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:04.317Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=3762 y=4679 altitude=0 facingRaw=3603 clientSpeed=1768 effectiveSpeed=1768 -> no echo (trust local key events) -2026-04-27T09:50:05.326Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2075, phase=combat) -2026-04-27T09:50:05.326Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:05.326Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 28 25 4b 4c 42 21 21 21 21 4b 48 |.!)KL(%KLB!!!!KH| -00000010 60 53 4a 48 6d 1b |`SJHm.| -2026-04-27T09:50:05.327Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 28 25 4b 4c 42 21 21 21 21 4b 48 |.!)KL(%KLB!!!!KH| -00000010 60 53 4a 48 6d 1b |`SJHm.| -2026-04-27T09:50:05.327Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:05.327Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=4212 y=6418 altitude=0 facingRaw=3609 clientSpeed=1793 effectiveSpeed=1793 -> no echo (trust local key events) -2026-04-27T09:50:06.326Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2109, phase=combat) -2026-04-27T09:50:06.327Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:06.327Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 2b 57 4b 4c 57 22 21 21 21 4b 48 |.!)KL+WKLW"!!!KH| -00000010 61 28 2c 23 28 1b |a(,#(.| -2026-04-27T09:50:06.328Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 2b 57 4b 4c 57 22 21 21 21 4b 48 |.!)KL+WKLW"!!!KH| -00000010 61 28 2c 23 28 1b |a(,#(.| -2026-04-27T09:50:06.328Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:06.328Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=4517 y=8204 altitude=0 facingRaw=3609 clientSpeed=1835 effectiveSpeed=1835 -> no echo (trust local key events) -2026-04-27T09:50:07.334Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2143, phase=combat) -2026-04-27T09:50:07.334Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:07.335Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 2e 35 4b 4c 6c 52 21 21 21 4b 48 |.!)KL.5KLlR!!!KH| -00000010 61 41 3d 66 4a 1b |aA=fJ.| -2026-04-27T09:50:07.335Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 2e 35 4b 4c 6c 52 21 21 21 4b 48 |.!)KL.5KLlR!!!KH| -00000010 61 41 3d 66 4a 1b |aA=fJ.| -2026-04-27T09:50:07.335Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:07.335Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=4738 y=10037 altitude=0 facingRaw=3609 clientSpeed=1860 effectiveSpeed=1860 -> no echo (trust local key events) -2026-04-27T09:50:08.333Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2177, phase=combat) -2026-04-27T09:50:08.333Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:08.334Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 30 2d 4b 4d 2d 31 21 21 21 4b 48 |.!)KL0-KM-1!!!KH| -00000010 61 4d 30 5e 43 1b |aM0^C.| -2026-04-27T09:50:08.334Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 30 2d 4b 4d 2d 31 21 21 21 4b 48 |.!)KL0-KM-1!!!KH| -00000010 61 4d 30 5e 43 1b |aM0^C.| -2026-04-27T09:50:08.335Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:08.335Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=4900 y=11874 altitude=0 facingRaw=3609 clientSpeed=1872 effectiveSpeed=1872 -> no echo (trust local key events) -2026-04-27T09:50:09.348Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2211, phase=combat) -2026-04-27T09:50:09.349Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:09.349Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 31 6a 4b 4d 43 30 21 21 21 4b 48 |.!)KL1jKMC0!!!KH| -00000010 61 53 41 34 70 1b |aSA4p.| -2026-04-27T09:50:09.349Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 31 6a 4b 4d 43 30 21 21 21 4b 48 |.!)KL1jKMC0!!!KH| -00000010 61 53 41 34 70 1b |aSA4p.| -2026-04-27T09:50:09.349Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:09.350Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5046 y=13743 altitude=0 facingRaw=3609 clientSpeed=1878 effectiveSpeed=1878 -> no echo (trust local key events) -2026-04-27T09:50:10.341Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2245, phase=combat) -2026-04-27T09:50:10.342Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:10.342Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 33 3b 4b 4d 58 73 21 21 21 4b 48 |.!)KL3;KMXs!!!KH| -00000010 61 51 61 45 6f 1b |aQaEo.| -2026-04-27T09:50:10.342Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 33 3b 4b 4d 58 73 21 21 21 4b 48 |.!)KL3;KMXs!!!KH| -00000010 61 51 61 45 6f 1b |aQaEo.| -2026-04-27T09:50:10.343Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:10.343Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5169 y=15595 altitude=0 facingRaw=3609 clientSpeed=1876 effectiveSpeed=1876 -> no echo (trust local key events) -2026-04-27T09:50:11.351Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2279, phase=combat) -2026-04-27T09:50:11.351Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:11.352Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 34 59 4b 4d 6e 74 21 21 21 4b 48 |.!)KL4YKMnt!!!KH| -00000010 61 53 54 5f 60 1b |aST_`.| -2026-04-27T09:50:11.352Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 34 59 4b 4d 6e 74 21 21 21 4b 48 |.!)KL4YKMnt!!!KH| -00000010 61 53 54 5f 60 1b |aST_`.| -2026-04-27T09:50:11.353Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:11.353Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5284 y=17466 altitude=0 facingRaw=3609 clientSpeed=1878 effectiveSpeed=1878 -> no echo (trust local key events) -2026-04-27T09:50:12.353Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2313, phase=combat) -2026-04-27T09:50:12.354Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:12.354Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 36 23 4b 4e 2f 61 21 21 21 4b 40 |.!)KL6#KN/a!!!K@| -00000010 61 47 24 31 60 1b |aG$1`.| -2026-04-27T09:50:12.354Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 36 23 4b 4e 2f 61 21 21 21 4b 40 |.!)KL6#KN/a!!!K@| -00000010 61 47 24 31 60 1b |aG$1`.| -2026-04-27T09:50:12.355Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:12.355Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5400 y=19317 altitude=0 facingRaw=3601 clientSpeed=1866 effectiveSpeed=1866 -> no echo (trust local key events) -2026-04-27T09:50:13.361Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2347, phase=combat) -2026-04-27T09:50:13.362Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:13.362Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 38 35 4b 4e 45 5d 21 21 21 4b 40 |.!)KL85KNE]!!!K@| -00000010 61 51 2e 44 37 1b |aQ.D7.| -2026-04-27T09:50:13.362Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 38 35 4b 4e 45 5d 21 21 21 4b 40 |.!)KL85KNE]!!!K@| -00000010 61 51 2e 44 37 1b |aQ.D7.| -2026-04-27T09:50:13.363Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:13.363Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5588 y=21183 altitude=0 facingRaw=3601 clientSpeed=1876 effectiveSpeed=1876 -> no echo (trust local key events) -2026-04-27T09:50:14.366Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2381, phase=combat) -2026-04-27T09:50:14.366Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:14.367Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 3b 3d 4b 4e 5b 3b 21 21 21 4b 3f |.!)KL;=KN[;!!!K?| -00000010 61 54 41 66 26 1b |aTAf&.| -2026-04-27T09:50:14.367Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 3b 3d 4b 4e 5b 3b 21 21 21 4b 3f |.!)KL;=KN[;!!!K?| -00000010 61 54 41 66 26 1b |aTAf&.| -2026-04-27T09:50:14.367Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:14.368Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=5851 y=23019 altitude=0 facingRaw=3600 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) -2026-04-27T09:50:15.366Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2415, phase=combat) -2026-04-27T09:50:15.366Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:15.367Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 3e 75 4b 4e 70 6e 21 21 21 4b 3f |.!)KL>uKNpn!!!K?| -00000010 61 57 3c 63 6c 1b |aWuKNpn!!!K?| -00000010 61 57 3c 63 6c 1b |aW no echo (trust local key events) -2026-04-27T09:50:16.376Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2449, phase=combat) -2026-04-27T09:50:16.377Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:16.377Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 43 24 4b 4f 31 47 21 21 21 4b 44 |.!)KLC$KO1G!!!KD| -00000010 61 50 3f 5d 57 1b |aP?]W.| -2026-04-27T09:50:16.378Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 43 24 4b 4f 31 47 21 21 21 4b 44 |.!)KLC$KO1G!!!KD| -00000010 61 50 3f 5d 57 1b |aP?]W.| -2026-04-27T09:50:16.378Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:16.378Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=6506 y=26686 altitude=0 facingRaw=3605 clientSpeed=1875 effectiveSpeed=1875 -> no echo (trust local key events) -2026-04-27T09:50:17.376Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2483, phase=combat) -2026-04-27T09:50:17.377Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:17.377Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 46 57 4b 4f 46 74 21 21 21 4b 44 |.!)KLFWKOFt!!!KD| -00000010 61 53 5a 47 5a 1b |aSZGZ.| -2026-04-27T09:50:17.377Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 46 57 4b 4f 46 74 21 21 21 4b 44 |.!)KLFWKOFt!!!KD| -00000010 61 53 5a 47 5a 1b |aSZGZ.| -2026-04-27T09:50:17.378Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:17.378Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=6812 y=28516 altitude=0 facingRaw=3605 clientSpeed=1878 effectiveSpeed=1878 -> no echo (trust local key events) -2026-04-27T09:50:18.375Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2517, phase=combat) -2026-04-27T09:50:18.375Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:18.375Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 49 6c 4b 4f 5c 51 21 21 21 4b 42 |.!)KLIlKO\Q!!!KB| -00000010 61 52 63 47 35 1b |aRcG5.| -2026-04-27T09:50:18.376Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 49 6c 4b 4f 5c 51 21 21 21 4b 42 |.!)KLIlKO\Q!!!KB| -00000010 61 52 63 47 35 1b |aRcG5.| -2026-04-27T09:50:18.376Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:18.376Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=7088 y=30351 altitude=0 facingRaw=3603 clientSpeed=1877 effectiveSpeed=1877 -> no echo (trust local key events) -2026-04-27T09:50:19.374Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2551, phase=combat) -2026-04-27T09:50:19.374Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:19.375Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 4d 45 4b 4f 72 24 21 21 21 4b 3c |.!)KLMEKOr$!!!K<| -00000010 61 4e 67 28 26 1b |aNg(&.| -2026-04-27T09:50:19.375Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 4d 45 4b 4f 72 24 21 21 21 4b 3c |.!)KLMEKOr$!!!K<| -00000010 61 4e 67 28 26 1b |aNg(&.| -2026-04-27T09:50:19.376Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:19.376Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=7389 y=32176 altitude=0 facingRaw=3597 clientSpeed=1873 effectiveSpeed=1873 -> no echo (trust local key events) -2026-04-27T09:50:20.373Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2585, phase=combat) -2026-04-27T09:50:20.374Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:20.374Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 51 74 4b 50 32 3a 21 21 21 4b 3c |.!)KLQtKP2:!!!K<| -00000010 61 55 5b 45 6b 1b |aU[Ek.| -2026-04-27T09:50:20.375Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 51 74 4b 50 32 3a 21 21 21 4b 3c |.!)KLQtKP2:!!!K<| -00000010 61 55 5b 45 6b 1b |aU[Ek.| -2026-04-27T09:50:20.375Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:20.375Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=7776 y=33983 altitude=0 facingRaw=3597 clientSpeed=1880 effectiveSpeed=1880 -> no echo (trust local key events) -2026-04-27T09:50:21.395Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2619, phase=combat) -2026-04-27T09:50:21.395Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:21.396Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 57 32 4b 50 47 6d 21 21 21 4b 33 |.!)KLW2KPGm!!!K3| -00000010 61 39 57 53 75 1b |a9WSu.| -2026-04-27T09:50:21.396Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 57 32 4b 50 47 6d 21 21 21 4b 33 |.!)KLW2KPGm!!!K3| -00000010 61 39 57 53 75 1b |a9WSu.| -2026-04-27T09:50:21.396Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:21.396Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=8220 y=35819 altitude=0 facingRaw=3588 clientSpeed=1852 effectiveSpeed=1852 -> no echo (trust local key events) -2026-04-27T09:50:22.404Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2653, phase=combat) -2026-04-27T09:50:22.404Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:22.405Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 5e 3a 4b 50 5c 53 21 21 21 4b 2e |.!)KL^:KP\S!!!K.| -00000010 61 38 26 6f 3e 1b |a8&o>.| -2026-04-27T09:50:22.405Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 5e 3a 4b 50 5c 53 21 21 21 4b 2e |.!)KL^:KP\S!!!K.| -00000010 61 38 26 6f 3e 1b |a8&o>.| -2026-04-27T09:50:22.405Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:22.406Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=8823 y=37578 altitude=0 facingRaw=3583 clientSpeed=1851 effectiveSpeed=1851 -> no echo (trust local key events) -2026-04-27T09:50:23.406Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2687, phase=combat) -2026-04-27T09:50:23.407Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:23.407Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 66 55 4b 50 70 54 21 21 21 4b 3a |.!)KLfUKPpT!!!K:| -00000010 61 47 58 3c 35 1b |aGX<5.| -2026-04-27T09:50:23.408Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 66 55 4b 50 70 54 21 21 21 4b 3a |.!)KLfUKPpT!!!K:| -00000010 61 47 58 3c 35 1b |aGX<5.| -2026-04-27T09:50:23.408Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:23.408Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=9530 y=39279 altitude=0 facingRaw=3595 clientSpeed=1866 effectiveSpeed=1866 -> no echo (trust local key events) -2026-04-27T09:50:24.419Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2721, phase=combat) -2026-04-27T09:50:24.420Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:24.420Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 6e 34 4b 51 30 29 21 21 21 4b 3a |.!)KLn4KQ0)!!!K:| -00000010 61 4f 30 67 4e 1b |aO0gN.| -2026-04-27T09:50:24.420Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4c 6e 34 4b 51 30 29 21 21 21 4b 3a |.!)KLn4KQ0)!!!K:| -00000010 61 4f 30 67 4e 1b |aO0gN.| -2026-04-27T09:50:24.421Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:24.421Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=10177 y=41021 altitude=0 facingRaw=3595 clientSpeed=1874 effectiveSpeed=1874 -> no echo (trust local key events) -2026-04-27T09:50:25.416Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2755, phase=combat) -2026-04-27T09:50:25.416Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:25.416Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4c 75 3c 4b 51 44 5c 21 21 21 4b 41 |.!)KLu no echo (trust local key events) -2026-04-27T09:50:26.425Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2789, phase=combat) -2026-04-27T09:50:26.425Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:26.426Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 25 71 4b 51 59 6c 21 21 21 4b 47 |.!)KM%qKQYl!!!KG| -00000010 61 38 59 29 33 1b |a8Y)3.| -2026-04-27T09:50:26.426Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 25 71 4b 51 59 6c 21 21 21 4b 47 |.!)KM%qKQYl!!!KG| -00000010 61 38 59 29 33 1b |a8Y)3.| -2026-04-27T09:50:26.426Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:26.427Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=11258 y=44573 altitude=0 facingRaw=3608 clientSpeed=1851 effectiveSpeed=1851 -> no echo (trust local key events) -2026-04-27T09:50:27.427Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2823, phase=combat) -2026-04-27T09:50:27.427Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:27.427Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 29 67 4b 51 6f 31 21 21 21 4b 47 |.!)KM)gKQo1!!!KG| -00000010 61 49 52 2f 40 1b |aIR/@.| -2026-04-27T09:50:27.428Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 29 67 4b 51 6f 31 21 21 21 4b 47 |.!)KM)gKQo1!!!KG| -00000010 61 49 52 2f 40 1b |aIR/@.| -2026-04-27T09:50:27.428Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:27.428Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=11588 y=46384 altitude=0 facingRaw=3608 clientSpeed=1868 effectiveSpeed=1868 -> no echo (trust local key events) -2026-04-27T09:50:28.432Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2857, phase=combat) -2026-04-27T09:50:28.433Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:28.433Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 2c 4c 4b 52 30 24 21 21 21 4b 47 |.!)KM,LKR0$!!!KG| -00000010 61 50 66 70 3d 1b |aPfp=.| -2026-04-27T09:50:28.433Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 2c 4c 4b 52 30 24 21 21 21 4b 47 |.!)KM,LKR0$!!!KG| -00000010 61 50 66 70 3d 1b |aPfp=.| -2026-04-27T09:50:28.433Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:28.434Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=11816 y=48241 altitude=0 facingRaw=3608 clientSpeed=1875 effectiveSpeed=1875 -> no echo (trust local key events) -2026-04-27T09:50:29.447Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2891, phase=combat) -2026-04-27T09:50:29.447Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:29.448Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 2e 61 4b 52 46 24 21 21 21 4b 47 |.!)KM.aKRF$!!!KG| -00000010 61 52 59 6d 3d 1b |aRYm=.| -2026-04-27T09:50:29.448Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 2e 61 4b 52 46 24 21 21 21 4b 47 |.!)KM.aKRF$!!!KG| -00000010 61 52 59 6d 3d 1b |aRYm=.| -2026-04-27T09:50:29.448Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:29.448Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12007 y=50111 altitude=0 facingRaw=3608 clientSpeed=1877 effectiveSpeed=1877 -> no echo (trust local key events) -2026-04-27T09:50:30.450Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2925, phase=combat) -2026-04-27T09:50:30.450Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:30.450Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 30 4f 4b 52 5b 66 21 21 21 4b 47 |.!)KM0OKR[f!!!KG| -00000010 61 54 3d 4c 3e 1b |aT=L>.| -2026-04-27T09:50:30.451Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 30 4f 4b 52 5b 66 21 21 21 4b 47 |.!)KM0OKR[f!!!KG| -00000010 61 54 3d 4c 3e 1b |aT=L>.| -2026-04-27T09:50:30.451Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:30.451Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12159 y=51962 altitude=0 facingRaw=3608 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) -2026-04-27T09:50:31.443Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2959, phase=combat) -2026-04-27T09:50:31.443Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:31.444Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 32 30 4b 52 71 56 21 21 21 4b 47 |.!)KM20KRqV!!!KG| -00000010 61 54 49 46 64 1b |aTIFd.| -2026-04-27T09:50:31.444Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 32 30 4b 52 71 56 21 21 21 4b 47 |.!)KM20KRqV!!!KG| -00000010 61 54 49 46 64 1b |aTIFd.| -2026-04-27T09:50:31.444Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:31.445Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12298 y=53816 altitude=0 facingRaw=3608 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) -2026-04-27T09:50:32.448Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=2993, phase=combat) -2026-04-27T09:50:32.449Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:32.449Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 33 67 4b 53 32 44 21 21 21 4b 47 |.!)KM3gKS2D!!!KG| -00000010 61 57 40 3e 39 1b |aW@>9.| -2026-04-27T09:50:32.450Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 33 67 4b 53 32 44 21 21 21 4b 47 |.!)KM3gKS2D!!!KG| -00000010 61 57 40 3e 39 1b |aW@>9.| -2026-04-27T09:50:32.450Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:32.450Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12438 y=55668 altitude=0 facingRaw=3608 clientSpeed=1882 effectiveSpeed=1882 -> no echo (trust local key events) -2026-04-27T09:50:33.452Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3027, phase=combat) -2026-04-27T09:50:33.452Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:33.452Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 35 46 4b 53 48 44 21 21 21 4b 47 |.!)KM5FKSHD!!!KG| -00000010 61 57 44 3b 38 1b |aWD;8.| -2026-04-27T09:50:33.453Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 35 46 4b 53 48 44 21 21 21 4b 47 |.!)KM5FKSHD!!!KG| -00000010 61 57 44 3b 38 1b |aWD;8.| -2026-04-27T09:50:33.453Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:33.453Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12575 y=57538 altitude=0 facingRaw=3608 clientSpeed=1882 effectiveSpeed=1882 -> no echo (trust local key events) -2026-04-27T09:50:34.452Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3061, phase=combat) -2026-04-27T09:50:34.452Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:34.452Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 36 61 4b 53 5e 31 21 21 21 4b 47 |.!)KM6aKS^1!!!KG| -00000010 61 57 3a 24 5d 1b |aW:$].| -2026-04-27T09:50:34.453Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 36 61 4b 53 5e 31 21 21 21 4b 47 |.!)KM6aKS^1!!!KG| -00000010 61 57 3a 24 5d 1b |aW:$].| -2026-04-27T09:50:34.453Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:34.453Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12687 y=59389 altitude=0 facingRaw=3608 clientSpeed=1882 effectiveSpeed=1882 -> no echo (trust local key events) -2026-04-27T09:50:35.468Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3095, phase=combat) -2026-04-27T09:50:35.469Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:35.469Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 38 22 4b 53 74 2e 21 21 21 4b 51 |.!)KM8"KSt.!!!KQ| -00000010 61 39 46 68 6f 1b |a9Fho.| -2026-04-27T09:50:35.469Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 38 22 4b 53 74 2e 21 21 21 4b 51 |.!)KM8"KSt.!!!KQ| -00000010 61 39 46 68 6f 1b |a9Fho.| -2026-04-27T09:50:35.470Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:35.470Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12794 y=61256 altitude=0 facingRaw=3618 clientSpeed=1852 effectiveSpeed=1852 -> no echo (trust local key events) -2026-04-27T09:50:36.466Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3129, phase=combat) -2026-04-27T09:50:36.467Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:36.467Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 38 28 4b 54 34 70 21 21 21 4b 51 |.!)KM8(KT4p!!!KQ| -00000010 61 46 54 36 45 1b |aFT6E.| -2026-04-27T09:50:36.467Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 38 28 4b 54 34 70 21 21 21 4b 51 |.!)KM8(KT4p!!!KQ| -00000010 61 46 54 36 45 1b |aFT6E.| -2026-04-27T09:50:36.468Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:36.468Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12800 y=63107 altitude=0 facingRaw=3618 clientSpeed=1865 effectiveSpeed=1865 -> no echo (trust local key events) -2026-04-27T09:50:37.467Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3163, phase=combat) -2026-04-27T09:50:37.468Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:37.468Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 37 36 4b 54 4a 5d 21 21 21 4b 51 |.!)KM76KTJ]!!!KQ| -00000010 61 51 5f 40 54 1b |aQ_@T.| -2026-04-27T09:50:37.468Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 37 36 4b 54 4a 5d 21 21 21 4b 51 |.!)KM76KTJ]!!!KQ| -00000010 61 51 5f 40 54 1b |aQ_@T.| -2026-04-27T09:50:37.468Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:37.469Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12729 y=64958 altitude=0 facingRaw=3618 clientSpeed=1876 effectiveSpeed=1876 -> no echo (trust local key events) -2026-04-27T09:50:38.462Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3197, phase=combat) -2026-04-27T09:50:38.462Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:38.463Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 35 5d 4b 54 60 4a 21 21 21 4b 51 |.!)KM5]KT`J!!!KQ| -00000010 61 53 46 71 3a 1b |aSFq:.| -2026-04-27T09:50:38.463Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 35 5d 4b 54 60 4a 21 21 21 4b 51 |.!)KM5]KT`J!!!KQ| -00000010 61 53 46 71 3a 1b |aSFq:.| -2026-04-27T09:50:38.463Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:38.464Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12598 y=66809 altitude=0 facingRaw=3618 clientSpeed=1878 effectiveSpeed=1878 -> no echo (trust local key events) -2026-04-27T09:50:39.477Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3231, phase=combat) -2026-04-27T09:50:39.477Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:39.477Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 33 68 4b 55 21 4b 21 21 21 4b 51 |.!)KM3hKU!K!!!KQ| -00000010 61 52 25 24 3a 1b |aR%$:.| -2026-04-27T09:50:39.478Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 33 68 4b 55 21 4b 21 21 21 4b 51 |.!)KM3hKU!K!!!KQ| -00000010 61 52 25 24 3a 1b |aR%$:.| -2026-04-27T09:50:39.478Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:39.478Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12439 y=68680 altitude=0 facingRaw=3618 clientSpeed=1877 effectiveSpeed=1877 -> no echo (trust local key events) -2026-04-27T09:50:40.471Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3265, phase=combat) -2026-04-27T09:50:40.472Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:40.472Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 31 4a 4b 55 37 32 21 21 21 4b 51 |.!)KM1JKU72!!!KQ| -00000010 61 54 26 71 54 1b |aT&qT.| -2026-04-27T09:50:40.473Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 31 4a 4b 55 37 32 21 21 21 4b 51 |.!)KM1JKU72!!!KQ| -00000010 61 54 26 71 54 1b |aT&qT.| -2026-04-27T09:50:40.473Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:40.474Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12239 y=70525 altitude=0 facingRaw=3618 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) -2026-04-27T09:50:41.483Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3299, phase=combat) -2026-04-27T09:50:41.484Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:41.484Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 2f 2a 4b 55 4d 30 21 21 21 4b 50 |.!)KM/*KUM0!!!KP| -00000010 61 54 49 63 35 1b |aTIc5.| -2026-04-27T09:50:41.484Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 2f 2a 4b 55 4d 30 21 21 21 4b 50 |.!)KM/*KUM0!!!KP| -00000010 61 54 49 63 35 1b |aTIc5.| -2026-04-27T09:50:41.484Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:41.485Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12037 y=72393 altitude=0 facingRaw=3617 clientSpeed=1879 effectiveSpeed=1879 -> no echo (trust local key events) -2026-04-27T09:50:42.490Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3333, phase=combat) -2026-04-27T09:50:42.490Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:42.491Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 2d 56 4b 55 62 72 21 21 21 4b 37 |.!)KM-VKUbr!!!K7| -00000010 5f 73 33 34 2c 1b |_s34,.| -2026-04-27T09:50:42.491Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 2d 56 4b 55 62 72 21 21 21 4b 37 |.!)KM-VKUbr!!!K7| -00000010 5f 73 33 34 2c 1b |_s34,.| -2026-04-27T09:50:42.491Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:42.492Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=11911 y=74244 altitude=0 facingRaw=3592 clientSpeed=1740 effectiveSpeed=1740 -> no echo (trust local key events) -2026-04-27T09:50:43.497Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3367, phase=combat) -2026-04-27T09:50:43.497Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:43.497Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 2f 62 4b 56 23 48 21 21 21 4b 2b |.!)KM/bKV#H!!!K+| -00000010 5f 5a 4c 59 3a 1b |_ZLY:.| -2026-04-27T09:50:43.498Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 2f 62 4b 56 23 48 21 21 21 4b 2b |.!)KM/bKV#H!!!K+| -00000010 5f 5a 4c 59 3a 1b |_ZLY:.| -2026-04-27T09:50:43.498Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:43.498Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12093 y=76072 altitude=0 facingRaw=3580 clientSpeed=1715 effectiveSpeed=1715 -> no echo (trust local key events) -2026-04-27T09:50:44.505Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3401, phase=combat) -2026-04-27T09:50:44.506Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:44.506Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 36 36 4b 56 37 58 21 21 21 4b 21 |.!)KM66KV7X!!!K!| -00000010 5f 6e 54 54 63 1b |_nTTc.| -2026-04-27T09:50:44.506Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 36 36 4b 56 37 58 21 21 21 4b 21 |.!)KM66KV7X!!!K!| -00000010 5f 6e 54 54 63 1b |_nTTc.| -2026-04-27T09:50:44.507Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:44.507Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=12644 y=77788 altitude=0 facingRaw=3570 clientSpeed=1735 effectiveSpeed=1735 -> no echo (trust local key events) -2026-04-27T09:50:45.503Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3435, phase=combat) -2026-04-27T09:50:45.503Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:45.503Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 40 32 4b 56 4a 32 21 21 21 4b 30 |.!)KM@2KVJ2!!!K0| -00000010 61 24 56 72 33 1b |a$Vr3.| -2026-04-27T09:50:45.504Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 40 32 4b 56 4a 32 21 21 21 4b 30 |.!)KM@2KVJ2!!!K0| -00000010 61 24 56 72 33 1b |a$Vr3.| -2026-04-27T09:50:45.504Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:45.504Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=13490 y=79365 altitude=0 facingRaw=3585 clientSpeed=1831 effectiveSpeed=1831 -> no echo (trust local key events) -2026-04-27T09:50:46.510Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3469, phase=combat) -2026-04-27T09:50:46.511Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:46.511Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 49 6d 4b 56 5d 27 21 21 21 4b 43 |.!)KMImKV]'!!!KC| -00000010 60 3e 5a 3c 55 1b |`>ZZ no echo (trust local key events) -2026-04-27T09:50:47.516Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3503, phase=combat) -2026-04-27T09:50:47.517Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:47.517Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 50 71 4b 56 71 47 21 21 21 4b 40 |.!)KMPqKVqG!!!K@| -00000010 61 30 50 28 5b 1b |a0P([.| -2026-04-27T09:50:47.517Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 50 71 4b 56 71 47 21 21 21 4b 40 |.!)KMPqKVqG!!!K@| -00000010 61 30 50 28 5b 1b |a0P([.| -2026-04-27T09:50:47.518Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:47.518Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=14913 y=82701 altitude=0 facingRaw=3601 clientSpeed=1843 effectiveSpeed=1843 -> no echo (trust local key events) -2026-04-27T09:50:48.520Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3537, phase=combat) -2026-04-27T09:50:48.521Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:48.521Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 57 27 4b 57 30 75 21 21 21 4b 33 |.!)KMW'KW0u!!!K3| -00000010 61 3b 32 49 26 1b |a;2I&.| -2026-04-27T09:50:48.521Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 57 27 4b 57 30 75 21 21 21 4b 33 |.!)KMW'KW0u!!!K3| -00000010 61 3b 32 49 26 1b |a;2I&.| -2026-04-27T09:50:48.522Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:48.522Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=15434 y=84447 altitude=0 facingRaw=3588 clientSpeed=1854 effectiveSpeed=1854 -> no echo (trust local key events) -2026-04-27T09:50:49.526Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3571, phase=combat) -2026-04-27T09:50:49.527Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:49.527Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 5e 32 4b 57 45 5b 21 21 21 4b 3e |.!)KM^2KWE[!!!K>| -00000010 61 43 3e 46 55 1b |aC>FU.| -2026-04-27T09:50:49.528Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 5e 32 4b 57 45 5b 21 21 21 4b 3e |.!)KM^2KWE[!!!K>| -00000010 61 43 3e 46 55 1b |aC>FU.| -2026-04-27T09:50:49.528Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:49.528Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=16040 y=86206 altitude=0 facingRaw=3599 clientSpeed=1862 effectiveSpeed=1862 -> no echo (trust local key events) -2026-04-27T09:50:50.526Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3605, phase=combat) -2026-04-27T09:50:50.527Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:50.527Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 64 3a 4b 57 5a 39 21 21 21 4b 3f |.!)KMd:KWZ9!!!K?| -00000010 61 2a 51 2a 26 1b |a*Q*&.| -2026-04-27T09:50:50.528Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 64 3a 4b 57 5a 39 21 21 21 4b 3f |.!)KMd:KWZ9!!!K?| -00000010 61 2a 51 2a 26 1b |a*Q*&.| -2026-04-27T09:50:50.528Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:50.528Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=16558 y=87957 altitude=0 facingRaw=3600 clientSpeed=1837 effectiveSpeed=1837 -> no echo (trust local key events) -2026-04-27T09:50:51.538Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3639, phase=combat) -2026-04-27T09:50:51.538Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:51.538Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 69 3d 4b 57 6d 6f 21 21 21 4b 3f |.!)KMi=KWmo!!!K?| -00000010 5e 73 3b 51 65 1b |^s;Qe.| -2026-04-27T09:50:51.539Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 69 3d 4b 57 6d 6f 21 21 21 4b 3f |.!)KMi=KWmo!!!K?| -00000010 5e 73 3b 51 65 1b |^s;Qe.| -2026-04-27T09:50:51.539Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:51.539Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=16986 y=89626 altitude=0 facingRaw=3600 clientSpeed=1655 effectiveSpeed=1655 -> no echo (trust local key events) -2026-04-27T09:50:52.541Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3673, phase=combat) -2026-04-27T09:50:52.542Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:52.542Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 6d 4e 4b 58 2b 27 21 21 21 4b 3f |.!)KMmNKX+'!!!K?| -00000010 5e 22 67 68 72 1b |^"ghr.| -2026-04-27T09:50:52.542Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 6d 4e 4b 58 2b 27 21 21 21 4b 3f |.!)KMmNKX+'!!!K?| -00000010 5e 22 67 68 72 1b |^"ghr.| -2026-04-27T09:50:52.543Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:52.543Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=17343 y=91169 altitude=0 facingRaw=3600 clientSpeed=1574 effectiveSpeed=1574 -> no echo (trust local key events) -2026-04-27T09:50:53.547Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3707, phase=combat) -2026-04-27T09:50:53.548Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:53.548Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 71 3b 4b 58 3c 39 21 21 21 4b 3f |.!)KMq;KX<9!!!K?| -00000010 5c 5a 48 23 52 1b |\ZH#R.| -2026-04-27T09:50:53.548Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 71 3b 4b 58 3c 39 21 21 21 4b 3f |.!)KMq;KX<9!!!K?| -00000010 5c 5a 48 23 52 1b |\ZH#R.| -2026-04-27T09:50:53.549Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:53.549Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=17664 y=92632 altitude=0 facingRaw=3600 clientSpeed=1460 effectiveSpeed=1460 -> no echo (trust local key events) -2026-04-27T09:50:54.554Z DEBUG [server:f38959b6] [world] recv 34 bytes (total=3741, phase=combat) -2026-04-27T09:50:54.554Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=22 -2026-04-27T09:50:54.554Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 29 4b 4d 74 6b 4b 58 4c 5f 21 21 21 4b 3f |.!)KMtkKXL_!!!K?| -00000010 5d 30 28 53 44 1b |]0(SD.| -2026-04-27T09:50:54.555Z DEBUG [server:f38959b6] [world] rx type-0 len=22 -00000000 1b 21 29 4b 4d 74 6b 4b 58 4c 5f 21 21 21 4b 3f |.!)KMtkKXL_!!!K?| -00000010 5d 30 28 53 44 1b |]0(SD.| -2026-04-27T09:50:54.555Z DEBUG [server:f38959b6] [world] client seq=0 cmd=8 -2026-04-27T09:50:54.555Z DEBUG [server:f38959b6] [world/combat] cmd8 coasting: x=17967 y=94030 altitude=0 facingRaw=3600 clientSpeed=1503 effectiveSpeed=1503 -> no echo (trust local key events) -2026-04-27T09:50:55.554Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=3781, phase=combat) -2026-04-27T09:50:55.555Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 -2026-04-27T09:50:55.555Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2a 4b 4e 23 63 4b 58 5f 26 21 21 21 4b 3f |.!*KN#cKX_&!!!K?| -00000010 4b 4b 4b 4c 4b 4b 5f 51 3a 3d 66 1b |KKKLKK_Q:=f.| -2026-04-27T09:50:55.555Z DEBUG [server:f38959b6] [world] rx type-0 len=28 -00000000 1b 21 2a 4b 4e 23 63 4b 58 5f 26 21 21 21 4b 3f |.!*KN#cKX_&!!!K?| -00000010 4b 4b 4b 4c 4b 4b 5f 51 3a 3d 66 1b |KKKLKK_Q:=f.| -2026-04-27T09:50:55.556Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 -2026-04-27T09:50:55.556Z DEBUG [server:f38959b6] [world/combat] cmd9 moving: altitude=0 facingRaw=3600 pitchRaw=3613 torsoYawRaw=3612 clientSpeed=1706 effectiveSpeed=1706 pitch=182 torsoYaw=0 localEcho=none -2026-04-27T09:50:56.556Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=3821, phase=combat) -2026-04-27T09:50:56.556Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 -2026-04-27T09:50:56.557Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2a 4b 4e 27 6d 4b 58 72 6b 21 21 21 4b 3f |.!*KN'mKXrk!!!K?| -00000010 4b 4b 4b 4c 4b 4b 60 59 55 47 75 1b |KKKLKK`YUGu.| -2026-04-27T09:50:56.557Z DEBUG [server:f38959b6] [world] rx type-0 len=28 -00000000 1b 21 2a 4b 4e 27 6d 4b 58 72 6b 21 21 21 4b 3f |.!*KN'mKXrk!!!K?| -00000010 4b 4b 4b 4c 4b 4b 60 59 55 47 75 1b |KKKLKK`YUGu.| -2026-04-27T09:50:56.557Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 -2026-04-27T09:50:56.557Z DEBUG [server:f38959b6] [world/combat] cmd9 moving: altitude=0 facingRaw=3600 pitchRaw=3613 torsoYawRaw=3612 clientSpeed=1799 effectiveSpeed=1799 pitch=182 torsoYaw=0 localEcho=none -2026-04-27T09:50:57.575Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=3861, phase=combat) -2026-04-27T09:50:57.575Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 -2026-04-27T09:50:57.576Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2a 4b 4e 2c 2c 4b 59 32 6b 21 21 21 4b 3f |.!*KN,,KY2k!!!K?| -00000010 4b 4b 4b 4c 4b 4b 61 25 31 38 58 1b |KKKLKKa%18X.| -2026-04-27T09:50:57.576Z DEBUG [server:f38959b6] [world] rx type-0 len=28 -00000000 1b 21 2a 4b 4e 2c 2c 4b 59 32 6b 21 21 21 4b 3f |.!*KN,,KY2k!!!K?| -00000010 4b 4b 4b 4c 4b 4b 61 25 31 38 58 1b |KKKLKKa%18X.| -2026-04-27T09:50:57.576Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 -2026-04-27T09:50:57.577Z DEBUG [server:f38959b6] [world/combat] cmd9 moving: altitude=0 facingRaw=3600 pitchRaw=3613 torsoYawRaw=3612 clientSpeed=1832 effectiveSpeed=1832 pitch=182 torsoYaw=0 localEcho=none -2026-04-27T09:50:58.315Z INFO [server:f38959b6] [world/combat] bot jump start: slot=5 mechId=98 fuel=120 from=(34227,172450) to=(31791,160700) apex=6000 reason=aggressive range close -2026-04-27T09:50:58.592Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=3901, phase=combat) -2026-04-27T09:50:58.592Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 -2026-04-27T09:50:58.593Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2a 4b 4e 30 3c 4b 59 48 29 21 21 21 4b 3f |.!*KN010 risk=0 expected=%.1f targetHealth=5.3999999999999995 updates=255 shots=[1] 0:LRM-15:15:center-torso-front:slot=5:target=player:attach=31:chance=18:roll=8:band=long:cross=1:health=255:updates=0x19=9,1:LRM-15:miss:target=player:chance=18:roll=26:band=long:cross=1:impact=16797/106416/0 -2026-04-27T09:51:01.619Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=4021, phase=combat) -2026-04-27T09:51:01.619Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 -2026-04-27T09:51:01.620Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2a 4b 4e 3c 6b 4b 5a 33 4e 21 21 21 4b 3f |.!*KN no echo (trust local key events) -2026-04-27T09:51:04.420Z DEBUG [server:f38959b6] [world] recv 56 bytes (total=4151, phase=combat) -2026-04-27T09:51:04.421Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=44 -2026-04-27T09:51:04.421Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2b 21 27 23 4b 4b 4b 4b 4b 4f 68 35 4e 57 |.!+!'#KKKKKOh5NW| -00000010 2c 49 75 75 75 2b 22 27 23 4b 4b 4b 4b 4b 54 2f |,Iuuu+"'#KKKKKT/| -00000020 74 4e 6d 26 6f 75 75 75 5f 68 42 1b |tNm&ouuu_hB.| -2026-04-27T09:51:04.421Z DEBUG [server:f38959b6] [world] rx type-0 len=44 -00000000 1b 21 2b 21 27 23 4b 4b 4b 4b 4b 4f 68 35 4e 57 |.!+!'#KKKKKOh5NW| -00000010 2c 49 75 75 75 2b 22 27 23 4b 4b 4b 4b 4b 54 2f |,Iuuu+"'#KKKKKT/| -00000020 74 4e 6d 26 6f 75 75 75 5f 68 42 1b |tNm&ouuu_hB.| -2026-04-27T09:51:04.421Z DEBUG [server:f38959b6] [world] client seq=0 cmd=10 -2026-04-27T09:51:04.423Z DEBUG [server:f38959b6] [world/combat] cmd10 direct-cmd10 path: no recent cmd12/action0 (age=n/a records=2) — compatible with TIC fire geometry -2026-04-27T09:51:04.424Z INFO [server:f38959b6] [world/combat] cmd10 weapon fire accepted: firePath=direct-cmd10 records=2 weaponSlots=0/1 botHealth=1890 updates=0 shots=[0:LRM-15:15:slot=5:miss:5/1:chance=34:roll=50:band=long:cross=0:bot=30986/156859,1:LRM-15:15:slot=5:miss:5/1:chance=34:roll=72:band=long:cross=0:bot=30986/156859] -2026-04-27T09:51:04.621Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=4191, phase=combat) -2026-04-27T09:51:04.622Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 -2026-04-27T09:51:04.622Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2a 4b 4e 49 3d 4b 5a 73 54 21 21 21 4b 3f |.!*KNI=KZsT!!!K?| -00000010 4b 4b 4b 49 4b 4b 60 55 28 33 5a 1b |KKKIKK`U(3Z.| -2026-04-27T09:51:04.622Z DEBUG [server:f38959b6] [world] rx type-0 len=28 -00000000 1b 21 2a 4b 4e 49 3d 4b 5a 73 54 21 21 21 4b 3f |.!*KNI=KZsT!!!K?| -00000010 4b 4b 4b 49 4b 4b 60 55 28 33 5a 1b |KKKIKK`U(3Z.| -2026-04-27T09:51:04.623Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 -2026-04-27T09:51:04.623Z DEBUG [server:f38959b6] [world/combat] cmd9 moving: altitude=0 facingRaw=3600 pitchRaw=3610 torsoYawRaw=3612 clientSpeed=1795 effectiveSpeed=1795 pitch=-364 torsoYaw=0 localEcho=none -2026-04-27T09:51:05.069Z INFO [server:f38959b6] [world/combat] bot volley: slot=6 side=2 mechId=98 target=player tic=B distance=623m weapons=0/1 heat=0->10 risk=0 expected=%.1f targetHealth=5.3999999999999995 updates=240 shots=[2] 0:LRM-15:miss:target=player:chance=18:roll=92:band=long:cross=73:impact=19268/108708/0,1:LRM-15:15:center-torso-front:slot=6:target=player:attach=31:chance=18:roll=8:band=long:cross=73:health=240:updates=0x19=0/0x24=15 -2026-04-27T09:51:05.581Z INFO [server:f38959b6] [world/combat] bot volley: slot=4 side=2 mechId=98 target=player tic=B distance=625m weapons=0/1 heat=0->10 risk=0 expected=%.1f targetHealth=5.3999999999999995 updates=225 shots=[3] 0:LRM-15:15:center-torso-front:slot=4:target=player:attach=31:chance=18:roll=4:band=long:cross=74:health=225:updates=0x24=0/0x2c=1/0x2d=1,1:LRM-15:miss:target=player:chance=18:roll=37:band=long:cross=74:impact=24382/109326/0 -2026-04-27T09:51:05.581Z INFO [server:f38959b6] [world/combat] queued match result=loss in 750ms (center torso destroyed) -2026-04-27T09:51:05.628Z DEBUG [server:f38959b6] [world] recv 40 bytes (total=4231, phase=combat) -2026-04-27T09:51:05.629Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=28 -2026-04-27T09:51:05.629Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 2a 4b 4e 4d 37 4b 5b 31 63 21 21 21 4b 3f |.!*KNM7K[1c!!!K?| -00000010 4b 4b 4b 47 4b 4b 5e 33 67 64 6e 1b |KKKGKK^3gdn.| -2026-04-27T09:51:05.629Z DEBUG [server:f38959b6] [world] rx type-0 len=28 -00000000 1b 21 2a 4b 4e 4d 37 4b 5b 31 63 21 21 21 4b 3f |.!*KNM7K[1c!!!K?| -00000010 4b 4b 4b 47 4b 4b 5e 33 67 64 6e 1b |KKKGKK^3gdn.| -2026-04-27T09:51:05.630Z DEBUG [server:f38959b6] [world] client seq=0 cmd=9 -2026-04-27T09:51:05.630Z DEBUG [server:f38959b6] [world/combat] cmd8/cmd9 ignored while result transition is pending -2026-04-27T09:51:06.332Z INFO [server:f38959b6] [world/combat] sending Cmd75/Cmd63 result transition (loss) -2026-04-27T09:51:16.848Z INFO [server:f38959b6] [world/combat] restoring world mode after result scene (loss) -2026-04-27T09:51:16.849Z DEBUG [server:f38959b6] [world] recv 23 bytes (total=4254, phase=world) -2026-04-27T09:51:16.850Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=11 -2026-04-27T09:51:16.850Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| -2026-04-27T09:51:16.850Z DEBUG [server:f38959b6] [world] rx type-0 len=11 -00000000 1b 21 24 22 3e 24 22 37 3e 22 1b |.!$">$"7>".| -2026-04-27T09:51:16.850Z DEBUG [server:f38959b6] [world] client seq=0 cmd=3 -2026-04-27T09:51:16.851Z INFO [server:f38959b6] [world] completing deferred world scene refresh (client-ready) -2026-04-27T09:51:16.851Z INFO [server:f38959b6] [world] scene refresh: logicalRoomId=167 sceneRoomId=167 header="Davion Arena" -2026-04-27T09:51:16.852Z INFO [server:f38959b6] [world] sending Cmd10 RoomPresenceSync (1 entries) -2026-04-27T09:51:16.853Z INFO [server:f38959b6] [world] notified room of arrival: rosterId=100015 callsign="Moose" -2026-04-27T09:51:20.655Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:51:20.657Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4266, phase=world) -2026-04-27T09:51:20.658Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:51:20.658Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:51:50.662Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:51:50.663Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4278, phase=world) -2026-04-27T09:51:50.664Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:51:50.664Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:52:20.676Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:52:20.678Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4290, phase=world) -2026-04-27T09:52:20.678Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:52:20.678Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:52:50.686Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:52:50.687Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4302, phase=world) -2026-04-27T09:52:50.687Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:52:50.687Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:53:20.690Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:53:20.691Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4314, phase=world) -2026-04-27T09:53:20.692Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:53:20.692Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:53:50.706Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:53:50.707Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4326, phase=world) -2026-04-27T09:53:50.707Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:53:50.707Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:54:20.710Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:54:20.711Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4338, phase=world) -2026-04-27T09:54:20.711Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:54:20.711Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:54:50.715Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:54:50.717Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4350, phase=world) -2026-04-27T09:54:50.717Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:54:50.718Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:55:20.726Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:55:20.727Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4362, phase=world) -2026-04-27T09:55:20.727Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:55:20.728Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:55:50.731Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:55:50.733Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4374, phase=world) -2026-04-27T09:55:50.733Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:55:50.733Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:56:20.742Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:56:20.743Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4386, phase=world) -2026-04-27T09:56:20.744Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:56:20.744Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:56:50.755Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:56:50.756Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4398, phase=world) -2026-04-27T09:56:50.757Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:56:50.757Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:57:20.756Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:57:20.758Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4410, phase=world) -2026-04-27T09:57:20.758Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:57:20.758Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:57:50.770Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:57:50.771Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4422, phase=world) -2026-04-27T09:57:50.771Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:57:50.772Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:58:20.777Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:58:20.778Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4434, phase=world) -2026-04-27T09:58:20.779Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:58:20.779Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:58:50.785Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:58:50.786Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4446, phase=world) -2026-04-27T09:58:50.787Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:58:50.787Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:59:20.790Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:59:20.791Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4458, phase=world) -2026-04-27T09:59:20.791Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:59:20.792Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T09:59:50.791Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T09:59:50.792Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4470, phase=world) -2026-04-27T09:59:50.793Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T09:59:50.793Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:00:20.800Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:00:20.801Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4482, phase=world) -2026-04-27T10:00:20.801Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:00:20.802Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:00:50.813Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:00:50.814Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4494, phase=world) -2026-04-27T10:00:50.814Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:00:50.814Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:01:20.814Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:01:20.815Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4506, phase=world) -2026-04-27T10:01:20.816Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:01:20.816Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:01:50.820Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:01:50.821Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4518, phase=world) -2026-04-27T10:01:50.821Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:01:50.822Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:02:20.820Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:02:20.821Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4530, phase=world) -2026-04-27T10:02:20.822Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:02:20.822Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:02:50.835Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:02:50.836Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4542, phase=world) -2026-04-27T10:02:50.836Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:02:50.836Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:03:20.847Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:03:20.848Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4554, phase=world) -2026-04-27T10:03:20.848Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:03:20.849Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:03:50.853Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:03:50.855Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4566, phase=world) -2026-04-27T10:03:50.855Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:03:50.856Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:04:20.861Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:04:20.862Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4578, phase=world) -2026-04-27T10:04:20.862Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:04:20.863Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:04:50.871Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:04:50.872Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4590, phase=world) -2026-04-27T10:04:50.872Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:04:50.873Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:05:20.886Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:05:20.887Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4602, phase=world) -2026-04-27T10:05:20.887Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:05:20.887Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:05:50.895Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:05:50.896Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4614, phase=world) -2026-04-27T10:05:50.896Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:05:50.896Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:06:20.896Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:06:20.897Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4626, phase=world) -2026-04-27T10:06:20.898Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:06:20.898Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:06:50.906Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:06:50.907Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4638, phase=world) -2026-04-27T10:06:50.907Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:06:50.908Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:07:20.912Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:07:20.913Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4650, phase=world) -2026-04-27T10:07:20.913Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:07:20.914Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:07:50.913Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:07:50.914Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4662, phase=world) -2026-04-27T10:07:50.915Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:07:50.915Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:08:20.928Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:08:20.929Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4674, phase=world) -2026-04-27T10:08:20.930Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:08:20.930Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:08:50.933Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:08:50.934Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4686, phase=world) -2026-04-27T10:08:50.935Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:08:50.935Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:09:20.948Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:09:20.949Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4698, phase=world) -2026-04-27T10:09:20.950Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:09:20.950Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:09:50.962Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:09:50.964Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4710, phase=world) -2026-04-27T10:09:50.964Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:09:50.964Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:10:20.969Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:10:20.971Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4722, phase=world) -2026-04-27T10:10:20.971Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:10:20.971Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:10:50.977Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:10:50.978Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4734, phase=world) -2026-04-27T10:10:50.979Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:10:50.979Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:11:20.979Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:11:20.980Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4746, phase=world) -2026-04-27T10:11:20.980Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:11:20.980Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:11:50.983Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:11:50.984Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4758, phase=world) -2026-04-27T10:11:50.984Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:11:50.985Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:12:20.988Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:12:20.990Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4770, phase=world) -2026-04-27T10:12:20.990Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:12:20.990Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:12:50.996Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:12:50.997Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4782, phase=world) -2026-04-27T10:12:50.997Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:12:50.997Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:13:21.001Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:13:21.002Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4794, phase=world) -2026-04-27T10:13:21.003Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:13:21.003Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:13:51.012Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:13:51.013Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4806, phase=world) -2026-04-27T10:13:51.013Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:13:51.014Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:14:21.019Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:14:21.021Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4818, phase=world) -2026-04-27T10:14:21.021Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:14:21.021Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:14:51.028Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:14:51.030Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4830, phase=world) -2026-04-27T10:14:51.030Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:14:51.030Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:15:21.032Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:15:21.033Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4842, phase=world) -2026-04-27T10:15:21.034Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:15:21.034Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:15:51.039Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:15:51.040Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4854, phase=world) -2026-04-27T10:15:51.040Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:15:51.041Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:16:21.052Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:16:21.053Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4866, phase=world) -2026-04-27T10:16:21.053Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:16:21.053Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:16:51.053Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:16:51.054Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4878, phase=world) -2026-04-27T10:16:51.055Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:16:51.055Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:17:21.056Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:17:21.057Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4890, phase=world) -2026-04-27T10:17:21.057Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:17:21.057Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:17:51.059Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:17:51.060Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4902, phase=world) -2026-04-27T10:17:51.060Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:17:51.061Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:18:21.069Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:18:21.070Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4914, phase=world) -2026-04-27T10:18:21.070Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:18:21.071Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:18:51.082Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:18:51.083Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4926, phase=world) -2026-04-27T10:18:51.083Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:18:51.083Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:19:21.083Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:19:21.084Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4938, phase=world) -2026-04-27T10:19:21.084Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:19:21.085Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:19:51.098Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:19:51.100Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4950, phase=world) -2026-04-27T10:19:51.101Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:19:51.101Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:20:21.113Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:20:21.114Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4962, phase=world) -2026-04-27T10:20:21.114Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:20:21.115Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:20:51.125Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:20:51.127Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4974, phase=world) -2026-04-27T10:20:51.127Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:20:51.127Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:21:21.139Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:21:21.140Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4986, phase=world) -2026-04-27T10:21:21.141Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:21:21.141Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:21:51.152Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:21:51.153Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=4998, phase=world) -2026-04-27T10:21:51.153Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:21:51.154Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:22:21.155Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:22:21.156Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=5010, phase=world) -2026-04-27T10:22:21.156Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:22:21.157Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:22:51.164Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:22:51.165Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=5022, phase=world) -2026-04-27T10:22:51.165Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:22:51.165Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:23:21.167Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:23:21.168Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=5034, phase=world) -2026-04-27T10:23:21.168Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:23:21.168Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:23:51.175Z DEBUG [server:f38959b6] [world] keepalive — sending ping -2026-04-27T10:23:51.176Z DEBUG [server:f38959b6] [world] recv 12 bytes (total=5046, phase=world) -2026-04-27T10:23:51.177Z DEBUG [server:f38959b6] [world] pkt type=0x05 tag=0x0 payloadLen=0 -2026-04-27T10:23:51.177Z DEBUG [server:f38959b6] [world] keepalive response received -2026-04-27T10:24:20.102Z DEBUG [server:f38959b6] [world] recv 20 bytes (total=5066, phase=world) -2026-04-27T10:24:20.103Z DEBUG [server:f38959b6] [world] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T10:24:20.103Z DEBUG [server:f38959b6] [world][rx] -00000000 1b 21 27 23 2c 66 33 1b |.!'#,f3.| -2026-04-27T10:24:20.103Z DEBUG [server:f38959b6] [world] rx type-0 len=8 -00000000 1b 21 27 23 2c 66 33 1b |.!'#,f3.| -2026-04-27T10:24:20.103Z DEBUG [server:f38959b6] [world] client seq=0 cmd=6 -2026-04-27T10:24:20.104Z DEBUG [server:f38959b6] [world] cmd=6 — not yet handled (M3 stub) -2026-04-27T10:24:20.110Z INFO [server:f38959b6] [world] client disconnected (phase=world, bytes=5066) -2026-04-27T10:24:20.111Z INFO [server:f38959b6] [world] notified room of departure: rosterId=100015 callsign="Moose" -2026-04-27T10:49:11.126Z INFO [server:2c39e89c] Client connected from 127.0.0.1:58647 (session 2c39e89c-0de7-4d00-b95f-3a217fc36f92) -2026-04-27T10:49:11.128Z INFO [server:2c39e89c] Sending LOGIN_REQUEST (0x16) — 12 bytes -2026-04-27T10:49:11.130Z DEBUG [server:2c39e89c] recv 349 bytes (total=349, phase=auth) -2026-04-27T10:49:11.130Z DEBUG [server:2c39e89c] pkt type=0x15 tag=0x0 payloadLen=337 -2026-04-27T10:49:11.131Z DEBUG [server:2c39e89c] [rx] -00000000 73 6d 6f 6b 65 5f 73 6f 6c 6f 5f 62 6f 74 5f 61 |smoke_solo_bot_a| -00000010 6c 6c 5f 61 6c 6c 79 5f 67 61 74 65 5f 6d 6f 68 |ll_ally_gate_moh| -00000020 32 72 67 37 71 33 33 32 77 00 00 00 00 00 00 00 |2rg7q332w.......| -00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 20 45 6e 67 69 |Kesmai Comm Engi| -00000080 6e 65 20 33 2e 32 32 00 00 00 00 00 00 00 00 00 |ne 3.22.........| -00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000e0 00 00 00 00 00 00 00 00 42 41 54 54 4c 45 54 45 |........BATTLETE| -000000f0 43 48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |CH..............| -00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 00 |..............9.| -00000140 00 00 0c 00 53 6d 6f 6b 65 50 61 73 73 31 32 33 |....SmokePass123| -00000150 00 |.| -2026-04-27T10:49:11.131Z DEBUG [server:2c39e89c] [auth] login payload len=337 -00000000 73 6d 6f 6b 65 5f 73 6f 6c 6f 5f 62 6f 74 5f 61 |smoke_solo_bot_a| -00000010 6c 6c 5f 61 6c 6c 79 5f 67 61 74 65 5f 6d 6f 68 |ll_ally_gate_moh| -00000020 32 72 67 37 71 33 33 32 77 00 00 00 00 00 00 00 |2rg7q332w.......| -00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000070 4b 65 73 6d 61 69 20 43 6f 6d 6d 20 45 6e 67 69 |Kesmai Comm Engi| -00000080 6e 65 20 33 2e 32 32 00 00 00 00 00 00 00 00 00 |ne 3.22.........| -00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -000000e0 00 00 00 00 00 00 00 00 42 41 54 54 4c 45 54 45 |........BATTLETE| -000000f0 43 48 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |CH..............| -00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| -00000130 00 00 00 00 00 00 00 00 00 00 00 00 07 d0 39 00 |..............9.| -00000140 00 00 0c 00 53 6d 6f 6b 65 50 61 73 73 31 32 33 |....SmokePass123| -00000150 00 |.| -2026-04-27T10:49:11.131Z INFO [server:2c39e89c] [auth] login from "smoke_solo_bot_all_ally_gate_moh2rg7q332w" ver="Kesmai Comm Engine 3.22" service="BATTLETECH" pwLen=12 -2026-04-27T10:49:11.132Z INFO [server:2c39e89c] [login] credentials received: user="smoke_solo_bot_all_ally_gate_moh2rg7q332w" service="BATTLETECH" clientVer="Kesmai Comm Engine 3.22" -2026-04-27T10:49:11.456Z INFO [server:2c39e89c] [login] new account created for "smoke_solo_bot_all_ally_gate_moh2rg7q332w" (id=697) -2026-04-27T10:49:11.456Z INFO [server:2c39e89c] [login] sending SYNC ack — 12 bytes -2026-04-27T10:49:11.457Z INFO [server:2c39e89c] [login] sending WELCOME escape — 45 bytes -2026-04-27T10:49:11.458Z DEBUG [server:2c39e89c] recv 20 bytes (total=369, phase=lobby) -2026-04-27T10:49:11.458Z DEBUG [server:2c39e89c] pkt type=0x00 tag=0x0 payloadLen=8 -2026-04-27T10:49:11.458Z DEBUG [server:2c39e89c] [rx] -00000000 1b 21 24 20 2c 65 6c 1b |.!$ ,el.| -2026-04-27T10:49:11.458Z DEBUG [server:2c39e89c] [game] rx type-0 len=8 -00000000 1b 21 24 20 2c 65 6c 1b |.!$ ,el.| -2026-04-27T10:49:11.459Z DEBUG [server:2c39e89c] [game] client seq=0 cmd=3 phase=lobby -2026-04-27T10:49:11.462Z INFO [server:2c39e89c] [game] no character for account 697 — starting char creation -2026-04-27T10:49:11.462Z INFO [server:2c39e89c] [game] sending Cmd9 character creation prompt (Davion/Steiner/Liao/Marik/Kurita) -2026-04-27T10:49:11.463Z DEBUG [server:2c39e89c] recv 41 bytes (total=410, phase=char-creation) -2026-04-27T10:49:11.464Z DEBUG [server:2c39e89c] pkt type=0x00 tag=0x0 payloadLen=29 -2026-04-27T10:49:11.464Z DEBUG [server:2c39e89c] [rx] -00000000 1b 22 2a 22 33 50 69 6c 6f 74 5f 6d 6f 68 32 72 |."*"3Pilot_moh2r| -00000010 67 37 71 33 33 32 77 25 20 32 60 21 1b |g7q332w% 2`!.| -2026-04-27T10:49:11.465Z DEBUG [server:2c39e89c] [game] rx type-0 len=29 -00000000 1b 22 2a 22 33 50 69 6c 6f 74 5f 6d 6f 68 32 72 |."*"3Pilot_moh2r| -00000010 67 37 71 33 33 32 77 25 20 32 60 21 1b |g7q332w% 2`!.| -2026-04-27T10:49:11.465Z DEBUG [server:2c39e89c] [game] client seq=1 cmd=9 phase=char-creation -2026-04-27T10:49:11.465Z INFO [server:2c39e89c] [game] cmd 9 character reply: subcmd=1 displayName="Pilot_moh2rg7q332w" selection=4 phase=char-creation -2026-04-27T10:49:11.467Z INFO [server:2c39e89c] [game] char-creation Cmd9 accepted: displayName="Pilot_moh2rg7q332w" allegiance=Marik mech=ANH-1A (id=156) → REDIRECT -2026-04-27T10:49:11.468Z INFO [server:2c39e89c] [game] recorded world launch: displayName="Pilot_moh2rg7q332w" allegiance=Marik mech=ANH-1A (id=156) -2026-04-27T10:49:11.468Z INFO [server:2c39e89c] [game] sending REDIRECT → 192.168.1.109:2001 -2026-04-27T10:49:11.986Z ERROR [server:2c39e89c] Socket error: read ECONNRESET -2026-04-27T10:49:11.986Z INFO [server:2c39e89c] Client disconnected (phase=closing, bytes=410)