mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-16 00:44:59 +00:00
fix typecheck script, animations
This commit is contained in:
parent
642fce9c06
commit
ceb9fea9f4
120 changed files with 1308 additions and 911 deletions
|
|
@ -35,7 +35,11 @@ export class BitStreamWriter {
|
|||
}
|
||||
|
||||
writeFlag(value: boolean): void {
|
||||
if (this.bitNum >= this.maxBitNum) return;
|
||||
if (this.bitNum >= this.maxBitNum) {
|
||||
throw new RangeError(
|
||||
`BitStreamWriter overflow: writeFlag at position ${this.bitNum}, max ${this.maxBitNum}`,
|
||||
);
|
||||
}
|
||||
if (value) {
|
||||
this.data[this.bitNum >> 3] |= 1 << (this.bitNum & 0x7);
|
||||
} else {
|
||||
|
|
@ -47,6 +51,11 @@ export class BitStreamWriter {
|
|||
/** Write N bits from an unsigned integer, LSB-first. */
|
||||
writeInt(value: number, bitCount: number): void {
|
||||
if (bitCount === 0) return;
|
||||
if (this.bitNum + bitCount > this.maxBitNum) {
|
||||
throw new RangeError(
|
||||
`BitStreamWriter overflow: need ${bitCount} bits at position ${this.bitNum}, max ${this.maxBitNum}`,
|
||||
);
|
||||
}
|
||||
value = value >>> 0;
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
if (value & (1 << i)) {
|
||||
|
|
@ -117,6 +126,11 @@ export class BitStreamWriter {
|
|||
|
||||
/** Write raw bits from a Uint8Array. */
|
||||
writeBitsBuffer(data: Uint8Array, bitCount: number): void {
|
||||
if (this.bitNum + bitCount > this.maxBitNum) {
|
||||
throw new RangeError(
|
||||
`BitStreamWriter overflow: need ${bitCount} bits at position ${this.bitNum}, max ${this.maxBitNum}`,
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < bitCount; i++) {
|
||||
const byteIndex = i >> 3;
|
||||
const bitIndex = i & 0x7;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
/** Warrior name to send in the ConnectRequest. */
|
||||
private warriorName: string;
|
||||
|
||||
/** The server address as "host:port". */
|
||||
get address(): string {
|
||||
return `${this.host}:${this.port}`;
|
||||
}
|
||||
|
||||
constructor(address: string, options?: { warriorName?: string }) {
|
||||
super();
|
||||
const [host, portStr] = address.split(":");
|
||||
|
|
@ -373,7 +378,6 @@ 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;
|
||||
|
|
@ -413,6 +417,16 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
|
||||
/** Handle a data protocol packet (established connection). */
|
||||
private handleDataPacket(msg: Buffer): void {
|
||||
// Ignore data packets before the connection is fully established.
|
||||
// They arrive in the window between socket creation and ConnectAccept
|
||||
// and would confuse both the browser parser and our ack tracking.
|
||||
if (
|
||||
this._status !== "connected" &&
|
||||
this._status !== "authenticating"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new Uint8Array(msg.buffer, msg.byteOffset, msg.byteLength);
|
||||
|
||||
this.dataPacketCount++;
|
||||
|
|
@ -482,11 +496,11 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
);
|
||||
}
|
||||
|
||||
// Measure RTT from the acked sequence's send timestamp.
|
||||
const sendTime = this.sendTimestamps.get(highestAck);
|
||||
// Measure RTT from the acked sequence's send timestamp (full 32-bit key).
|
||||
const sendTime = this.sendTimestamps.get(result.highestAck);
|
||||
if (sendTime) {
|
||||
const rtt = Date.now() - sendTime;
|
||||
this.sendTimestamps.delete(highestAck);
|
||||
this.sendTimestamps.delete(result.highestAck);
|
||||
// Exponential moving average (alpha=0.5 for responsive updates).
|
||||
this.smoothedPing =
|
||||
this.smoothedPing === 0 ? rtt : this.smoothedPing * 0.5 + rtt * 0.5;
|
||||
|
|
@ -732,9 +746,10 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
moves: ClientMoveData[],
|
||||
moveStartIndex: number,
|
||||
): void {
|
||||
// Record send time for RTT measurement.
|
||||
const nextSeq9 = (this.protocol.lastSendSeq + 1) & 0x1ff;
|
||||
this.sendTimestamps.set(nextSeq9, Date.now());
|
||||
// Record send time for RTT measurement using the full 32-bit sequence
|
||||
// (not the 9-bit wire value) to avoid stale timestamps after wrap.
|
||||
const nextSeqFull = (this.protocol.lastSendSeq + 1) >>> 0;
|
||||
this.sendTimestamps.set(nextSeqFull, Date.now());
|
||||
|
||||
// Absorb any new pending events into the send queue.
|
||||
for (const event of this.pendingEvents.splice(0)) {
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ async function queryServers(addresses: string[]): Promise<ServerInfo[]> {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on("error", () => {
|
||||
socket.on("error", (err) => {
|
||||
masterLog.warn({ err }, "Socket error during Phase 1");
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
|
|
@ -176,7 +177,8 @@ async function queryServers(addresses: string[]): Promise<ServerInfo[]> {
|
|||
.map(([addr]) => addr);
|
||||
|
||||
if (compatibleAddrs.length > 0) {
|
||||
socket.removeAllListeners("message");
|
||||
// Clear all Phase 1 listeners (including stale error handler).
|
||||
socket.removeAllListeners();
|
||||
|
||||
masterLog.debug(
|
||||
{ count: compatibleAddrs.length },
|
||||
|
|
@ -186,6 +188,12 @@ async function queryServers(addresses: string[]): Promise<ServerInfo[]> {
|
|||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => resolve(), PHASE_TIMEOUT_MS);
|
||||
|
||||
socket.on("error", (err) => {
|
||||
masterLog.warn({ err }, "Socket error during Phase 2");
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
|
||||
socket.on("message", (msg, rinfo) => {
|
||||
const addr = resolveAddr(rinfo);
|
||||
if (msg[0] === 20) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { BitStreamWriter } from "./BitStreamWriter.js";
|
||||
import { packNetString, writeString } from "./HuffmanWriter.js";
|
||||
import { connLog } from "./logger.js";
|
||||
|
||||
const DataPacket = 0;
|
||||
const PingPacket = 1;
|
||||
|
|
@ -68,13 +69,17 @@ export class ConnectionProtocol {
|
|||
|
||||
this._sendCount++;
|
||||
if (this._sendCount <= 30 || this._sendCount % 50 === 0) {
|
||||
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"})`,
|
||||
connLog.debug(
|
||||
{
|
||||
n: this._sendCount,
|
||||
seq: this.lastSendSeq,
|
||||
ack: this.lastSeqRecvd,
|
||||
type: packetType === 0 ? "data" : packetType === 1 ? "ping" : "ack",
|
||||
ackBytes: ackByteCount,
|
||||
mask: `0x${mask.toString(16).padStart(8, "0")}`,
|
||||
},
|
||||
"Sent packet #%d",
|
||||
this._sendCount,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -89,12 +94,12 @@ export class ConnectionProtocol {
|
|||
connectSeqBit: number;
|
||||
ackByteCount: number;
|
||||
ackMask: number;
|
||||
}): { accepted: boolean; dispatchData: boolean } {
|
||||
}): { accepted: boolean; dispatchData: boolean; highestAck: number } {
|
||||
if (header.connectSeqBit !== (this.connectSequence & 1)) {
|
||||
return { accepted: false, dispatchData: false };
|
||||
return { accepted: false, dispatchData: false, highestAck: 0 };
|
||||
}
|
||||
if (header.ackByteCount > 4 || header.packetType > 2) {
|
||||
return { accepted: false, dispatchData: false };
|
||||
return { accepted: false, dispatchData: false, highestAck: 0 };
|
||||
}
|
||||
|
||||
let seqNumber =
|
||||
|
|
@ -103,7 +108,7 @@ export class ConnectionProtocol {
|
|||
seqNumber = (seqNumber + 0x200) >>> 0;
|
||||
}
|
||||
if (this.lastSeqRecvd + 0x1f < seqNumber) {
|
||||
return { accepted: false, dispatchData: false };
|
||||
return { accepted: false, dispatchData: false, highestAck: 0 };
|
||||
}
|
||||
|
||||
let highestAck =
|
||||
|
|
@ -112,7 +117,7 @@ export class ConnectionProtocol {
|
|||
highestAck = (highestAck + 0x200) >>> 0;
|
||||
}
|
||||
if (this.lastSendSeq < highestAck) {
|
||||
return { accepted: false, dispatchData: false };
|
||||
return { accepted: false, dispatchData: false, highestAck: 0 };
|
||||
}
|
||||
|
||||
const seqShift = (seqNumber - this.lastSeqRecvd) & 0x1f;
|
||||
|
|
@ -144,7 +149,7 @@ export class ConnectionProtocol {
|
|||
this.lastSeqRecvd !== seqNumber && header.packetType === DataPacket;
|
||||
this.lastSeqRecvd = seqNumber;
|
||||
|
||||
return { accepted: true, dispatchData };
|
||||
return { accepted: true, dispatchData, highestAck };
|
||||
}
|
||||
|
||||
/** Build a ping response packet. */
|
||||
|
|
|
|||
|
|
@ -87,7 +87,11 @@ setInterval(() => {
|
|||
wsClients,
|
||||
gameConns,
|
||||
);
|
||||
}, 10_000);
|
||||
if (gameConns > 0) {
|
||||
const addrs = [...activeGameConnections].map((c) => c.address);
|
||||
relayLog.debug({ connections: addrs }, "Active game connections: %s", addrs.join(", "));
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
wss.on("connection", (ws) => {
|
||||
relayLog.info("Browser client connected");
|
||||
|
|
@ -120,13 +124,14 @@ wss.on("connection", (ws) => {
|
|||
gameConnection.setMapName(cachedServer.mapName);
|
||||
}
|
||||
|
||||
const conn = gameConnection;
|
||||
gameConnection.on("status", (status, statusMessage) => {
|
||||
relayLog.info(
|
||||
{
|
||||
status,
|
||||
statusMessage,
|
||||
connectSequence: gameConnection?.connectSequence,
|
||||
mapName: gameConnection?.mapName,
|
||||
connectSequence: conn.connectSequence,
|
||||
mapName: conn.mapName,
|
||||
},
|
||||
"Game connection status changed",
|
||||
);
|
||||
|
|
@ -152,13 +157,19 @@ wss.on("connection", (ws) => {
|
|||
type: "status",
|
||||
status: "connecting",
|
||||
message: `${statusMessage} — retrying (${retryCount}/${MAX_RETRIES})...`,
|
||||
connectSequence: gameConnection?.connectSequence,
|
||||
mapName: gameConnection?.mapName,
|
||||
connectSequence: conn.connectSequence,
|
||||
mapName: conn.mapName,
|
||||
});
|
||||
retryTimer = setTimeout(() => {
|
||||
retryTimer = null;
|
||||
if (lastJoinAddress === address && ws.readyState === WebSocket.OPEN) {
|
||||
connectToServer(ws, address, lastWarriorName);
|
||||
connectToServer(ws, address, lastWarriorName).catch((err) => {
|
||||
relayLog.error({ err }, "Retry connection failed");
|
||||
sendToClient(ws, {
|
||||
type: "error",
|
||||
message: `Reconnect failed: ${err instanceof Error ? err.message : err}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, RETRY_DELAY_MS);
|
||||
return;
|
||||
|
|
@ -168,8 +179,8 @@ wss.on("connection", (ws) => {
|
|||
type: "status",
|
||||
status,
|
||||
message: statusMessage,
|
||||
connectSequence: gameConnection?.connectSequence,
|
||||
mapName: gameConnection?.mapName,
|
||||
connectSequence: conn.connectSequence,
|
||||
mapName: conn.mapName,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -206,10 +217,10 @@ wss.on("connection", (ws) => {
|
|||
|
||||
gameConnection.on("close", () => {
|
||||
relayLog.info("Game connection closed");
|
||||
if (gameConnection) {
|
||||
activeGameConnections.delete(gameConnection);
|
||||
activeGameConnections.delete(conn);
|
||||
if (gameConnection === conn) {
|
||||
gameConnection = null;
|
||||
}
|
||||
gameConnection = null;
|
||||
});
|
||||
|
||||
await gameConnection.connect();
|
||||
|
|
@ -241,7 +252,6 @@ wss.on("connection", (ws) => {
|
|||
}
|
||||
if (gameConnection) {
|
||||
gameConnection.disconnect();
|
||||
gameConnection = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -336,6 +346,7 @@ wss.on("connection", (ws) => {
|
|||
},
|
||||
"Computing CRC from game files",
|
||||
);
|
||||
// Fire-and-forget: computeAndSendCRC has its own try/catch.
|
||||
gameConnection.computeAndSendCRC(
|
||||
message.seed,
|
||||
message.field2,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue