mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-18 17:53:29 +00:00
perf(core): plan-driven ships via packedMotionPlans
Send movement intent as packedMotionPlans for trade/transport ships and stop emitting per-tick movement UnitUpdates for plan-driven unit ids.
This commit is contained in:
@@ -65,11 +65,27 @@ export class UnitLayer implements Layer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
const unitIds = this.game
|
const updatedUnitIds =
|
||||||
.updatesSinceLastTick()
|
this.game
|
||||||
?.[GameUpdateType.Unit]?.map((unit) => unit.id);
|
.updatesSinceLastTick()
|
||||||
|
?.[GameUpdateType.Unit]?.map((unit) => unit.id) ?? [];
|
||||||
|
|
||||||
this.updateUnitsSprites(unitIds ?? []);
|
const motionPlanUnitIds = Array.from(this.game.motionPlans().keys());
|
||||||
|
|
||||||
|
if (updatedUnitIds.length === 0) {
|
||||||
|
this.updateUnitsSprites(motionPlanUnitIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (motionPlanUnitIds.length === 0) {
|
||||||
|
this.updateUnitsSprites(updatedUnitIds);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unitIds = new Set<number>(updatedUnitIds);
|
||||||
|
for (const id of motionPlanUnitIds) {
|
||||||
|
unitIds.add(id);
|
||||||
|
}
|
||||||
|
this.updateUnitsSprites(Array.from(unitIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|||||||
@@ -169,10 +169,12 @@ export class GameRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const packedTileUpdates = this.game.drainPackedTileUpdates();
|
const packedTileUpdates = this.game.drainPackedTileUpdates();
|
||||||
|
const packedMotionPlans = this.game.drainPackedMotionPlans();
|
||||||
|
|
||||||
this.callBack({
|
this.callBack({
|
||||||
tick: this.game.ticks(),
|
tick: this.game.ticks(),
|
||||||
packedTileUpdates,
|
packedTileUpdates,
|
||||||
|
...(packedMotionPlans ? { packedMotionPlans } : {}),
|
||||||
updates: updates,
|
updates: updates,
|
||||||
playerNameViewData: this.playerViewData,
|
playerNameViewData: this.playerViewData,
|
||||||
tickExecutionDuration: tickExecutionDuration,
|
tickExecutionDuration: tickExecutionDuration,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
UnitType,
|
UnitType,
|
||||||
} from "../game/Game";
|
} from "../game/Game";
|
||||||
import { TileRef } from "../game/GameMap";
|
import { TileRef } from "../game/GameMap";
|
||||||
|
import { MotionPlanRecord } from "../game/MotionPlans";
|
||||||
import { PathFinding } from "../pathfinding/PathFinder";
|
import { PathFinding } from "../pathfinding/PathFinder";
|
||||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||||
import { distSortUnit } from "../Util";
|
import { distSortUnit } from "../Util";
|
||||||
@@ -19,6 +20,8 @@ export class TradeShipExecution implements Execution {
|
|||||||
private wasCaptured = false;
|
private wasCaptured = false;
|
||||||
private pathFinder: SteppingPathFinder<TileRef>;
|
private pathFinder: SteppingPathFinder<TileRef>;
|
||||||
private tilesTraveled = 0;
|
private tilesTraveled = 0;
|
||||||
|
private motionPlanId = 1;
|
||||||
|
private motionPlanDst: TileRef | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private origOwner: Player,
|
private origOwner: Player,
|
||||||
@@ -32,6 +35,7 @@ export class TradeShipExecution implements Execution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tick(ticks: number): void {
|
tick(ticks: number): void {
|
||||||
|
let spawnedThisTick = false;
|
||||||
if (this.tradeShip === undefined) {
|
if (this.tradeShip === undefined) {
|
||||||
const spawn = this.origOwner.canBuild(
|
const spawn = this.origOwner.canBuild(
|
||||||
UnitType.TradeShip,
|
UnitType.TradeShip,
|
||||||
@@ -47,6 +51,18 @@ export class TradeShipExecution implements Execution {
|
|||||||
lastSetSafeFromPirates: ticks,
|
lastSetSafeFromPirates: ticks,
|
||||||
});
|
});
|
||||||
this.mg.stats().boatSendTrade(this.origOwner, this._dstPort.owner());
|
this.mg.stats().boatSendTrade(this.origOwner, this._dstPort.owner());
|
||||||
|
spawnedThisTick = true;
|
||||||
|
|
||||||
|
const placeholderPlan: MotionPlanRecord = {
|
||||||
|
kind: "grid",
|
||||||
|
unitId: this.tradeShip.id(),
|
||||||
|
planId: this.motionPlanId,
|
||||||
|
startTick: ticks + 1,
|
||||||
|
ticksPerStep: 1,
|
||||||
|
path: [spawn],
|
||||||
|
};
|
||||||
|
this.mg.recordMotionPlan(placeholderPlan);
|
||||||
|
this.motionPlanDst = this._dstPort.tile();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.tradeShip.isActive()) {
|
if (!this.tradeShip.isActive()) {
|
||||||
@@ -93,6 +109,8 @@ export class TradeShipExecution implements Execution {
|
|||||||
} else {
|
} else {
|
||||||
this._dstPort = ports[0];
|
this._dstPort = ports[0];
|
||||||
this.tradeShip.setTargetUnit(this._dstPort);
|
this.tradeShip.setTargetUnit(this._dstPort);
|
||||||
|
// Plan-driven units don't emit per-tick unit updates, so force a sync for the new target.
|
||||||
|
this.tradeShip.touch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +124,6 @@ export class TradeShipExecution implements Execution {
|
|||||||
|
|
||||||
switch (result.status) {
|
switch (result.status) {
|
||||||
case PathStatus.PENDING:
|
case PathStatus.PENDING:
|
||||||
// Fire unit event to rerender.
|
|
||||||
this.tradeShip.move(curTile);
|
|
||||||
break;
|
break;
|
||||||
case PathStatus.NEXT:
|
case PathStatus.NEXT:
|
||||||
// Update safeFromPirates status
|
// Update safeFromPirates status
|
||||||
@@ -128,6 +144,26 @@ export class TradeShipExecution implements Execution {
|
|||||||
this.active = false;
|
this.active = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dst = this._dstPort.tile();
|
||||||
|
if (spawnedThisTick || dst !== this.motionPlanDst) {
|
||||||
|
this.motionPlanId++;
|
||||||
|
const from = this.tradeShip.tile();
|
||||||
|
const path = this.pathFinder.findPath(from, dst) ?? [from];
|
||||||
|
if (path.length === 0 || path[0] !== from) {
|
||||||
|
path.unshift(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mg.recordMotionPlan({
|
||||||
|
kind: "grid",
|
||||||
|
unitId: this.tradeShip.id(),
|
||||||
|
planId: this.motionPlanId,
|
||||||
|
startTick: ticks + 1,
|
||||||
|
ticksPerStep: 1,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
this.motionPlanDst = dst;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private complete() {
|
private complete() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
UnitType,
|
UnitType,
|
||||||
} from "../game/Game";
|
} from "../game/Game";
|
||||||
import { TileRef } from "../game/GameMap";
|
import { TileRef } from "../game/GameMap";
|
||||||
|
import { MotionPlanRecord } from "../game/MotionPlans";
|
||||||
import { targetTransportTile } from "../game/TransportShipUtils";
|
import { targetTransportTile } from "../game/TransportShipUtils";
|
||||||
import { PathFinding } from "../pathfinding/PathFinder";
|
import { PathFinding } from "../pathfinding/PathFinder";
|
||||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||||
@@ -30,6 +31,8 @@ export class TransportShipExecution implements Execution {
|
|||||||
private dst: TileRef | null;
|
private dst: TileRef | null;
|
||||||
private src: TileRef | null;
|
private src: TileRef | null;
|
||||||
private boat: Unit;
|
private boat: Unit;
|
||||||
|
private motionPlanId = 1;
|
||||||
|
private motionPlanDst: TileRef | null = null;
|
||||||
|
|
||||||
private originalOwner: Player;
|
private originalOwner: Player;
|
||||||
|
|
||||||
@@ -109,6 +112,22 @@ export class TransportShipExecution implements Execution {
|
|||||||
targetTile: this.dst,
|
targetTile: this.dst,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fullPath = this.pathFinder.findPath(this.src, this.dst) ?? [this.src];
|
||||||
|
if (fullPath.length === 0 || fullPath[0] !== this.src) {
|
||||||
|
fullPath.unshift(this.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
const motionPlan: MotionPlanRecord = {
|
||||||
|
kind: "grid",
|
||||||
|
unitId: this.boat.id(),
|
||||||
|
planId: this.motionPlanId,
|
||||||
|
startTick: ticks + this.ticksPerMove,
|
||||||
|
ticksPerStep: this.ticksPerMove,
|
||||||
|
path: fullPath,
|
||||||
|
};
|
||||||
|
this.mg.recordMotionPlan(motionPlan);
|
||||||
|
this.motionPlanDst = this.dst;
|
||||||
|
|
||||||
// Notify the target player about the incoming naval invasion
|
// Notify the target player about the incoming naval invasion
|
||||||
if (this.target.id() !== mg.terraNullius().id()) {
|
if (this.target.id() !== mg.terraNullius().id()) {
|
||||||
mg.displayIncomingUnit(
|
mg.displayIncomingUnit(
|
||||||
@@ -249,6 +268,26 @@ export class TransportShipExecution implements Execution {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.dst !== null && this.dst !== this.motionPlanDst) {
|
||||||
|
this.motionPlanId++;
|
||||||
|
const fullPath = this.pathFinder.findPath(this.boat.tile(), this.dst) ?? [
|
||||||
|
this.boat.tile(),
|
||||||
|
];
|
||||||
|
if (fullPath.length === 0 || fullPath[0] !== this.boat.tile()) {
|
||||||
|
fullPath.unshift(this.boat.tile());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mg.recordMotionPlan({
|
||||||
|
kind: "grid",
|
||||||
|
unitId: this.boat.id(),
|
||||||
|
planId: this.motionPlanId,
|
||||||
|
startTick: ticks + 1,
|
||||||
|
ticksPerStep: this.ticksPerMove,
|
||||||
|
path: fullPath,
|
||||||
|
});
|
||||||
|
this.motionPlanDst = this.dst;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
owner(): Player {
|
owner(): Player {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
PlayerUpdate,
|
PlayerUpdate,
|
||||||
UnitUpdate,
|
UnitUpdate,
|
||||||
} from "./GameUpdates";
|
} from "./GameUpdates";
|
||||||
|
import { MotionPlanRecord } from "./MotionPlans";
|
||||||
import { RailNetwork } from "./RailNetwork";
|
import { RailNetwork } from "./RailNetwork";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { UnitPredicate } from "./UnitGrid";
|
import { UnitPredicate } from "./UnitGrid";
|
||||||
@@ -767,6 +768,8 @@ export interface Game extends GameMap {
|
|||||||
inSpawnPhase(): boolean;
|
inSpawnPhase(): boolean;
|
||||||
executeNextTick(): GameUpdates;
|
executeNextTick(): GameUpdates;
|
||||||
drainPackedTileUpdates(): Uint32Array;
|
drainPackedTileUpdates(): Uint32Array;
|
||||||
|
recordMotionPlan(record: MotionPlanRecord): void;
|
||||||
|
drainPackedMotionPlans(): Uint32Array | null;
|
||||||
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void;
|
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void;
|
||||||
getWinner(): Player | Team | null;
|
getWinner(): Player | Team | null;
|
||||||
config(): Config;
|
config(): Config;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
} from "./Game";
|
} from "./Game";
|
||||||
import { GameMap, TileRef } from "./GameMap";
|
import { GameMap, TileRef } from "./GameMap";
|
||||||
import { GameUpdate, GameUpdateType } from "./GameUpdates";
|
import { GameUpdate, GameUpdateType } from "./GameUpdates";
|
||||||
|
import { MotionPlanRecord, packMotionPlans } from "./MotionPlans";
|
||||||
import { PlayerImpl } from "./PlayerImpl";
|
import { PlayerImpl } from "./PlayerImpl";
|
||||||
import { RailNetwork } from "./RailNetwork";
|
import { RailNetwork } from "./RailNetwork";
|
||||||
import { createRailNetwork } from "./RailNetworkImpl";
|
import { createRailNetwork } from "./RailNetworkImpl";
|
||||||
@@ -84,6 +85,8 @@ export class GameImpl implements Game {
|
|||||||
|
|
||||||
private updates: GameUpdates = createGameUpdatesMap();
|
private updates: GameUpdates = createGameUpdatesMap();
|
||||||
private tileUpdatePairs: number[] = [];
|
private tileUpdatePairs: number[] = [];
|
||||||
|
private motionPlanRecords: MotionPlanRecord[] = [];
|
||||||
|
private planDrivenUnitIds = new Set<number>();
|
||||||
private unitGrid: UnitGrid;
|
private unitGrid: UnitGrid;
|
||||||
|
|
||||||
private playerTeams: Team[];
|
private playerTeams: Team[];
|
||||||
@@ -430,6 +433,29 @@ export class GameImpl implements Game {
|
|||||||
return packed;
|
return packed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recordMotionPlan(record: MotionPlanRecord): void {
|
||||||
|
switch (record.kind) {
|
||||||
|
case "grid":
|
||||||
|
this.planDrivenUnitIds.add(record.unitId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.motionPlanRecords.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
isUnitPlanDriven(unitId: number): boolean {
|
||||||
|
return this.planDrivenUnitIds.has(unitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
drainPackedMotionPlans(): Uint32Array | null {
|
||||||
|
const records = this.motionPlanRecords;
|
||||||
|
if (records.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const packed = packMotionPlans(records);
|
||||||
|
records.length = 0;
|
||||||
|
return packed;
|
||||||
|
}
|
||||||
|
|
||||||
private hash(): number {
|
private hash(): number {
|
||||||
let hash = 1;
|
let hash = 1;
|
||||||
this._players.forEach((p) => {
|
this._players.forEach((p) => {
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ export interface GameUpdateViewData {
|
|||||||
* state (`uint16`) stored in a `uint32` lane.
|
* state (`uint16`) stored in a `uint32` lane.
|
||||||
*/
|
*/
|
||||||
packedTileUpdates: Uint32Array;
|
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>;
|
playerNameViewData: Record<string, NameViewData>;
|
||||||
tickExecutionDuration?: number;
|
tickExecutionDuration?: number;
|
||||||
pendingTurns?: number;
|
pendingTurns?: number;
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ import {
|
|||||||
PlayerUpdate,
|
PlayerUpdate,
|
||||||
UnitUpdate,
|
UnitUpdate,
|
||||||
} from "./GameUpdates";
|
} from "./GameUpdates";
|
||||||
|
import {
|
||||||
|
MOTION_PLANS_SCHEMA_VERSION,
|
||||||
|
MotionPlanRecord,
|
||||||
|
unpackMotionPlans,
|
||||||
|
} from "./MotionPlans";
|
||||||
import { TerrainMapData } from "./TerrainMapLoader";
|
import { TerrainMapData } from "./TerrainMapLoader";
|
||||||
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
import { TerraNulliusImpl } from "./TerraNulliusImpl";
|
||||||
import { UnitGrid, UnitPredicate } from "./UnitGrid";
|
import { UnitGrid, UnitPredicate } from "./UnitGrid";
|
||||||
@@ -83,6 +88,17 @@ export class UnitView {
|
|||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyDerivedPosition(pos: TileRef) {
|
||||||
|
const prev = this.data.pos;
|
||||||
|
this.lastPos.push(pos);
|
||||||
|
this._wasUpdated = true;
|
||||||
|
this.data = {
|
||||||
|
...this.data,
|
||||||
|
lastPos: prev,
|
||||||
|
pos,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
id(): number {
|
id(): number {
|
||||||
return this.data.id;
|
return this.data.id;
|
||||||
}
|
}
|
||||||
@@ -592,6 +608,15 @@ export class GameView implements GameMap {
|
|||||||
private _myPlayer: PlayerView | null = null;
|
private _myPlayer: PlayerView | null = null;
|
||||||
|
|
||||||
private unitGrid: UnitGrid;
|
private unitGrid: UnitGrid;
|
||||||
|
private unitMotionPlans = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
planId: number;
|
||||||
|
startTick: number;
|
||||||
|
ticksPerStep: number;
|
||||||
|
path: Uint32Array;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
private toDelete = new Set<number>();
|
private toDelete = new Set<number>();
|
||||||
|
|
||||||
@@ -637,6 +662,18 @@ export class GameView implements GameMap {
|
|||||||
return this.lastUpdate?.updates ?? null;
|
return this.lastUpdate?.updates ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public motionPlans(): ReadonlyMap<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
planId: number;
|
||||||
|
startTick: number;
|
||||||
|
ticksPerStep: number;
|
||||||
|
path: Uint32Array;
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
return this.unitMotionPlans;
|
||||||
|
}
|
||||||
|
|
||||||
public isCatchingUp(): boolean {
|
public isCatchingUp(): boolean {
|
||||||
return (this.lastUpdate?.pendingTurns ?? 0) > 1;
|
return (this.lastUpdate?.pendingTurns ?? 0) > 1;
|
||||||
}
|
}
|
||||||
@@ -656,6 +693,15 @@ export class GameView implements GameMap {
|
|||||||
this.updatedTiles.push(tile);
|
this.updatedTiles.push(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gu.packedMotionPlans) {
|
||||||
|
const { schemaVersion, records } = unpackMotionPlans(
|
||||||
|
gu.packedMotionPlans,
|
||||||
|
);
|
||||||
|
if (schemaVersion === MOTION_PLANS_SCHEMA_VERSION) {
|
||||||
|
this.applyMotionPlanRecords(records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (gu.updates === null) {
|
if (gu.updates === null) {
|
||||||
throw new Error("lastUpdate.updates not initialized");
|
throw new Error("lastUpdate.updates not initialized");
|
||||||
}
|
}
|
||||||
@@ -704,8 +750,72 @@ export class GameView implements GameMap {
|
|||||||
if (!unit.isActive()) {
|
if (!unit.isActive()) {
|
||||||
// Wait until next tick to delete the unit.
|
// Wait until next tick to delete the unit.
|
||||||
this.toDelete.add(unit.id());
|
this.toDelete.add(unit.id());
|
||||||
|
this.unitMotionPlans.delete(unit.id());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.advanceMotionPlannedUnits(gu.tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
private advanceMotionPlannedUnits(currentTick: Tick): void {
|
||||||
|
for (const [unitId, plan] of this.unitMotionPlans) {
|
||||||
|
const unit = this._units.get(unitId);
|
||||||
|
if (!unit || !unit.isActive()) {
|
||||||
|
this.unitMotionPlans.delete(unitId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldTile = unit.tile();
|
||||||
|
const newTile = this.motionTileAtTick(plan, currentTick);
|
||||||
|
unit.applyDerivedPosition(newTile);
|
||||||
|
|
||||||
|
if (newTile !== oldTile) {
|
||||||
|
this.unitGrid.updateUnitCell(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private motionTileAtTick(
|
||||||
|
plan: { startTick: number; ticksPerStep: number; path: Uint32Array },
|
||||||
|
tick: Tick,
|
||||||
|
): TileRef {
|
||||||
|
if (plan.path.length < 1) {
|
||||||
|
throw new Error("motion plan path must be non-empty");
|
||||||
|
}
|
||||||
|
const dt = tick - plan.startTick;
|
||||||
|
const stepIndex =
|
||||||
|
dt <= 0 ? 0 : Math.floor(dt / Math.max(1, plan.ticksPerStep));
|
||||||
|
const idx = Math.max(0, Math.min(plan.path.length - 1, stepIndex));
|
||||||
|
return plan.path[idx] as TileRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyMotionPlanRecords(records: readonly MotionPlanRecord[]): void {
|
||||||
|
for (const record of records) {
|
||||||
|
switch (record.kind) {
|
||||||
|
case "grid": {
|
||||||
|
if (record.ticksPerStep < 1 || record.path.length < 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const existing = this.unitMotionPlans.get(record.unitId);
|
||||||
|
if (existing && record.planId <= existing.planId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path =
|
||||||
|
record.path instanceof Uint32Array
|
||||||
|
? record.path
|
||||||
|
: Uint32Array.from(record.path);
|
||||||
|
|
||||||
|
this.unitMotionPlans.set(record.unitId, {
|
||||||
|
planId: record.planId,
|
||||||
|
startTick: record.startTick,
|
||||||
|
ticksPerStep: record.ticksPerStep,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recentlyUpdatedTiles(): TileRef[] {
|
recentlyUpdatedTiles(): TileRef[] {
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { TileRef } from "./GameMap";
|
||||||
|
|
||||||
|
export const MOTION_PLANS_SCHEMA_VERSION = 3;
|
||||||
|
|
||||||
|
export enum PackedMotionPlanKind {
|
||||||
|
GridPathSet = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridPathPlan {
|
||||||
|
kind: "grid";
|
||||||
|
unitId: number;
|
||||||
|
planId: number;
|
||||||
|
startTick: number;
|
||||||
|
ticksPerStep: number;
|
||||||
|
/**
|
||||||
|
* TileRef path where `path[0]` is the unit tile at `startTick`.
|
||||||
|
*/
|
||||||
|
path: readonly TileRef[] | Uint32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MotionPlanRecord = GridPathPlan;
|
||||||
|
|
||||||
|
export function packMotionPlans(
|
||||||
|
records: readonly MotionPlanRecord[],
|
||||||
|
): Uint32Array {
|
||||||
|
const out: number[] = [MOTION_PLANS_SCHEMA_VERSION, records.length];
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
switch (record.kind) {
|
||||||
|
case "grid": {
|
||||||
|
const path =
|
||||||
|
record.path instanceof Uint32Array
|
||||||
|
? record.path
|
||||||
|
: Uint32Array.from(record.path);
|
||||||
|
const pathLen = path.length >>> 0;
|
||||||
|
const wordCount = 2 + 5 + pathLen;
|
||||||
|
out.push(
|
||||||
|
PackedMotionPlanKind.GridPathSet,
|
||||||
|
wordCount,
|
||||||
|
record.unitId >>> 0,
|
||||||
|
record.planId >>> 0,
|
||||||
|
record.startTick >>> 0,
|
||||||
|
record.ticksPerStep >>> 0,
|
||||||
|
pathLen,
|
||||||
|
);
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
out.push(path[i] >>> 0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint32Array(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unpackMotionPlans(packed: Uint32Array): {
|
||||||
|
schemaVersion: number;
|
||||||
|
records: MotionPlanRecord[];
|
||||||
|
} {
|
||||||
|
if (packed.length < 2) {
|
||||||
|
return { schemaVersion: 0, records: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaVersion = packed[0] >>> 0;
|
||||||
|
const recordCount = packed[1] >>> 0;
|
||||||
|
|
||||||
|
const records: MotionPlanRecord[] = [];
|
||||||
|
let offset = 2;
|
||||||
|
|
||||||
|
for (let i = 0; i < recordCount && offset + 1 < packed.length; i++) {
|
||||||
|
const kind = packed[offset] >>> 0;
|
||||||
|
const wordCount = packed[offset + 1] >>> 0;
|
||||||
|
|
||||||
|
if (wordCount < 2 || offset + wordCount > packed.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case PackedMotionPlanKind.GridPathSet: {
|
||||||
|
if (wordCount < 2 + 5) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const unitId = packed[offset + 2] >>> 0;
|
||||||
|
const planId = packed[offset + 3] >>> 0;
|
||||||
|
const startTick = packed[offset + 4] >>> 0;
|
||||||
|
const ticksPerStep = packed[offset + 5] >>> 0;
|
||||||
|
const pathLen = packed[offset + 6] >>> 0;
|
||||||
|
|
||||||
|
const expectedWordCount = 2 + 5 + pathLen;
|
||||||
|
if (expectedWordCount !== wordCount) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathStart = offset + 7;
|
||||||
|
const pathEnd = pathStart + pathLen;
|
||||||
|
const path = packed.slice(pathStart, pathEnd);
|
||||||
|
|
||||||
|
records.push({
|
||||||
|
kind: "grid",
|
||||||
|
unitId,
|
||||||
|
planId,
|
||||||
|
startTick,
|
||||||
|
ticksPerStep,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Unknown kind: skip.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += wordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { schemaVersion, records };
|
||||||
|
}
|
||||||
@@ -160,7 +160,9 @@ export class UnitImpl implements Unit {
|
|||||||
this._lastTile = this._tile;
|
this._lastTile = this._tile;
|
||||||
this._tile = tile;
|
this._tile = tile;
|
||||||
this.mg.updateUnitTile(this);
|
this.mg.updateUnitTile(this);
|
||||||
this.mg.addUpdate(this.toUpdate());
|
if (!this.mg.isUnitPlanDriven(this._id)) {
|
||||||
|
this.mg.addUpdate(this.toUpdate());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTroops(troops: number): void {
|
setTroops(troops: number): void {
|
||||||
@@ -336,7 +338,10 @@ export class UnitImpl implements Unit {
|
|||||||
if (this.type() !== UnitType.TransportShip) {
|
if (this.type() !== UnitType.TransportShip) {
|
||||||
throw new Error(`Cannot retreat ${this.type()}`);
|
throw new Error(`Cannot retreat ${this.type()}`);
|
||||||
}
|
}
|
||||||
this._retreating = true;
|
if (!this._retreating) {
|
||||||
|
this._retreating = true;
|
||||||
|
this.mg.addUpdate(this.toUpdate());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isUnderConstruction(): boolean {
|
isUnderConstruction(): boolean {
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ function sendMessage(message: WorkerMessage) {
|
|||||||
if (message.type === "game_update") {
|
if (message.type === "game_update") {
|
||||||
// Transfer the packed tile updates buffer to avoid structured-clone copies and
|
// Transfer the packed tile updates buffer to avoid structured-clone copies and
|
||||||
// reduce worker-side memory churn during long runs / catch-up.
|
// reduce worker-side memory churn during long runs / catch-up.
|
||||||
ctx.postMessage(message, [message.gameUpdate.packedTileUpdates.buffer]);
|
const transfers: Transferable[] = [
|
||||||
|
message.gameUpdate.packedTileUpdates.buffer,
|
||||||
|
];
|
||||||
|
if (message.gameUpdate.packedMotionPlans) {
|
||||||
|
transfers.push(message.gameUpdate.packedMotionPlans.buffer);
|
||||||
|
}
|
||||||
|
ctx.postMessage(message, transfers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.postMessage(message);
|
ctx.postMessage(message);
|
||||||
|
|||||||
Reference in New Issue
Block a user