mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-16 00:44:59 +00:00
improve observer mode and disconnect
This commit is contained in:
parent
25b4a53a00
commit
78e791f763
39 changed files with 877 additions and 837 deletions
|
|
@ -54,6 +54,7 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
private handshakeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private challengeRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private authDelayTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private disconnectRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
private nextSendEventSeq = 0;
|
||||
private pendingEvents: ClientEvent[] = [];
|
||||
|
|
@ -249,8 +250,14 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
{ reason, bytes: msg.length },
|
||||
"Server sent Disconnect packet",
|
||||
);
|
||||
this.setStatus("disconnected", reason);
|
||||
this.disconnect();
|
||||
if (this._status === "disconnected") {
|
||||
// We initiated the disconnect — this is the server's confirmation.
|
||||
this.onServerDisconnectConfirmed(reason);
|
||||
} else {
|
||||
// Server-initiated disconnect.
|
||||
this.setStatus("disconnected", reason);
|
||||
this.disconnect();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
@ -791,26 +798,49 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
});
|
||||
}
|
||||
|
||||
/** Clean up the socket and emit "close" (idempotent). */
|
||||
private closeSocket(): void {
|
||||
if (this.disconnectRetryTimer) {
|
||||
clearTimeout(this.disconnectRetryTimer);
|
||||
this.disconnectRetryTimer = null;
|
||||
}
|
||||
const hadSocket = this.socket != null;
|
||||
if (this.socket) {
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch {
|
||||
// Already closed
|
||||
}
|
||||
this.socket = null;
|
||||
}
|
||||
// Only emit "close" once — the first call that actually tears down.
|
||||
if (hadSocket) {
|
||||
this.emit("close");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the server confirms our disconnect by sending a type 38
|
||||
* packet back. Cancels any pending retransmissions and closes immediately.
|
||||
*/
|
||||
private onServerDisconnectConfirmed(reason: string): void {
|
||||
connLog.info({ reason }, "Server confirmed disconnect");
|
||||
this.closeSocket();
|
||||
}
|
||||
|
||||
/** Disconnect from the server, sending a Disconnect OOB packet first. */
|
||||
disconnect(): void {
|
||||
if (this._status === "disconnected" && !this.socket) return;
|
||||
|
||||
connLog.info("Disconnecting");
|
||||
|
||||
// Send a Disconnect packet so the server knows we're leaving
|
||||
if (this.socket && this.serverConnectSequence !== 0) {
|
||||
try {
|
||||
const packet = buildDisconnectPacket(
|
||||
this.serverConnectSequence,
|
||||
this.clientConnectSequence,
|
||||
);
|
||||
this.socket.send(packet, this.port, this.host);
|
||||
connLog.info("Sent Disconnect packet to server");
|
||||
} catch {
|
||||
// Best effort
|
||||
}
|
||||
// Stop all periodic work immediately, including any in-progress
|
||||
// disconnect retransmission (guards against re-entrant calls from
|
||||
// socket error handlers during the retry window).
|
||||
if (this.disconnectRetryTimer) {
|
||||
clearTimeout(this.disconnectRetryTimer);
|
||||
this.disconnectRetryTimer = null;
|
||||
}
|
||||
|
||||
if (this.keepaliveTimer) {
|
||||
clearInterval(this.keepaliveTimer);
|
||||
this.keepaliveTimer = null;
|
||||
|
|
@ -827,17 +857,52 @@ export class GameConnection extends EventEmitter<GameConnectionEvents> {
|
|||
clearTimeout(this.handshakeTimer);
|
||||
this.handshakeTimer = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch {
|
||||
// Already closed
|
||||
}
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
if (this._status !== "disconnected") {
|
||||
this.setStatus("disconnected");
|
||||
}
|
||||
this.emit("close");
|
||||
|
||||
// Send a Disconnect packet and wait for the server to confirm by
|
||||
// sending its own type 38 back. If no confirmation arrives within
|
||||
// 500ms, retransmit — UDP is unreliable and a single packet may be
|
||||
// lost. After 3 attempts, close the socket regardless.
|
||||
if (this.socket && this.serverConnectSequence !== 0) {
|
||||
const packet = buildDisconnectPacket(
|
||||
this.serverConnectSequence,
|
||||
this.clientConnectSequence,
|
||||
);
|
||||
let attempts = 0;
|
||||
const MAX_ATTEMPTS = 3;
|
||||
const RETRY_MS = 500;
|
||||
|
||||
const trySend = () => {
|
||||
if (!this.socket) return; // Already confirmed and closed.
|
||||
attempts++;
|
||||
try {
|
||||
this.socket.send(packet, this.port, this.host);
|
||||
connLog.info(
|
||||
"Sent Disconnect packet (%d/%d)",
|
||||
attempts,
|
||||
MAX_ATTEMPTS,
|
||||
);
|
||||
} catch {
|
||||
connLog.warn("Failed to send Disconnect packet, closing socket");
|
||||
this.closeSocket();
|
||||
return;
|
||||
}
|
||||
if (attempts < MAX_ATTEMPTS) {
|
||||
this.disconnectRetryTimer = setTimeout(trySend, RETRY_MS);
|
||||
} else {
|
||||
// Give up waiting for confirmation.
|
||||
this.disconnectRetryTimer = setTimeout(() => {
|
||||
connLog.warn("No disconnect confirmation from server, closing");
|
||||
this.closeSocket();
|
||||
}, RETRY_MS);
|
||||
}
|
||||
};
|
||||
trySend();
|
||||
} else {
|
||||
this.closeSocket();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,6 +232,9 @@ wss.on("connection", (ws) => {
|
|||
|
||||
ws.on("close", () => {
|
||||
relayLog.info("Browser client disconnected");
|
||||
// Clear retry state so we never auto-reconnect without a browser client.
|
||||
lastJoinAddress = null;
|
||||
retryCount = 0;
|
||||
if (retryTimer) {
|
||||
clearTimeout(retryTimer);
|
||||
retryTimer = null;
|
||||
|
|
@ -288,19 +291,6 @@ wss.on("connection", (ws) => {
|
|||
break;
|
||||
}
|
||||
|
||||
case "disconnect": {
|
||||
relayLog.info("Disconnect requested");
|
||||
if (retryTimer) {
|
||||
clearTimeout(retryTimer);
|
||||
retryTimer = null;
|
||||
}
|
||||
if (gameConnection) {
|
||||
gameConnection.disconnect();
|
||||
gameConnection = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "sendCommand": {
|
||||
if (gameConnection) {
|
||||
const authEvents = [
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
export type ClientMessage =
|
||||
| { type: "listServers" }
|
||||
| { type: "joinServer"; address: string; warriorName?: string }
|
||||
| { type: "disconnect" }
|
||||
| { type: "sendMoves"; moves: ClientMove[]; moveStartIndex: number }
|
||||
| { type: "sendCommand"; command: string; args: string[] }
|
||||
| {
|
||||
|
|
@ -31,7 +30,6 @@ export type ServerMessage =
|
|||
connectSequence?: number;
|
||||
mapName?: string;
|
||||
}
|
||||
| { type: "gamePacket"; data: Uint8Array }
|
||||
| { type: "ping"; ms: number }
|
||||
| { type: "wsPong"; ts: number }
|
||||
| { type: "error"; message: string };
|
||||
|
|
@ -69,13 +67,3 @@ export interface ClientMove {
|
|||
trigger: boolean[];
|
||||
freeLook: boolean;
|
||||
}
|
||||
|
||||
export interface RelayConfig {
|
||||
port: number;
|
||||
accountName: string;
|
||||
accountPassword: string;
|
||||
accountCertificate: string;
|
||||
accountEncryptedKey: string;
|
||||
authServerAddress: string;
|
||||
masterServerAddress: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue