Monitoring client connections (#941)

## Description:
Disconnected client detection :
If a client haven't send a ping to the server since more than 30 seconds
They will then be marked disconnected with a dedicated icon.
No action are yet taken, this allows for extensive in-game test before
adding the *consequences* of the player leaving the game.
I also added extensive unit tests, lessening the risk of regression for
the future.


![image](https://github.com/user-attachments/assets/884e5e99-15e8-4544-bd52-7524542cc82a)



## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:
theodoreleon.aetarax

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Théodore Léon
2025-06-06 13:30:23 -07:00
committed by GitHub
co-authored by evanpelle
parent 02f7caef88
commit a4fffce7f3
12 changed files with 287 additions and 2 deletions
+2 -1
View File
@@ -4,7 +4,8 @@ import { Tick } from "../core/game/Game";
import { ClientID } from "../core/Schemas";
export class Client {
public lastPing: number;
public lastPing: number = Date.now();
public isDisconnected: boolean = false;
public hashes: Map<Tick, number> = new Map();
+43
View File
@@ -35,6 +35,8 @@ export class GameServer {
private maxGameDuration = 3 * 60 * 60 * 1000; // 3 hours
private disconnectedTimeout = 1 * 30 * 1000; // 30 seconds
private turns: Turn[] = [];
private intents: Intent[] = [];
public activeClients: Client[] = [];
@@ -164,6 +166,10 @@ export class GameServer {
});
return;
}
client.isDisconnected = existing.isDisconnected;
client.lastPing = existing.lastPing;
existing.ws.removeAllListeners("message");
this.activeClients = this.activeClients.filter((c) => c !== existing);
}
@@ -195,6 +201,12 @@ export class GameServer {
);
return;
}
if (clientMsg.intent.type === "mark_disconnected") {
this.log.warn(
`Should not receive mark_disconnected intent from client`,
);
return;
}
this.addIntent(clientMsg.intent);
}
if (clientMsg.type === "ping") {
@@ -357,6 +369,7 @@ export class GameServer {
this.intents = [];
this.handleSynchronization();
this.checkDisconnectedStatus();
let msg = "";
try {
@@ -537,6 +550,36 @@ export class GameServer {
}
}
private checkDisconnectedStatus() {
if (this.turns.length % 5 !== 0) {
return;
}
const now = Date.now();
for (const [clientID, client] of this.allClients) {
if (
client.isDisconnected === false &&
now - client.lastPing > this.disconnectedTimeout
) {
this.markClientDisconnected(client, true);
} else if (
client.isDisconnected &&
now - client.lastPing < this.disconnectedTimeout
) {
this.markClientDisconnected(client, false);
}
}
}
private markClientDisconnected(client: Client, isDisconnected: boolean) {
client.isDisconnected = isDisconnected;
this.addIntent({
type: "mark_disconnected",
clientID: client.clientID,
isDisconnected: isDisconnected,
});
}
private archiveGame() {
this.log.info("archiving game", {
gameID: this.id,