mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-14 07:56:42 +00:00
new UI, unify map/demo/live architecture more, cleanup
This commit is contained in:
parent
d9b5e30831
commit
4741f59582
146 changed files with 5477 additions and 3005 deletions
|
|
@ -2,32 +2,24 @@ import { BitStreamWriter } from "./BitStreamWriter.js";
|
|||
|
||||
/** Hardcoded character frequency table from the V12 engine (bitStream.cc). */
|
||||
const CSM_CHAR_FREQS: number[] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 329, 21, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
2809, 68, 0, 27, 0, 58, 3, 62, 4, 7, 0, 0, 15, 65, 554, 3,
|
||||
394, 404, 189, 117, 30, 51, 27, 15, 34, 32, 80, 1, 142, 3, 142, 39,
|
||||
0, 144, 125, 44, 122, 275, 70, 135, 61, 127, 8, 12, 113, 246, 122, 36,
|
||||
185, 1, 149, 309, 335, 12, 11, 14, 54, 151, 0, 0, 2, 0, 0, 211,
|
||||
0, 2090, 344, 736, 993, 2872, 701, 605, 646, 1552, 328, 305, 1240, 735, 1533, 1713,
|
||||
562, 3, 1775, 1149, 1469, 979, 407, 553, 59, 279, 31, 0, 0, 0, 68, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 329, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 2809, 68, 0, 27, 0, 58, 3, 62, 4, 7, 0, 0, 15, 65, 554,
|
||||
3, 394, 404, 189, 117, 30, 51, 27, 15, 34, 32, 80, 1, 142, 3, 142, 39, 0, 144,
|
||||
125, 44, 122, 275, 70, 135, 61, 127, 8, 12, 113, 246, 122, 36, 185, 1, 149,
|
||||
309, 335, 12, 11, 14, 54, 151, 0, 0, 2, 0, 0, 211, 0, 2090, 344, 736, 993,
|
||||
2872, 701, 605, 646, 1552, 328, 305, 1240, 735, 1533, 1713, 562, 3, 1775,
|
||||
1149, 1469, 979, 407, 553, 59, 279, 31, 0, 0, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
|
||||
const PROB_BOOST = 1;
|
||||
|
||||
function isAlphaNumeric(c: number): boolean {
|
||||
return (
|
||||
(c >= 48 && c <= 57) ||
|
||||
(c >= 65 && c <= 90) ||
|
||||
(c >= 97 && c <= 122)
|
||||
);
|
||||
return (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122);
|
||||
}
|
||||
|
||||
interface HuffLeaf {
|
||||
|
|
@ -62,7 +54,8 @@ function buildTables(): void {
|
|||
leaves = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
leaves.push({
|
||||
pop: CSM_CHAR_FREQS[i] + (isAlphaNumeric(i) ? PROB_BOOST : 0) + PROB_BOOST,
|
||||
pop:
|
||||
CSM_CHAR_FREQS[i] + (isAlphaNumeric(i) ? PROB_BOOST : 0) + PROB_BOOST,
|
||||
symbol: i,
|
||||
numBits: 0,
|
||||
code: 0,
|
||||
|
|
|
|||
|
|
@ -317,7 +317,10 @@ export class T2csriAuth {
|
|||
// Sanitize: must be hex only
|
||||
const challenge = this.encryptedChallenge.toLowerCase();
|
||||
authLog.info(
|
||||
{ challengeLen: challenge.length, clientChallengeLen: this.clientChallenge.length },
|
||||
{
|
||||
challengeLen: challenge.length,
|
||||
clientChallengeLen: this.clientChallenge.length,
|
||||
},
|
||||
"Auth: starting challenge decryption",
|
||||
);
|
||||
for (let i = 0; i < challenge.length; i++) {
|
||||
|
|
@ -339,7 +342,11 @@ export class T2csriAuth {
|
|||
const modulusHex = fields[3];
|
||||
|
||||
authLog.debug(
|
||||
{ encryptedLen: challenge.length, modulusLen: modulusHex?.length, privateKeyLen: this.credentials.privateKey.length },
|
||||
{
|
||||
encryptedLen: challenge.length,
|
||||
modulusLen: modulusHex?.length,
|
||||
privateKeyLen: this.credentials.privateKey.length,
|
||||
},
|
||||
"Auth: RSA parameters",
|
||||
);
|
||||
|
||||
|
|
|
|||
12
relay/crc.ts
12
relay/crc.ts
|
|
@ -49,7 +49,9 @@ export interface CRCDataBlock {
|
|||
}
|
||||
|
||||
// Manifest types (mirrored from src/manifest.ts)
|
||||
type SourceTuple = [sourcePath: string] | [sourcePath: string, actualPath: string];
|
||||
type SourceTuple =
|
||||
| [sourcePath: string]
|
||||
| [sourcePath: string, actualPath: string];
|
||||
type ResourceEntry = [firstSeenPath: string, ...SourceTuple[]];
|
||||
interface Manifest {
|
||||
resources: Record<string, ResourceEntry>;
|
||||
|
|
@ -120,8 +122,8 @@ export async function computeGameCRC(
|
|||
|
||||
console.log(
|
||||
`[crc] starting computation: seed=0x${(seed >>> 0).toString(16)}, ` +
|
||||
`${sorted.length} ShapeBaseData datablocks (of ${datablocks.length} total), ` +
|
||||
`includeTextures=${includeTextures}`,
|
||||
`${sorted.length} ShapeBaseData datablocks (of ${datablocks.length} total), ` +
|
||||
`includeTextures=${includeTextures}`,
|
||||
);
|
||||
|
||||
for (const db of sorted) {
|
||||
|
|
@ -153,7 +155,7 @@ export async function computeGameCRC(
|
|||
|
||||
console.log(
|
||||
`[crc] #${filesFound} id=${db.objectId} ${db.className} "${db.shapeName}" ` +
|
||||
`size=${data.length} crc=0x${prevCrc.toString(16)}→0x${crc.toString(16)}`,
|
||||
`size=${data.length} crc=0x${prevCrc.toString(16)}→0x${crc.toString(16)}`,
|
||||
);
|
||||
|
||||
// TODO: If includeTextures && db.className !== "PlayerData",
|
||||
|
|
@ -172,7 +174,7 @@ export async function computeGameCRC(
|
|||
|
||||
console.log(
|
||||
`[crc] RESULT: ${filesFound} files CRC'd, ${filesMissing} missing, ` +
|
||||
`crc=0x${crc.toString(16)}, totalSize=${totalSize}, elapsed=${elapsed.toFixed(0)}ms`,
|
||||
`crc=0x${crc.toString(16)}, totalSize=${totalSize}, elapsed=${elapsed.toFixed(0)}ms`,
|
||||
);
|
||||
|
||||
return { crc, totalSize };
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
private nextSendEventSeq = 0;
|
||||
private pendingEvents: ClientEvent[] = [];
|
||||
/** Events sent but not yet acked, keyed by packet sequence number. */
|
||||
private sentEventsByPacket = new Map<number, { seq: number; event: ClientEvent }[]>();
|
||||
private sentEventsByPacket = new Map<
|
||||
number,
|
||||
{ seq: number; event: ClientEvent }[]
|
||||
>();
|
||||
/** Events waiting to be sent (new or retransmitted from lost packets). */
|
||||
private eventSendQueue: { seq: number; event: ClientEvent }[] = [];
|
||||
private stringTable = new ClientNetStringTable();
|
||||
|
|
@ -178,7 +181,11 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
this.rawMessageCount++;
|
||||
if (this.rawMessageCount <= 30 || this.rawMessageCount % 50 === 0) {
|
||||
connLog.debug(
|
||||
{ bytes: msg.length, firstByte: msg[0], rawTotal: this.rawMessageCount },
|
||||
{
|
||||
bytes: msg.length,
|
||||
firstByte: msg[0],
|
||||
rawTotal: this.rawMessageCount,
|
||||
},
|
||||
"Raw UDP message received",
|
||||
);
|
||||
}
|
||||
|
|
@ -225,11 +232,16 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
case 36: // ConnectAccept
|
||||
this.handleConnectAccept(msg);
|
||||
break;
|
||||
case 38: { // Disconnect — U8(type) + U32(seq1) + U32(seq2) + HuffString(reason)
|
||||
case 38: {
|
||||
// Disconnect — U8(type) + U32(seq1) + U32(seq2) + HuffString(reason)
|
||||
let reason = "Server disconnected";
|
||||
if (msg.length > 9) {
|
||||
try {
|
||||
const data = new Uint8Array(msg.buffer, msg.byteOffset, msg.byteLength);
|
||||
const data = new Uint8Array(
|
||||
msg.buffer,
|
||||
msg.byteOffset,
|
||||
msg.byteLength,
|
||||
);
|
||||
// Skip 9-byte header (1 type + 4 connectSeq + 4 connectSeq2).
|
||||
// Reason is Huffman-encoded via BitStream::writeString (no stringBuffer).
|
||||
const bs = new BitStream(data.subarray(9));
|
||||
|
|
@ -239,7 +251,10 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
// Fall back to default reason
|
||||
}
|
||||
}
|
||||
connLog.warn({ reason, bytes: msg.length }, "Server sent Disconnect packet");
|
||||
connLog.warn(
|
||||
{ reason, bytes: msg.length },
|
||||
"Server sent Disconnect packet",
|
||||
);
|
||||
this.setStatus("disconnected", reason);
|
||||
this.disconnect();
|
||||
break;
|
||||
|
|
@ -249,16 +264,24 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
}
|
||||
}
|
||||
|
||||
/** Handle ChallengeReject (type 28): U8(28) + U32(connectSeq) + ASCII reason. */
|
||||
/** Handle ChallengeReject (type 28): U8(28) + U32(clientSeq) + HuffString(reason). */
|
||||
private handleChallengeReject(msg: Buffer): void {
|
||||
if (msg.length < 5) return;
|
||||
const dv = new DataView(msg.buffer, msg.byteOffset, msg.byteLength);
|
||||
const seq = dv.getUint32(1, true);
|
||||
if (seq !== this.clientConnectSequence) {
|
||||
connLog.debug({ expected: this.clientConnectSequence, got: seq }, "ChallengeReject sequence mismatch, ignoring");
|
||||
return;
|
||||
}
|
||||
let reason = "Challenge rejected";
|
||||
if (msg.length > 5) {
|
||||
const chars: number[] = [];
|
||||
for (let i = 5; i < msg.length && msg[i] !== 0; i++) {
|
||||
chars.push(msg[i]);
|
||||
}
|
||||
if (chars.length > 0) {
|
||||
reason = String.fromCharCode(...chars);
|
||||
try {
|
||||
const data = new Uint8Array(msg.buffer, msg.byteOffset, msg.byteLength);
|
||||
const bs = new BitStream(data.subarray(5));
|
||||
const parsed = bs.readString();
|
||||
if (parsed) reason = parsed;
|
||||
} catch {
|
||||
// Fall back to default reason
|
||||
}
|
||||
}
|
||||
connLog.warn({ reason }, "ChallengeReject received");
|
||||
|
|
@ -269,18 +292,11 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
/** Handle ConnectChallengeResponse. */
|
||||
private handleChallengeResponse(msg: Buffer): void {
|
||||
if (msg.length < 14) {
|
||||
connLog.error(
|
||||
{ bytes: msg.length },
|
||||
"ChallengeResponse too short",
|
||||
);
|
||||
connLog.error({ bytes: msg.length }, "ChallengeResponse too short");
|
||||
return;
|
||||
}
|
||||
|
||||
const dv = new DataView(
|
||||
msg.buffer,
|
||||
msg.byteOffset,
|
||||
msg.byteLength,
|
||||
);
|
||||
const dv = new DataView(msg.buffer, msg.byteOffset, msg.byteLength);
|
||||
const serverProtocolVersion = dv.getUint32(1, true);
|
||||
this.serverConnectSequence = dv.getUint32(5, true);
|
||||
const echoedClientSeq = dv.getUint32(9, true);
|
||||
|
|
@ -354,14 +370,30 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
}
|
||||
|
||||
/** Handle ConnectReject. */
|
||||
/** Handle ConnectReject (type 34): U8(34) + U32(serverSeq) + U32(clientSeq) + HuffString(reason). */
|
||||
private handleConnectReject(msg: Buffer): void {
|
||||
if (msg.length < 9) return;
|
||||
const dv = new DataView(msg.buffer, msg.byteOffset, msg.byteLength);
|
||||
const serverSeq = dv.getUint32(1, true);
|
||||
const clientSeq = dv.getUint32(5, true);
|
||||
if (serverSeq !== this.serverConnectSequence || clientSeq !== this.clientConnectSequence) {
|
||||
connLog.debug(
|
||||
{ expectedServer: this.serverConnectSequence, gotServer: serverSeq,
|
||||
expectedClient: this.clientConnectSequence, gotClient: clientSeq },
|
||||
"ConnectReject sequence mismatch, ignoring",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let reason = "Connection rejected";
|
||||
if (msg.length > 1) {
|
||||
const chars: number[] = [];
|
||||
for (let i = 1; i < msg.length && msg[i] !== 0; i++) {
|
||||
chars.push(msg[i]);
|
||||
if (msg.length > 9) {
|
||||
try {
|
||||
const data = new Uint8Array(msg.buffer, msg.byteOffset, msg.byteLength);
|
||||
const bs = new BitStream(data.subarray(9));
|
||||
const parsed = bs.readString();
|
||||
if (parsed) reason = parsed;
|
||||
} catch {
|
||||
// Fall back to default reason
|
||||
}
|
||||
reason = String.fromCharCode(...chars);
|
||||
}
|
||||
connLog.warn({ reason }, "ConnectReject received");
|
||||
this.setStatus("disconnected", reason);
|
||||
|
|
@ -385,7 +417,6 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
|
||||
// We still need to process the dnet header locally to track ack state
|
||||
this.processPacketForAcks(data);
|
||||
|
||||
}
|
||||
|
||||
/** Process a packet's dnet header to maintain ack state. */
|
||||
|
|
@ -415,7 +446,10 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
// The server's processRawPacket calls sendPingResponse on receiving a
|
||||
// PingPacket. Without this response, the server may time us out.
|
||||
if (packetType === 1) {
|
||||
connLog.debug({ seq: seqNumber }, "Received PingPacket, sending ping response");
|
||||
connLog.debug(
|
||||
{ seq: seqNumber },
|
||||
"Received PingPacket, sending ping response",
|
||||
);
|
||||
const pingResponse = this.protocol.buildPingPacket();
|
||||
this.sendRaw(pingResponse);
|
||||
}
|
||||
|
|
@ -472,19 +506,15 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
}
|
||||
|
||||
/** Handle a parsed T2csri event from the browser. */
|
||||
handleAuthEvent(
|
||||
eventName: string,
|
||||
args: string[],
|
||||
): void {
|
||||
handleAuthEvent(eventName: string, args: string[]): void {
|
||||
if (!this.auth) return;
|
||||
|
||||
switch (eventName) {
|
||||
case "t2csri_pokeClient": {
|
||||
connLog.info("Auth: received pokeClient, sending certificate + challenge");
|
||||
const result = this.auth.onPokeClient(
|
||||
args[0] || "",
|
||||
this.host,
|
||||
connLog.info(
|
||||
"Auth: received pokeClient, sending certificate + challenge",
|
||||
);
|
||||
const result = this.auth.onPokeClient(args[0] || "", this.host);
|
||||
for (const cmd of result.commands) {
|
||||
this.sendCommand(cmd.name, ...cmd.args);
|
||||
}
|
||||
|
|
@ -512,10 +542,7 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
this.authDelayTimer = setTimeout(() => {
|
||||
this.authDelayTimer = null;
|
||||
if (this._status !== "authenticating") return;
|
||||
this.sendCommand(
|
||||
result.command.name,
|
||||
...result.command.args,
|
||||
);
|
||||
this.sendCommand(result.command.name, ...result.command.args);
|
||||
this.enforceObserver();
|
||||
this.setStatus("connected");
|
||||
}, delay);
|
||||
|
|
@ -553,11 +580,20 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
basePath: string,
|
||||
): Promise<void> {
|
||||
connLog.info(
|
||||
{ seed: `0x${(seed >>> 0).toString(16)}`, datablocks: datablocks.length, includeTextures },
|
||||
{
|
||||
seed: `0x${(seed >>> 0).toString(16)}`,
|
||||
datablocks: datablocks.length,
|
||||
includeTextures,
|
||||
},
|
||||
"Computing CRC over game files",
|
||||
);
|
||||
try {
|
||||
const { crc, totalSize } = await computeGameCRC(seed, datablocks, basePath, includeTextures);
|
||||
const { crc, totalSize } = await computeGameCRC(
|
||||
seed,
|
||||
datablocks,
|
||||
basePath,
|
||||
includeTextures,
|
||||
);
|
||||
connLog.info(
|
||||
{ crc: `0x${(crc >>> 0).toString(16)}`, totalSize },
|
||||
"CRC computed, sending response",
|
||||
|
|
@ -586,7 +622,10 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
|
||||
/** Send a commandToServer as a RemoteCommandEvent. */
|
||||
sendCommand(command: string, ...args: string[]): void {
|
||||
connLog.debug({ command, args, eventSeq: this.nextSendEventSeq }, "Sending commandToServer");
|
||||
connLog.debug(
|
||||
{ command, args, eventSeq: this.nextSendEventSeq },
|
||||
"Sending commandToServer",
|
||||
);
|
||||
const events = buildRemoteCommandEvent(this.stringTable, command, ...args);
|
||||
this.pendingEvents.push(...events);
|
||||
this.flushEvents();
|
||||
|
|
@ -608,9 +647,7 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
* Build and send a data packet that includes events from the send queue.
|
||||
* Events stay tracked per-packet so they can be re-queued on loss.
|
||||
*/
|
||||
private sendDataPacketWithEvents(
|
||||
move?: ClientMoveData,
|
||||
): void {
|
||||
private sendDataPacketWithEvents(move?: ClientMoveData): void {
|
||||
const events = this.eventSendQueue.splice(0);
|
||||
if (events.length === 0) return;
|
||||
|
||||
|
|
@ -631,8 +668,12 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
this.sentEventsByPacket.set(packetSeq, events);
|
||||
|
||||
const moveData = move ?? {
|
||||
x: 0, y: 0, z: 0,
|
||||
yaw: 0, pitch: 0, roll: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
yaw: 0,
|
||||
pitch: 0,
|
||||
roll: 0,
|
||||
freeLook: false,
|
||||
trigger: [false, false, false, false, false, false],
|
||||
};
|
||||
|
|
@ -702,7 +743,14 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
this.sendMoveCount++;
|
||||
if (this.sendMoveCount <= 5 || this.sendMoveCount % 100 === 0) {
|
||||
connLog.debug(
|
||||
{ yaw: move.yaw, pitch: move.pitch, x: move.x, y: move.y, z: move.z, total: this.sendMoveCount },
|
||||
{
|
||||
yaw: move.yaw,
|
||||
pitch: move.pitch,
|
||||
x: move.x,
|
||||
y: move.y,
|
||||
z: move.z,
|
||||
total: this.sendMoveCount,
|
||||
},
|
||||
"Sending move",
|
||||
);
|
||||
}
|
||||
|
|
@ -779,7 +827,8 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
let keepaliveCount = 0;
|
||||
this.keepaliveTimer = setInterval(() => {
|
||||
keepaliveCount++;
|
||||
if (keepaliveCount % 300 === 0) { // ~10s at 32ms tick rate
|
||||
if (keepaliveCount % 300 === 0) {
|
||||
// ~10s at 32ms tick rate
|
||||
connLog.info(
|
||||
{
|
||||
dataPackets: this.dataPacketCount,
|
||||
|
|
|
|||
|
|
@ -133,7 +133,12 @@ async function queryServers(addresses: string[]): Promise<ServerInfo[]> {
|
|||
if (info) {
|
||||
pingResults.set(addr, info);
|
||||
masterLog.debug(
|
||||
{ addr, name: info.name, build: info.buildVersion, ping: info.ping },
|
||||
{
|
||||
addr,
|
||||
name: info.name,
|
||||
build: info.buildVersion,
|
||||
ping: info.ping,
|
||||
},
|
||||
"Ping response",
|
||||
);
|
||||
}
|
||||
|
|
@ -246,10 +251,7 @@ async function queryServers(addresses: string[]): Promise<ServerInfo[]> {
|
|||
* U32 buildVersion (e.g. 25034)
|
||||
* HuffString serverName (24 chars max)
|
||||
*/
|
||||
function parsePingResponse(
|
||||
data: Buffer,
|
||||
sendTime?: number,
|
||||
): PingInfo | null {
|
||||
function parsePingResponse(data: Buffer, sendTime?: number): PingInfo | null {
|
||||
if (data.length < 7 || data[0] !== 16) return null;
|
||||
try {
|
||||
const bs = new BitStream(
|
||||
|
|
@ -301,7 +303,15 @@ function parseInfoResponse(data: Buffer): GameInfo | null {
|
|||
const playerCount = bs.readU8();
|
||||
const maxPlayers = bs.readU8();
|
||||
const botCount = bs.readU8();
|
||||
return { mod, gameType, mapName, status, playerCount, maxPlayers, botCount };
|
||||
return {
|
||||
mod,
|
||||
gameType,
|
||||
mapName,
|
||||
status,
|
||||
playerCount,
|
||||
maxPlayers,
|
||||
botCount,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,7 @@ export class ConnectionProtocol {
|
|||
|
||||
private _sendCount = 0;
|
||||
|
||||
buildSendPacketHeader(
|
||||
packetType: number = DataPacket,
|
||||
): BitStreamWriter {
|
||||
buildSendPacketHeader(packetType: number = DataPacket): BitStreamWriter {
|
||||
const bs = new BitStreamWriter(1500);
|
||||
|
||||
// gameFlag — always true for data connection packets
|
||||
|
|
@ -42,8 +40,7 @@ export class ConnectionProtocol {
|
|||
|
||||
// Increment send sequence
|
||||
this.lastSendSeq = (this.lastSendSeq + 1) >>> 0;
|
||||
this.lastSeqRecvdAtSend[this.lastSendSeq & 0x1f] =
|
||||
this.lastSeqRecvd >>> 0;
|
||||
this.lastSeqRecvdAtSend[this.lastSendSeq & 0x1f] = this.lastSeqRecvd >>> 0;
|
||||
|
||||
// seqNumber (9 bits)
|
||||
bs.writeInt(this.lastSendSeq & 0x1ff, 9);
|
||||
|
|
@ -71,12 +68,13 @@ export class ConnectionProtocol {
|
|||
|
||||
this._sendCount++;
|
||||
if (this._sendCount <= 30 || this._sendCount % 50 === 0) {
|
||||
const typeName = packetType === 0 ? "data" : packetType === 1 ? "ping" : "ack";
|
||||
const typeName =
|
||||
packetType === 0 ? "data" : packetType === 1 ? "ping" : "ack";
|
||||
console.log(
|
||||
`[proto] SEND #${this._sendCount} seq=${this.lastSendSeq} ` +
|
||||
`highestAck=${this.lastSeqRecvd} type=${typeName} ` +
|
||||
`ackBytes=${ackByteCount} mask=0x${mask.toString(16).padStart(8, "0")} ` +
|
||||
`(${mask.toString(2).replace(/^0+/, "") || "0"})`,
|
||||
`highestAck=${this.lastSeqRecvd} type=${typeName} ` +
|
||||
`ackBytes=${ackByteCount} mask=0x${mask.toString(16).padStart(8, "0")} ` +
|
||||
`(${mask.toString(2).replace(/^0+/, "") || "0"})`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -131,8 +129,7 @@ export class ConnectionProtocol {
|
|||
const isAcked =
|
||||
(header.ackMask & (1 << ((highestAck - ackSeq) & 0x1f))) !== 0;
|
||||
if (isAcked) {
|
||||
this.lastRecvAckAck =
|
||||
this.lastSeqRecvdAtSend[ackSeq & 0x1f] >>> 0;
|
||||
this.lastRecvAckAck = this.lastSeqRecvdAtSend[ackSeq & 0x1f] >>> 0;
|
||||
}
|
||||
if (this.onNotify) {
|
||||
this.onNotify(ackSeq, isAcked);
|
||||
|
|
@ -144,8 +141,7 @@ export class ConnectionProtocol {
|
|||
this.highestAckedSeq = highestAck;
|
||||
|
||||
const dispatchData =
|
||||
this.lastSeqRecvd !== seqNumber &&
|
||||
header.packetType === DataPacket;
|
||||
this.lastSeqRecvd !== seqNumber && header.packetType === DataPacket;
|
||||
this.lastSeqRecvd = seqNumber;
|
||||
|
||||
return { accepted: true, dispatchData };
|
||||
|
|
@ -168,9 +164,7 @@ export class ConnectionProtocol {
|
|||
* The caller provides a callback that writes game data to the stream
|
||||
* after the dnet header.
|
||||
*/
|
||||
buildDataPacket(
|
||||
writePayload: (bs: BitStreamWriter) => void,
|
||||
): Uint8Array {
|
||||
buildDataPacket(writePayload: (bs: BitStreamWriter) => void): Uint8Array {
|
||||
const bs = this.buildSendPacketHeader(DataPacket);
|
||||
writePayload(bs);
|
||||
return bs.getBuffer();
|
||||
|
|
@ -343,10 +337,7 @@ export class ClientNetStringTable {
|
|||
}
|
||||
|
||||
/** Build a NetStringEvent to register a string with the server. */
|
||||
export function buildNetStringEvent(
|
||||
id: number,
|
||||
value: string,
|
||||
): ClientEvent {
|
||||
export function buildNetStringEvent(id: number, value: string): ClientEvent {
|
||||
return {
|
||||
classId: NetStringEventClassId,
|
||||
write(bs: BitStreamWriter) {
|
||||
|
|
@ -489,9 +480,7 @@ export function buildConnectRequest(
|
|||
}
|
||||
|
||||
/** Build a Disconnect (type 38) OOB packet. */
|
||||
export function buildDisconnectPacket(
|
||||
connectSequence: number,
|
||||
): Uint8Array {
|
||||
export function buildDisconnectPacket(connectSequence: number): Uint8Array {
|
||||
const bs = new BitStreamWriter(64);
|
||||
bs.writeU8(38); // Disconnect type
|
||||
bs.writeU32(connectSequence);
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ const MANIFEST_PATH =
|
|||
path.resolve(GAME_BASE_PATH, "..", "..", "public", "manifest.json");
|
||||
|
||||
const RELAY_PORT = parseInt(process.env.RELAY_PORT || "8765", 10);
|
||||
const MASTER_SERVER =
|
||||
process.env.T2_MASTER_SERVER || "master.tribesnext.com";
|
||||
const MASTER_SERVER = process.env.T2_MASTER_SERVER || "master.tribesnext.com";
|
||||
|
||||
/** HTTP server for health checks; WebSocket upgrades are handled separately. */
|
||||
const httpServer = http.createServer(async (req, res) => {
|
||||
|
|
@ -58,7 +57,9 @@ const httpServer = http.createServer(async (req, res) => {
|
|||
|
||||
const allOk = Object.values(checks).every((c) => c.ok);
|
||||
res.writeHead(allOk ? 200 : 503, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: allOk ? "ok" : "degraded", checks }, null, 2));
|
||||
res.end(
|
||||
JSON.stringify({ status: allOk ? "ok" : "degraded", checks }, null, 2),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +88,11 @@ wss.on("connection", (ws) => {
|
|||
const RETRY_DELAY_MS = 6000;
|
||||
const RETRYABLE_REASONS = ["Server is cycling mission"];
|
||||
|
||||
async function connectToServer(ws: WebSocket, address: string, warriorName?: string): Promise<void> {
|
||||
async function connectToServer(
|
||||
ws: WebSocket,
|
||||
address: string,
|
||||
warriorName?: string,
|
||||
): Promise<void> {
|
||||
if (gameConnection) {
|
||||
gameConnection.disconnect();
|
||||
}
|
||||
|
|
@ -95,9 +100,7 @@ wss.on("connection", (ws) => {
|
|||
gameConnection = new GameConnection(address, { warriorName });
|
||||
|
||||
// Set mapName from the cached server list if available.
|
||||
const cachedServer = cachedServers.find(
|
||||
(s) => s.address === address,
|
||||
);
|
||||
const cachedServer = cachedServers.find((s) => s.address === address);
|
||||
if (cachedServer?.mapName) {
|
||||
gameConnection.setMapName(cachedServer.mapName);
|
||||
}
|
||||
|
|
@ -123,7 +126,11 @@ wss.on("connection", (ws) => {
|
|||
) {
|
||||
retryCount++;
|
||||
relayLog.info(
|
||||
{ attempt: retryCount, maxRetries: MAX_RETRIES, delay: RETRY_DELAY_MS },
|
||||
{
|
||||
attempt: retryCount,
|
||||
maxRetries: MAX_RETRIES,
|
||||
delay: RETRY_DELAY_MS,
|
||||
},
|
||||
"Retryable disconnect — will reconnect",
|
||||
);
|
||||
sendToClient(ws, {
|
||||
|
|
@ -243,7 +250,10 @@ wss.on("connection", (ws) => {
|
|||
}
|
||||
|
||||
case "joinServer": {
|
||||
relayLog.info({ address: message.address, warriorName: message.warriorName }, "Join server requested");
|
||||
relayLog.info(
|
||||
{ address: message.address, warriorName: message.warriorName },
|
||||
"Join server requested",
|
||||
);
|
||||
if (gameConnection) {
|
||||
relayLog.info("Disconnecting existing game connection");
|
||||
gameConnection.disconnect();
|
||||
|
|
@ -285,10 +295,7 @@ wss.on("connection", (ws) => {
|
|||
{ event: message.command },
|
||||
"Forwarding auth event from browser",
|
||||
);
|
||||
gameConnection.handleAuthEvent(
|
||||
message.command,
|
||||
message.args,
|
||||
);
|
||||
gameConnection.handleAuthEvent(message.command, message.args);
|
||||
} else {
|
||||
relayLog.debug(
|
||||
{ command: message.command },
|
||||
|
|
@ -315,7 +322,10 @@ wss.on("connection", (ws) => {
|
|||
case "sendCRCCompute": {
|
||||
if (gameConnection) {
|
||||
relayLog.info(
|
||||
{ datablocks: message.datablocks.length, includeTextures: message.includeTextures },
|
||||
{
|
||||
datablocks: message.datablocks.length,
|
||||
includeTextures: message.includeTextures,
|
||||
},
|
||||
"Computing CRC from game files",
|
||||
);
|
||||
gameConnection.computeAndSendCRC(
|
||||
|
|
|
|||
|
|
@ -5,15 +5,32 @@ export type ClientMessage =
|
|||
| { type: "disconnect" }
|
||||
| { type: "sendMove"; move: ClientMove }
|
||||
| { type: "sendCommand"; command: string; args: string[] }
|
||||
| { type: "sendCRCResponse"; crcValue: number; field1: number; field2: number }
|
||||
| { type: "sendCRCCompute"; seed: number; field2: number; includeTextures: boolean; datablocks: { objectId: number; className: string; shapeName: string }[] }
|
||||
| {
|
||||
type: "sendCRCResponse";
|
||||
crcValue: number;
|
||||
field1: number;
|
||||
field2: number;
|
||||
}
|
||||
| {
|
||||
type: "sendCRCCompute";
|
||||
seed: number;
|
||||
field2: number;
|
||||
includeTextures: boolean;
|
||||
datablocks: { objectId: number; className: string; shapeName: string }[];
|
||||
}
|
||||
| { type: "sendGhostAck"; sequence: number; ghostCount: number }
|
||||
| { type: "wsPing"; ts: number };
|
||||
|
||||
/** Messages from relay server to browser client. */
|
||||
export type ServerMessage =
|
||||
| { type: "serverList"; servers: ServerInfo[] }
|
||||
| { type: "status"; status: ConnectionStatus; message?: string; connectSequence?: number; mapName?: string }
|
||||
| {
|
||||
type: "status";
|
||||
status: ConnectionStatus;
|
||||
message?: string;
|
||||
connectSequence?: number;
|
||||
mapName?: string;
|
||||
}
|
||||
| { type: "gamePacket"; data: Uint8Array }
|
||||
| { type: "ping"; ms: number }
|
||||
| { type: "wsPong"; ts: number }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue