mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-24 09:44:00 +00:00
diffPlayerUpdate runs once per player per tick on the worker thread. The array/object fields (outgoingAttacks, incomingAttacks, alliances, outgoingEmojis) were compared via JSON.stringify — two string allocations per field, run on every call even when nothing changed. This made the cost flat at ~3.4µs/call regardless of what actually changed. Replace jsonEqual with three typed structural comparators (attackArrayEqual, allianceArrayEqual, emojiArrayEqual) that short-circuit on reference/length, compare known fields with ===, early-exit on the first difference, and allocate nothing — matching the existing numberArrayEqual/stringArrayEqual style. ~9-10x faster across all cases (276k -> 2.4M ops/sec when unchanged). Add tests/perf/DiffPlayerUpdatePerf.ts (BEFORE/AFTER benchmark, run via npm run perf) and warnings on PlayerUpdate and diffPlayerUpdate noting that new fields must be wired into the diff/apply functions or their changes are silently dropped after the first emission. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
329 lines
7.3 KiB
TypeScript
329 lines
7.3 KiB
TypeScript
import { AllPlayersStats, ClientID, Winner } from "../Schemas";
|
|
import {
|
|
EmojiMessage,
|
|
GameUpdates,
|
|
Gold,
|
|
MessageType,
|
|
NameViewData,
|
|
PlayerID,
|
|
PlayerType,
|
|
Team,
|
|
Tick,
|
|
TrainType,
|
|
TransportShipState,
|
|
UnitType,
|
|
WarshipState,
|
|
} from "./Game";
|
|
import { TileRef } from "./GameMap";
|
|
|
|
export interface GameUpdateViewData {
|
|
tick: number;
|
|
updates: GameUpdates;
|
|
/**
|
|
* Packed tile updates as `[tileRef, state]` uint32 pairs.
|
|
*
|
|
* `tileRef` is a `TileRef` (fits in uint32), and `state` is the packed per-tile
|
|
* state (`uint16`) stored in a `uint32` lane.
|
|
*/
|
|
packedTileUpdates: Uint32Array;
|
|
/**
|
|
* Optional packed motion plan records.
|
|
*
|
|
* When present, this buffer is expected to be transferred worker -> main
|
|
* (similar to `packedTileUpdates`) to avoid structured-clone copies.
|
|
*/
|
|
packedMotionPlans?: Uint32Array;
|
|
playerNameViewData: Record<string, NameViewData>;
|
|
tickExecutionDuration?: number;
|
|
pendingTurns?: number;
|
|
}
|
|
|
|
export interface ErrorUpdate {
|
|
errMsg: string;
|
|
stack?: string;
|
|
}
|
|
|
|
export enum GameUpdateType {
|
|
// Tile updates are delivered via `packedTileUpdates` on the outer GameUpdateViewData.
|
|
Tile,
|
|
Unit,
|
|
Player,
|
|
DisplayEvent,
|
|
DisplayChatEvent,
|
|
AllianceRequest,
|
|
AllianceRequestReply,
|
|
BrokeAlliance,
|
|
AllianceExpired,
|
|
AllianceExtension,
|
|
TargetPlayer,
|
|
Emoji,
|
|
Win,
|
|
Hash,
|
|
UnitIncoming,
|
|
BonusEvent,
|
|
RailroadDestructionEvent,
|
|
RailroadConstructionEvent,
|
|
RailroadSnapEvent,
|
|
ConquestEvent,
|
|
EmbargoEvent,
|
|
SpawnPhaseEnd,
|
|
GamePaused,
|
|
DonateEvent,
|
|
}
|
|
|
|
export type GameUpdate =
|
|
| UnitUpdate
|
|
| PlayerUpdate
|
|
| AllianceRequestUpdate
|
|
| AllianceRequestReplyUpdate
|
|
| BrokeAllianceUpdate
|
|
| AllianceExpiredUpdate
|
|
| DisplayMessageUpdate
|
|
| DisplayChatMessageUpdate
|
|
| TargetPlayerUpdate
|
|
| EmojiUpdate
|
|
| WinUpdate
|
|
| HashUpdate
|
|
| UnitIncomingUpdate
|
|
| AllianceExtensionUpdate
|
|
| BonusEventUpdate
|
|
| RailroadConstructionUpdate
|
|
| RailroadDestructionUpdate
|
|
| RailroadSnapUpdate
|
|
| ConquestUpdate
|
|
| EmbargoUpdate
|
|
| SpawnPhaseEndUpdate
|
|
| GamePausedUpdate
|
|
| DonateEventUpdate;
|
|
|
|
export interface BonusEventUpdate {
|
|
type: GameUpdateType.BonusEvent;
|
|
player: PlayerID;
|
|
tile: TileRef;
|
|
gold: number;
|
|
troops: number;
|
|
}
|
|
|
|
export interface RailroadConstructionUpdate {
|
|
type: GameUpdateType.RailroadConstructionEvent;
|
|
id: number;
|
|
tiles: TileRef[];
|
|
}
|
|
|
|
export interface RailroadDestructionUpdate {
|
|
type: GameUpdateType.RailroadDestructionEvent;
|
|
id: number;
|
|
}
|
|
|
|
export interface RailroadSnapUpdate {
|
|
type: GameUpdateType.RailroadSnapEvent;
|
|
originalId: number;
|
|
newId1: number;
|
|
newId2: number;
|
|
tiles1: TileRef[];
|
|
tiles2: TileRef[];
|
|
}
|
|
|
|
export interface ConquestUpdate {
|
|
type: GameUpdateType.ConquestEvent;
|
|
conquerorId: PlayerID;
|
|
conqueredId: PlayerID;
|
|
gold: Gold;
|
|
}
|
|
|
|
export interface DonateEventUpdate {
|
|
type: GameUpdateType.DonateEvent;
|
|
donationType: "troops" | "gold";
|
|
senderId: PlayerID;
|
|
recipientId: PlayerID;
|
|
amount: bigint;
|
|
}
|
|
|
|
export interface UnitUpdate {
|
|
type: GameUpdateType.Unit;
|
|
unitType: UnitType;
|
|
troops: number;
|
|
id: number;
|
|
ownerID: number;
|
|
lastOwnerID?: number;
|
|
// TODO: make these tilerefs
|
|
pos: TileRef;
|
|
lastPos: TileRef;
|
|
isActive: boolean;
|
|
reachedTarget: boolean;
|
|
warshipState?: WarshipState;
|
|
transportShipState?: TransportShipState;
|
|
targetable: boolean;
|
|
markedForDeletion: number | false;
|
|
targetUnitId?: number; // Only for trade ships
|
|
targetTile?: TileRef; // Only for nukes
|
|
health?: number;
|
|
underConstruction?: boolean;
|
|
missileTimerQueue: number[];
|
|
level: number;
|
|
hasTrainStation: boolean;
|
|
trainType?: TrainType; // Only for trains
|
|
loaded?: boolean; // Only for trains
|
|
}
|
|
|
|
export interface AttackUpdate {
|
|
attackerID: number;
|
|
targetID: number;
|
|
troops: number;
|
|
id: string;
|
|
retreating: boolean;
|
|
}
|
|
|
|
/**
|
|
* Player snapshot delivered worker -> main thread.
|
|
*
|
|
* Only `type` and `id` are guaranteed. Every other field is omitted when its
|
|
* value matches the previous emission for the same player. The first emission
|
|
* for a player always includes all fields; consumers must handle subsequent
|
|
* partial updates by merging into local state, not overwriting.
|
|
*
|
|
* When adding a field here, also wire it into diffPlayerUpdate() and
|
|
* applyStateUpdate() in GameUpdateUtils.ts — otherwise it is only ever sent on
|
|
* the first emission and later changes are silently dropped.
|
|
*/
|
|
export interface PlayerUpdate {
|
|
type: GameUpdateType.Player;
|
|
id: PlayerID;
|
|
nameViewData?: NameViewData;
|
|
clientID?: ClientID | null;
|
|
name?: string;
|
|
displayName?: string;
|
|
team?: Team;
|
|
smallID?: number;
|
|
playerType?: PlayerType;
|
|
isAlive?: boolean;
|
|
isDisconnected?: boolean;
|
|
tilesOwned?: number;
|
|
gold?: Gold;
|
|
troops?: number;
|
|
allies?: number[];
|
|
embargoes?: Set<PlayerID>;
|
|
isTraitor?: boolean;
|
|
traitorRemainingTicks?: number;
|
|
targets?: number[];
|
|
outgoingEmojis?: EmojiMessage[];
|
|
outgoingAttacks?: AttackUpdate[];
|
|
incomingAttacks?: AttackUpdate[];
|
|
outgoingAllianceRequests?: PlayerID[];
|
|
alliances?: AllianceView[];
|
|
hasSpawned?: boolean;
|
|
spawnTile?: TileRef;
|
|
betrayals?: number;
|
|
lastDeleteUnitTick?: Tick;
|
|
isLobbyCreator?: boolean;
|
|
}
|
|
|
|
export interface AllianceView {
|
|
id: number;
|
|
other: PlayerID;
|
|
createdAt: Tick;
|
|
expiresAt: Tick;
|
|
hasExtensionRequest: boolean;
|
|
}
|
|
|
|
export interface AllianceRequestUpdate {
|
|
type: GameUpdateType.AllianceRequest;
|
|
requestorID: number;
|
|
recipientID: number;
|
|
createdAt: Tick;
|
|
}
|
|
|
|
export interface AllianceRequestReplyUpdate {
|
|
type: GameUpdateType.AllianceRequestReply;
|
|
request: AllianceRequestUpdate;
|
|
accepted: boolean;
|
|
}
|
|
|
|
export interface BrokeAllianceUpdate {
|
|
type: GameUpdateType.BrokeAlliance;
|
|
traitorID: number;
|
|
betrayedID: number;
|
|
allianceID: number;
|
|
}
|
|
|
|
export interface AllianceExpiredUpdate {
|
|
type: GameUpdateType.AllianceExpired;
|
|
player1ID: number;
|
|
player2ID: number;
|
|
}
|
|
|
|
export interface AllianceExtensionUpdate {
|
|
type: GameUpdateType.AllianceExtension;
|
|
playerID: number;
|
|
allianceID: number;
|
|
}
|
|
|
|
export interface TargetPlayerUpdate {
|
|
type: GameUpdateType.TargetPlayer;
|
|
playerID: number;
|
|
targetID: number;
|
|
}
|
|
|
|
export interface EmojiUpdate {
|
|
type: GameUpdateType.Emoji;
|
|
emoji: EmojiMessage;
|
|
}
|
|
|
|
export interface DisplayMessageUpdate {
|
|
type: GameUpdateType.DisplayEvent;
|
|
message: string;
|
|
messageType: MessageType;
|
|
goldAmount?: bigint;
|
|
playerID: number | null;
|
|
params?: Record<string, string | number>;
|
|
unitID?: number;
|
|
focusPlayerID?: number;
|
|
}
|
|
|
|
export type DisplayChatMessageUpdate = {
|
|
type: GameUpdateType.DisplayChatEvent;
|
|
key: string;
|
|
category: string;
|
|
target: string | undefined;
|
|
playerID: number | null;
|
|
isFrom: boolean;
|
|
recipient: string;
|
|
};
|
|
|
|
export interface WinUpdate {
|
|
type: GameUpdateType.Win;
|
|
allPlayersStats: AllPlayersStats;
|
|
winner: Winner;
|
|
}
|
|
|
|
export interface HashUpdate {
|
|
type: GameUpdateType.Hash;
|
|
tick: Tick;
|
|
hash: number;
|
|
}
|
|
|
|
export interface UnitIncomingUpdate {
|
|
type: GameUpdateType.UnitIncoming;
|
|
unitID: number;
|
|
message: string;
|
|
messageType: MessageType;
|
|
playerID: number;
|
|
}
|
|
|
|
export interface EmbargoUpdate {
|
|
type: GameUpdateType.EmbargoEvent;
|
|
event: "start" | "stop";
|
|
playerID: number;
|
|
embargoedID: number;
|
|
}
|
|
|
|
export interface SpawnPhaseEndUpdate {
|
|
type: GameUpdateType.SpawnPhaseEnd;
|
|
startTick: Tick;
|
|
}
|
|
|
|
export interface GamePausedUpdate {
|
|
type: GameUpdateType.GamePaused;
|
|
paused: boolean;
|
|
}
|