Optimised snapshot traffic and map rendering

This commit is contained in:
2026-09-20 01:07:03 +02:00
parent b385679e52
commit 34e755d1a4
24 changed files with 403 additions and 30 deletions
+44 -1
View File
@@ -9,6 +9,11 @@ import { LoginManager } from "../shared/login_manager.js";
import { CIVILISATIONS } from "../shared/data.js";
import { dateString } from "../shared/game_clock.js";
// The large per-tile arrays a delta snapshot may leave out when their version
// has not moved since the last one sent to that peer. The viewer keeps the
// previous copy until it changes.
const DELTA_COLLECTIONS = ["territory", "regions", "population", "tileEthnicity"];
export class GameServer extends EventEmitter {
constructor(network) {
super();
@@ -24,6 +29,10 @@ export class GameServer extends EventEmitter {
this.identityCiv = new Map();
this._configured = false;
this._stateDirty = false;
// Per-peer delta bookkeeping: the versions last sent and how many explored
// tiles the viewer already has.
this._peerSnapshotVersions = new Map();
this._peerExploredSent = new Map();
}
isConfigured() {
@@ -265,6 +274,10 @@ export class GameServer extends EventEmitter {
}
onPeerJoined(peerId, playerName = "") {
// A fresh connection must receive a full snapshot, never a delta against a
// previous peer that happened to reuse the id.
this._peerSnapshotVersions.delete(peerId);
this._peerExploredSent.delete(peerId);
if (!this._configured) return;
// A returning player keeps the nation they claimed before.
if (this._reclaimIdentity(peerId, playerName)) {
@@ -284,6 +297,8 @@ export class GameServer extends EventEmitter {
onPeerLeft(peerId) {
this.peerCiv.delete(peerId);
this.peerNames.delete(peerId);
this._peerSnapshotVersions.delete(peerId);
this._peerExploredSent.delete(peerId);
}
// Indices of the civilisations no connected peer has claimed.
@@ -379,12 +394,40 @@ export class GameServer extends EventEmitter {
for (const peerId of this.network.getPeerIds()) {
const civ = this.peerCiv.has(peerId) ? this.peerCiv.get(peerId) : -1;
if (!statsCache.has(civ)) statsCache.set(civ, this.state.viewerStats(civ));
const snapshot = this.state.viewerSnapshot(shared, civ, statsCache.get(civ));
this.network.send(peerId, {
t: "game_state",
state: this.state.viewerSnapshot(shared, civ, statsCache.get(civ)),
state: this._deltaState(peerId, snapshot),
});
}
}
// Trims a viewer snapshot down to what changed since the last one sent to this
// peer: the large per-tile arrays are left out while their version is
// unchanged, and only the newly explored tiles are shipped. The first
// snapshot to a peer (and after a reconnect) is always full.
_deltaState(peerId, snapshot) {
const versions = snapshot.versions;
const previous = this._peerSnapshotVersions.get(peerId);
if (!previous) {
this._peerSnapshotVersions.set(peerId, versions);
this._peerExploredSent.set(peerId, (snapshot.explored || []).length);
return { ...snapshot, delta: false };
}
const state = { ...snapshot, delta: true };
for (const key of DELTA_COLLECTIONS) {
if (previous[key] === versions[key]) delete state[key];
}
const explored = snapshot.explored || [];
const sent = this._peerExploredSent.get(peerId) || 0;
if (explored.length > sent) {
state.exploredAdded = explored.slice(sent);
this._peerExploredSent.set(peerId, explored.length);
}
delete state.explored;
this._peerSnapshotVersions.set(peerId, versions);
return state;
}
}
function coordsOf(value) {