Connection status monitoring

This commit is contained in:
aqw42
2025-05-29 23:10:30 +02:00
parent 62cff7a2c4
commit 5eed7db4e3
13 changed files with 343 additions and 328 deletions
+17 -11
View File
@@ -1,8 +1,8 @@
import allianceIcon from "../../../../resources/images/AllianceIcon.svg";
import allianceRequestIcon from "../../../../resources/images/AllianceRequestIcon.svg";
import crownIcon from "../../../../resources/images/CrownIcon.svg";
import disconnectedIcon from "../../../../resources/images/DisconnectedIcon.svg";
import embargoIcon from "../../../../resources/images/EmbargoIcon.svg";
import idleIcon from "../../../../resources/images/IdleIcon.svg";
import nukeRedIcon from "../../../../resources/images/NukeIconRed.svg";
import nukeWhiteIcon from "../../../../resources/images/NukeIconWhite.svg";
import shieldIcon from "../../../../resources/images/ShieldIconBlack.svg";
@@ -39,7 +39,7 @@ export class NameLayer implements Layer {
private renders: RenderInfo[] = [];
private seenPlayers: Set<PlayerView> = new Set();
private traitorIconImage: HTMLImageElement;
private idleIconImage: HTMLImageElement;
private disconnectedIconImage: HTMLImageElement;
private allianceRequestIconImage: HTMLImageElement;
private allianceIconImage: HTMLImageElement;
private targetIconImage: HTMLImageElement;
@@ -60,8 +60,8 @@ export class NameLayer implements Layer {
) {
this.traitorIconImage = new Image();
this.traitorIconImage.src = traitorIcon;
this.idleIconImage = new Image();
this.idleIconImage.src = idleIcon;
this.disconnectedIconImage = new Image();
this.disconnectedIconImage.src = disconnectedIcon;
this.allianceIconImage = new Image();
this.allianceIconImage.src = allianceIcon;
this.allianceRequestIconImage = new Image();
@@ -353,16 +353,22 @@ export class NameLayer implements Layer {
existingTraitor.remove();
}
// Idle icon
const existingIdle = iconsDiv.querySelector('[data-icon="idle"]');
if (render.player.isIdle()) {
if (!existingIdle) {
// Disconnected icon
const existingDisconnected = iconsDiv.querySelector(
'[data-icon="disconnected"]',
);
if (render.player.isDisconnected()) {
if (!existingDisconnected) {
iconsDiv.appendChild(
this.createIconElement(this.idleIconImage.src, iconSize, "idle"),
this.createIconElement(
this.disconnectedIconImage.src,
iconSize,
"disconnected",
),
);
}
} else if (existingIdle) {
existingIdle.remove();
} else if (existingDisconnected) {
existingDisconnected.remove();
}
// Alliance icon
+9 -7
View File
@@ -34,7 +34,7 @@ export type Intent =
| EmbargoIntent
| QuickChatIntent
| MoveWarshipIntent
| MarkIdleIntent;
| MarkDisconnectedIntent;
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
@@ -57,7 +57,9 @@ export type TargetTroopRatioIntent = z.infer<
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
export type MoveWarshipIntent = z.infer<typeof MoveWarshipIntentSchema>;
export type QuickChatIntent = z.infer<typeof QuickChatIntentSchema>;
export type MarkIdleIntent = z.infer<typeof MarkIdleIntentSchema>;
export type MarkDisconnectedIntent = z.infer<
typeof MarkDisconnectedIntentSchema
>;
export type Turn = z.infer<typeof TurnSchema>;
export type GameConfig = z.infer<typeof GameConfigSchema>;
@@ -168,7 +170,7 @@ const BaseIntentSchema = z.object({
"attack",
"cancel_attack",
"spawn",
"mark_idle",
"mark_disconnected",
"boat",
"cancel_boat",
"name",
@@ -293,16 +295,16 @@ export const QuickChatIntentSchema = BaseIntentSchema.extend({
variables: z.record(SafeString).optional(),
});
export const MarkIdleIntentSchema = BaseIntentSchema.extend({
type: z.literal("mark_idle"),
isIdle: z.boolean(),
export const MarkDisconnectedIntentSchema = BaseIntentSchema.extend({
type: z.literal("mark_disconnected"),
isDisconnected: z.boolean(),
});
const IntentSchema = z.union([
AttackIntentSchema,
CancelAttackIntentSchema,
SpawnIntentSchema,
MarkIdleIntentSchema,
MarkDisconnectedIntentSchema,
BoatAttackIntentSchema,
CancelBoatIntentSchema,
AllianceRequestIntentSchema,
+3 -3
View File
@@ -15,7 +15,7 @@ import { DonateTroopsExecution } from "./DonateTroopExecution";
import { EmbargoExecution } from "./EmbargoExecution";
import { EmojiExecution } from "./EmojiExecution";
import { FakeHumanExecution } from "./FakeHumanExecution";
import { MarkIdleExecution } from "./MarkIdleExecution";
import { MarkDisconnectedExecution } from "./MarkDisconnectedExecution";
import { MoveWarshipExecution } from "./MoveWarshipExecution";
import { NoOpExecution } from "./NoOpExecution";
import { QuickChatExecution } from "./QuickChatExecution";
@@ -121,8 +121,8 @@ export class Executor {
intent.quickChatKey,
intent.variables ?? {},
);
case "mark_idle":
return new MarkIdleExecution(playerID, intent.isIdle);
case "mark_disconnected":
return new MarkDisconnectedExecution(playerID, intent.isDisconnected);
default:
throw new Error(`intent type ${intent} not found`);
}
@@ -1,18 +1,18 @@
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class MarkIdleExecution implements Execution {
export class MarkDisconnectedExecution implements Execution {
private player: Player;
private active: boolean = true;
constructor(
private playerID: PlayerID,
private isIdle: boolean,
private isDisconnected: boolean,
) {}
init(mg: Game, ticks: number): void {
if (!mg.hasPlayer(this.playerID)) {
console.warn(
`MarkIdleExecution: player ${this.playerID} not found in game`,
`MarkDisconnectedExecution: player ${this.playerID} not found in game`,
);
this.active = false;
return;
@@ -21,7 +21,7 @@ export class MarkIdleExecution implements Execution {
this.player = mg.player(this.playerID);
if (!this.player) {
console.warn(
`MarkIdleExecution: failed to retrieve player ${this.playerID}`,
`MarkDisconnectedExecution: failed to retrieve player ${this.playerID}`,
);
this.active = false;
return;
@@ -29,7 +29,7 @@ export class MarkIdleExecution implements Execution {
}
tick(ticks: number): void {
this.player.markIdle(this.isIdle);
this.player.markDisconnected(this.isDisconnected);
this.active = false;
}
+2 -2
View File
@@ -433,8 +433,8 @@ export interface Player {
largestClusterBoundingBox: { min: Cell; max: Cell } | null;
lastTileChange(): Tick;
isIdle(): boolean;
markIdle(isIdle: boolean): void;
isDisconnected(): boolean;
markDisconnected(isDisconnected: boolean): void;
hasSpawned(): boolean;
setHasSpawned(hasSpawned: boolean): void;
+1 -1
View File
@@ -102,7 +102,7 @@ export interface PlayerUpdate {
smallID: number;
playerType: PlayerType;
isAlive: boolean;
isIdle: boolean;
isDisconnected: boolean;
tilesOwned: number;
gold: number;
population: number;
+2 -2
View File
@@ -292,8 +292,8 @@ export class PlayerView {
hasSpawned(): boolean {
return this.data.hasSpawned;
}
isIdle(): boolean {
return this.data.isIdle;
isDisconnected(): boolean {
return this.data.isDisconnected;
}
}
+6 -6
View File
@@ -99,7 +99,7 @@ export class PlayerImpl implements Player {
public _outgoingLandAttacks: Attack[] = [];
private _hasSpawned = false;
private _isIdle = false;
private _isDisconnected = false;
constructor(
private mg: GameImpl,
@@ -137,7 +137,7 @@ export class PlayerImpl implements Player {
smallID: this.smallID(),
playerType: this.type(),
isAlive: this.isAlive(),
isIdle: this.isIdle(),
isDisconnected: this.isDisconnected(),
tilesOwned: this.numTilesOwned(),
gold: Number(this._gold),
population: this.population(),
@@ -927,12 +927,12 @@ export class PlayerImpl implements Player {
return this._lastTileChange;
}
isIdle(): boolean {
return this._isIdle;
isDisconnected(): boolean {
return this._isDisconnected;
}
markIdle(isIdle: boolean): void {
this._isIdle = isIdle;
markDisconnected(isDisconnected: boolean): void {
this._isDisconnected = isDisconnected;
}
hash(): number {
+1 -2
View File
@@ -5,8 +5,7 @@ import { ClientID } from "../core/Schemas";
export class Client {
public lastPing: number = Date.now();
public lastAction: number = Date.now();
public isIdle: boolean = false;
public isDisconnected: boolean = false;
public hashes: Map<Tick, number> = new Map();
+14 -18
View File
@@ -35,7 +35,7 @@ export class GameServer {
private maxGameDuration = 3 * 60 * 60 * 1000; // 3 hours
private idleTimeout = 1 * 60 * 1000; // 1 minute
private disconnectedTimeout = 1 * 30 * 1000; // 30 seconds
private turns: Turn[] = [];
private intents: Intent[] = [];
@@ -167,8 +167,7 @@ export class GameServer {
return;
}
client.isIdle = existing.isIdle;
client.lastAction = existing.lastAction;
client.isDisconnected = existing.isDisconnected;
client.lastPing = existing.lastPing;
existing.ws.removeAllListeners("message");
@@ -198,7 +197,6 @@ export class GameServer {
);
return;
}
client.lastAction = Date.now();
this.addIntent(clientMsg.intent);
}
if (clientMsg.type === "ping") {
@@ -361,7 +359,7 @@ export class GameServer {
this.intents = [];
this.handleSynchronization();
this.checkIdleStatus();
this.checkDisconnectedStatus();
let msg = "";
try {
@@ -542,7 +540,7 @@ export class GameServer {
}
}
private checkIdleStatus() {
private checkDisconnectedStatus() {
if (this.turns.length % 5 !== 0) {
return;
}
@@ -550,27 +548,25 @@ export class GameServer {
const now = Date.now();
for (const [clientID, client] of this.allClients) {
if (
client.isIdle === false &&
now - client.lastPing > this.idleTimeout &&
now - client.lastAction > this.idleTimeout
client.isDisconnected === false &&
now - client.lastPing > this.disconnectedTimeout
) {
this.markClientIdle(client, true);
this.markClientDisconnected(client, true);
} else if (
client.isIdle &&
now - client.lastPing < this.idleTimeout &&
now - client.lastAction < this.idleTimeout
client.isDisconnected &&
now - client.lastPing < this.disconnectedTimeout
) {
this.markClientIdle(client, false);
this.markClientDisconnected(client, false);
}
}
}
private markClientIdle(client: Client, isIdle: boolean) {
client.isIdle = isIdle;
private markClientDisconnected(client: Client, isDisconnected: boolean) {
client.isDisconnected = isDisconnected;
this.addIntent({
type: "mark_idle",
type: "mark_disconnected",
clientID: client.clientID,
isIdle: isIdle,
isDisconnected: isDisconnected,
});
}