91b878f347
- Added 6 Quaternius weapon models (Pistol, Revolver, Rifle, Shotgun, P90, SniperRifle) - Created weapon resource files with balanced stats - Wired Pistol + Rifle into player.tscn replacing Kenney blasters - Fixed WASD input (matched input map action names) - Fixed Escape key toggles mouse capture - Added Audio autoload singleton - Fixed broken sound asset paths across all scripts - Fixed all scene ext_resource text paths (nested under assets/kenney-fps/) - Fixed all .import source_file paths - Deleted stale .import files (will be regenerated by editor on open)
78 lines
3.1 KiB
JavaScript
78 lines
3.1 KiB
JavaScript
/**
|
|
* Wire format shared between the Node-side `GodotRunner.sendCommand` and the
|
|
* GDScript-side `McpBridge` autoload.
|
|
*
|
|
* KEEP IN SYNC: src/scripts/mcp_bridge.gd implements the same framing on the
|
|
* Godot side. Any change here MUST be mirrored there (and vice versa).
|
|
*
|
|
* Frame: 4-byte big-endian length prefix + UTF-8 JSON payload.
|
|
* Max frame size is 16 MiB; oversize frames are rejected on receive.
|
|
*/
|
|
import * as net from 'net';
|
|
export const DEFAULT_BRIDGE_PORT = 9900;
|
|
export const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
export const FRAME_HEADER_BYTES = 4;
|
|
/**
|
|
* Find an available TCP port by binding to port 0 (OS-assigned ephemeral port),
|
|
* reading the assigned port, and closing the listener. The brief TOCTOU window
|
|
* between close and the consumer's listen is acceptable — if a collision occurs,
|
|
* the bridge readiness check will surface the failure.
|
|
*/
|
|
export function findFreePort() {
|
|
return new Promise((resolve, reject) => {
|
|
const srv = net.createServer();
|
|
srv.listen(0, '127.0.0.1', () => {
|
|
const addr = srv.address();
|
|
if (!addr || typeof addr === 'string') {
|
|
srv.close();
|
|
reject(new Error('Failed to determine assigned port'));
|
|
return;
|
|
}
|
|
const port = addr.port;
|
|
srv.close(() => resolve(port));
|
|
});
|
|
// One-shot: only fires before listen() succeeds. If listen succeeded,
|
|
// we proceed to srv.close() in the listening callback — a later error
|
|
// is not possible from this server, so the listener stays safely dormant.
|
|
srv.on('error', reject);
|
|
});
|
|
}
|
|
/**
|
|
* Encode a JSON string as a length-prefixed frame.
|
|
*/
|
|
export function encodeFrame(payload) {
|
|
const body = Buffer.from(payload, 'utf8');
|
|
if (body.length > MAX_FRAME_BYTES) {
|
|
throw new Error(`Bridge frame too large: ${body.length} bytes (limit ${MAX_FRAME_BYTES})`);
|
|
}
|
|
const frame = Buffer.allocUnsafe(FRAME_HEADER_BYTES + body.length);
|
|
frame.writeUInt32BE(body.length, 0);
|
|
body.copy(frame, FRAME_HEADER_BYTES);
|
|
return frame;
|
|
}
|
|
/**
|
|
* Pull as many complete frames as possible from a streaming buffer. Any
|
|
* partial frame at the tail is returned as `remainder` for the next call.
|
|
*
|
|
* Throws if a header advertises a payload larger than {@link MAX_FRAME_BYTES} —
|
|
* the caller should treat this as a fatal protocol error and close the socket.
|
|
*/
|
|
export function parseFrames(buffer) {
|
|
const frames = [];
|
|
let offset = 0;
|
|
while (buffer.length - offset >= FRAME_HEADER_BYTES) {
|
|
const len = buffer.readUInt32BE(offset);
|
|
if (len > MAX_FRAME_BYTES) {
|
|
throw new Error(`Bridge frame header advertises ${len} bytes, exceeds limit ${MAX_FRAME_BYTES}`);
|
|
}
|
|
const frameStart = offset + FRAME_HEADER_BYTES;
|
|
const frameEnd = frameStart + len;
|
|
if (buffer.length < frameEnd)
|
|
break;
|
|
frames.push(buffer.subarray(frameStart, frameEnd));
|
|
offset = frameEnd;
|
|
}
|
|
const remainder = offset === 0 ? buffer : buffer.subarray(offset);
|
|
return { frames, remainder };
|
|
}
|
|
//# sourceMappingURL=bridge-protocol.js.map
|