-
- ${lobby.numClients}
- ${lobby.numClients === 1 ? "Player" : "Players"} waiting
+
+
+ ${lobby.numClients}
+ ${lobby.numClients === 1 ? "Player" : "Players"} waiting
+
-
-
diff --git a/src/client/Transport.ts b/src/client/Transport.ts
index c13cbbeba..c4fd5f700 100644
--- a/src/client/Transport.ts
+++ b/src/client/Transport.ts
@@ -26,10 +26,6 @@ import {
} from "../core/Schemas";
import { LobbyConfig } from "./ClientGameRunner";
import { LocalServer } from "./LocalServer";
-import { UsernameInput } from "./UsernameInput";
-import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
-import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
-import { SinglePlayerModal } from "./SinglePlayerModal";
import { PlayerView } from "../core/game/GameView";
export class PauseGameEvent implements GameEvent {
@@ -175,6 +171,7 @@ export class Transport {
ClientPingMessageSchema.parse({
type: "ping",
clientID: this.lobbyConfig.clientID,
+ persistentID: this.lobbyConfig.persistentID,
gameID: this.lobbyConfig.gameID,
}),
),
@@ -311,7 +308,7 @@ export class Transport {
this.sendIntent({
type: "allianceRequest",
clientID: this.lobbyConfig.clientID,
- requestor: event.requestor.id(),
+ playerID: event.requestor.id(),
recipient: event.recipient.id(),
});
}
@@ -321,7 +318,7 @@ export class Transport {
type: "allianceRequestReply",
clientID: this.lobbyConfig.clientID,
requestor: event.requestor.id(),
- recipient: event.recipient.id(),
+ playerID: event.recipient.id(),
accept: event.accepted,
});
}
@@ -330,7 +327,7 @@ export class Transport {
this.sendIntent({
type: "breakAlliance",
clientID: this.lobbyConfig.clientID,
- requestor: event.requestor.id(),
+ playerID: event.requestor.id(),
recipient: event.recipient.id(),
});
}
@@ -352,7 +349,7 @@ export class Transport {
this.sendIntent({
type: "attack",
clientID: this.lobbyConfig.clientID,
- attackerID: this.lobbyConfig.playerID,
+ playerID: this.lobbyConfig.playerID,
targetID: event.targetID,
troops: event.troops,
});
@@ -362,7 +359,7 @@ export class Transport {
this.sendIntent({
type: "boat",
clientID: this.lobbyConfig.clientID,
- attackerID: this.lobbyConfig.playerID,
+ playerID: this.lobbyConfig.playerID,
targetID: event.targetID,
troops: event.troops,
x: event.cell.x,
@@ -374,7 +371,7 @@ export class Transport {
this.sendIntent({
type: "targetPlayer",
clientID: this.lobbyConfig.clientID,
- requestor: this.lobbyConfig.playerID,
+ playerID: this.lobbyConfig.playerID,
target: event.targetID,
});
}
@@ -383,7 +380,7 @@ export class Transport {
this.sendIntent({
type: "emoji",
clientID: this.lobbyConfig.clientID,
- sender: this.lobbyConfig.playerID,
+ playerID: this.lobbyConfig.playerID,
recipient:
event.recipient == AllPlayers ? AllPlayers : event.recipient.id(),
emoji: event.emoji,
@@ -394,7 +391,7 @@ export class Transport {
this.sendIntent({
type: "donate",
clientID: this.lobbyConfig.clientID,
- sender: event.sender.id(),
+ playerID: event.sender.id(),
recipient: event.recipient.id(),
troops: event.troops,
});
@@ -404,7 +401,7 @@ export class Transport {
this.sendIntent({
type: "troop_ratio",
clientID: this.lobbyConfig.clientID,
- player: this.lobbyConfig.playerID,
+ playerID: this.lobbyConfig.playerID,
ratio: event.ratio,
});
}
@@ -413,7 +410,7 @@ export class Transport {
this.sendIntent({
type: "build_unit",
clientID: this.lobbyConfig.clientID,
- player: this.lobbyConfig.playerID,
+ playerID: this.lobbyConfig.playerID,
unit: event.unit,
x: event.cell.x,
y: event.cell.y,
@@ -437,6 +434,7 @@ export class Transport {
const msg = ClientSendWinnerSchema.parse({
type: "winner",
clientID: this.lobbyConfig.clientID,
+ persistentID: this.lobbyConfig.persistentID,
gameID: this.lobbyConfig.gameID,
winner: event.winner,
});
@@ -455,6 +453,7 @@ export class Transport {
const msg = ClientIntentMessageSchema.parse({
type: "intent",
clientID: this.lobbyConfig.clientID,
+ persistentID: this.lobbyConfig.persistentID,
gameID: this.lobbyConfig.gameID,
intent: intent,
});
diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts
index dd1468265..3ddd1eed8 100644
--- a/src/client/graphics/layers/ControlPanel.ts
+++ b/src/client/graphics/layers/ControlPanel.ts
@@ -52,6 +52,10 @@ export class ControlPanel extends LitElement implements Layer {
@state()
private _goldPerSecond: number;
+ private _lastPopulationIncreaseRate: number;
+
+ private _popRateIsIncreasing: boolean = true;
+
init() {
this.attackRatio = 0.2;
this.uiState.attackRatio = this.attackRatio;
@@ -63,12 +67,19 @@ export class ControlPanel extends LitElement implements Layer {
this.setVisibile(true);
}
- const player = this.game.playerByClientID(this.clientID);
+ const player = this.game.myPlayer();
if (player == null || !player.isAlive()) {
this.setVisibile(false);
return;
}
+ const popIncreaseRate = player.population() - this._population;
+ if (this.game.ticks() % 5 == 0) {
+ this._popRateIsIncreasing =
+ popIncreaseRate >= this._lastPopulationIncreaseRate;
+ this._lastPopulationIncreaseRate = popIncreaseRate;
+ }
+
this._population = player.population();
this._maxPopulation = this.game.config().maxPopulation(player);
this._gold = player.gold();
@@ -78,6 +89,7 @@ export class ControlPanel extends LitElement implements Layer {
this._goldPerSecond = this.game.config().goldAdditionRate(player) * 10;
this.currentTroopRatio = player.troops() / player.population();
+ this.requestUpdate();
}
onAttackRatioChange(newRatio: number) {
@@ -163,7 +175,12 @@ export class ControlPanel extends LitElement implements Layer {
${renderTroops(this._population)} /
${renderTroops(this._maxPopulation)}
- (+${renderTroops(this.popRate)})(+${renderTroops(this.popRate)})
diff --git a/src/client/index.html b/src/client/index.html
index 375c8a83b..8f608969a 100644
--- a/src/client/index.html
+++ b/src/client/index.html
@@ -167,7 +167,7 @@
+
diff --git a/src/client/utilities/Maps.ts b/src/client/utilities/Maps.ts
index 6325b3073..a7306aaa0 100644
--- a/src/client/utilities/Maps.ts
+++ b/src/client/utilities/Maps.ts
@@ -5,6 +5,7 @@ import mena from "../../../resources/maps/Mena.png";
import northAmerica from "../../../resources/maps/NorthAmerica.png";
import blackSea from "../../../resources/maps/BlackSea.png";
import africa from "../../../resources/maps/Africa.png";
+ main
import { GameMapType } from "../../core/game/Game";
export function getMapsImage(map: GameMapType): string {
@@ -23,6 +24,8 @@ export function getMapsImage(map: GameMapType): string {
return blackSea;
case GameMapType.Africa
return Africa;
+
+ main
default:
return "";
}
diff --git a/src/core/PseudoRandom.ts b/src/core/PseudoRandom.ts
index e24815b07..985de26fd 100644
--- a/src/core/PseudoRandom.ts
+++ b/src/core/PseudoRandom.ts
@@ -47,4 +47,13 @@ export class PseudoRandom {
chance(odds: number): boolean {
return this.nextInt(0, odds) == 0;
}
+
+ shuffleArray(array: any[]) {
+ for (let i = array.length - 1; i >= 0; i--) {
+ const j = Math.floor(this.nextInt(0, i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+
+ return array;
+ }
}
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index 6ebcc2d7d..eb81c6698 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -133,11 +133,12 @@ const BaseIntentSchema = z.object({
"build_unit",
]),
clientID: ID,
+ playerID: ID,
});
export const AttackIntentSchema = BaseIntentSchema.extend({
type: z.literal("attack"),
- attackerID: ID,
+ playerID: ID,
targetID: ID.nullable(),
troops: z.number().nullable(),
});
@@ -154,7 +155,7 @@ export const SpawnIntentSchema = BaseIntentSchema.extend({
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
type: z.literal("boat"),
- attackerID: ID,
+ playerID: ID,
targetID: ID.nullable(),
troops: z.number().nullable(),
x: z.number(),
@@ -163,52 +164,52 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({
export const AllianceRequestIntentSchema = BaseIntentSchema.extend({
type: z.literal("allianceRequest"),
- requestor: ID,
+ playerID: ID,
recipient: ID,
});
export const AllianceRequestReplyIntentSchema = BaseIntentSchema.extend({
type: z.literal("allianceRequestReply"),
requestor: ID, // The one who made the original alliance request
- recipient: ID,
+ playerID: ID,
accept: z.boolean(),
});
export const BreakAllianceIntentSchema = BaseIntentSchema.extend({
type: z.literal("breakAlliance"),
- requestor: ID, // The one who made the original alliance request
+ playerID: ID,
recipient: ID,
});
export const TargetPlayerIntentSchema = BaseIntentSchema.extend({
type: z.literal("targetPlayer"),
- requestor: ID,
+ playerID: ID,
target: ID,
});
export const EmojiIntentSchema = BaseIntentSchema.extend({
type: z.literal("emoji"),
- sender: ID,
+ playerID: ID,
recipient: z.union([ID, z.literal(AllPlayers)]),
emoji: EmojiSchema,
});
export const DonateIntentSchema = BaseIntentSchema.extend({
type: z.literal("donate"),
- sender: ID,
+ playerID: ID,
recipient: ID,
troops: z.number().nullable(),
});
export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({
type: z.literal("troop_ratio"),
- player: ID,
+ playerID: ID,
ratio: z.number().min(0).max(1),
});
export const BuildUnitIntentSchema = BaseIntentSchema.extend({
type: z.literal("build_unit"),
- player: ID,
+ playerID: ID,
unit: z.nativeEnum(UnitType),
x: z.number(),
y: z.number(),
@@ -267,6 +268,7 @@ export const ServerMessageSchema = z.union([
const ClientBaseMessageSchema = z.object({
type: z.enum(["winner", "join", "intent", "ping", "log"]),
clientID: ID,
+ persistentID: SafeString.nullable(), // WARNING: persistent id is private.
gameID: ID,
});
@@ -294,7 +296,6 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({
// WARNING: never send this message to clients.
export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
type: z.literal("join"),
- persistentID: SafeString, // WARNING: persistent id is private.
lastTurn: z.number(), // The last turn the client saw.
username: SafeString,
});
diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts
index 6bff9b4b0..0bd6a365f 100644
--- a/src/core/execution/AttackExecution.ts
+++ b/src/core/execution/AttackExecution.ts
@@ -62,6 +62,17 @@ export class AttackExecution implements Execution {
}
this.mg = mg;
+ if (!mg.hasPlayer(this._ownerID)) {
+ console.warn(`player ${this._ownerID} not found`);
+ this.active = false;
+ return;
+ }
+ if (this._targetID != null && !mg.hasPlayer(this._targetID)) {
+ console.warn(`target ${this._targetID} not found`);
+ this.active = false;
+ return;
+ }
+
this._owner = mg.player(this._ownerID);
this.target =
this._targetID == this.mg.terraNullius().id()
@@ -90,8 +101,8 @@ export class AttackExecution implements Execution {
.config()
.attackAmount(this._owner, this.target);
}
- this.startTroops = Math.min(this._owner.troops(), this.startTroops);
if (this.removeTroops) {
+ this.startTroops = Math.min(this._owner.troops(), this.startTroops);
this._owner.removeTroops(this.startTroops);
}
this.attack = this._owner.createAttack(
diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts
index 47270708e..d6cbaf2c6 100644
--- a/src/core/execution/CityExecution.ts
+++ b/src/core/execution/CityExecution.ts
@@ -22,6 +22,11 @@ export class CityExecution implements Execution {
init(mg: Game, ticks: number): void {
this.mg = mg;
+ if (!mg.hasPlayer(this.ownerId)) {
+ console.warn(`CityExecution: player ${this.ownerId} not found`);
+ this.active = false;
+ return;
+ }
this.player = mg.player(this.ownerId);
}
diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts
index be86ad275..f6a545528 100644
--- a/src/core/execution/ConstructionExecution.ts
+++ b/src/core/execution/ConstructionExecution.ts
@@ -36,6 +36,11 @@ export class ConstructionExecution implements Execution {
init(mg: Game, ticks: number): void {
this.mg = mg;
+ if (!mg.hasPlayer(this.ownerId)) {
+ console.warn(`ConstructionExecution: owner ${this.ownerId} not found`);
+ this.active = false;
+ return;
+ }
this.player = mg.player(this.ownerId);
}
diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts
index d298ebc1e..74001526b 100644
--- a/src/core/execution/DefensePostExecution.ts
+++ b/src/core/execution/DefensePostExecution.ts
@@ -23,6 +23,11 @@ export class DefensePostExecution implements Execution {
init(mg: Game, ticks: number): void {
this.mg = mg;
+ if (!mg.hasPlayer(this.ownerId)) {
+ console.warn(`DefensePostExectuion: owner ${this.ownerId} not found`);
+ this.active = false;
+ return;
+ }
this.player = mg.player(this.ownerId);
}
diff --git a/src/core/execution/DonateExecution.ts b/src/core/execution/DonateExecution.ts
index 0cbefa5da..70012b0e3 100644
--- a/src/core/execution/DonateExecution.ts
+++ b/src/core/execution/DonateExecution.ts
@@ -14,6 +14,17 @@ export class DonateExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.senderID)) {
+ console.warn(`DonateExecution: sender ${this.senderID} not found`);
+ this.active = false;
+ return;
+ }
+ if (!mg.hasPlayer(this.recipientID)) {
+ console.warn(`DonateExecution recipient ${this.recipientID} not found`);
+ this.active = false;
+ return;
+ }
+
this.sender = mg.player(this.senderID);
this.recipient = mg.player(this.recipientID);
if (this.troops == null) {
diff --git a/src/core/execution/EmojiExecution.ts b/src/core/execution/EmojiExecution.ts
index 4a7147469..1793ce7d2 100644
--- a/src/core/execution/EmojiExecution.ts
+++ b/src/core/execution/EmojiExecution.ts
@@ -22,6 +22,17 @@ export class EmojiExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.senderID)) {
+ console.warn(`EmojiExecution: sender ${this.senderID} not found`);
+ this.active = false;
+ return;
+ }
+ if (this.recipientID != AllPlayers && !mg.hasPlayer(this.recipientID)) {
+ console.warn(`EmojiExecution: recipient ${this.recipientID} not found`);
+ this.active = false;
+ return;
+ }
+
this.requestor = mg.player(this.senderID);
this.recipient =
this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID);
diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts
index b7e3f2c92..e876c9cac 100644
--- a/src/core/execution/ExecutionManager.ts
+++ b/src/core/execution/ExecutionManager.ts
@@ -33,6 +33,7 @@ import { DonateExecution } from "./DonateExecution";
import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
import { ConstructionExecution } from "./ConstructionExecution";
import { fixProfaneUsername, isProfaneUsername } from "../validations/username";
+import { NoOpExecution } from "./NoOpExecution";
export class Executor {
// private random = new PseudoRandom(999)
@@ -52,11 +53,26 @@ export class Executor {
}
createExec(intent: Intent): Execution {
+ if (intent.type != "spawn") {
+ if (!this.mg.hasPlayer(intent.playerID)) {
+ console.warn(
+ `player ${intent.playerID} not found on intent ${intent.type}`,
+ );
+ return new NoOpExecution();
+ }
+ const player = this.mg.player(intent.playerID);
+ if (player.clientID() != intent.clientID) {
+ console.warn(
+ `intent ${intent.type} has incorrect clientID ${intent.clientID} for player ${player.name()} with clientID ${player.clientID()}`,
+ );
+ return new NoOpExecution();
+ }
+ }
switch (intent.type) {
case "attack": {
return new AttackExecution(
intent.troops,
- intent.attackerID,
+ intent.playerID,
intent.targetID,
null,
);
@@ -77,40 +93,40 @@ export class Executor {
);
case "boat":
return new TransportShipExecution(
- intent.attackerID,
+ intent.playerID,
intent.targetID,
this.mg.ref(intent.x, intent.y),
intent.troops,
);
case "allianceRequest":
- return new AllianceRequestExecution(intent.requestor, intent.recipient);
+ return new AllianceRequestExecution(intent.playerID, intent.recipient);
case "allianceRequestReply":
return new AllianceRequestReplyExecution(
intent.requestor,
- intent.recipient,
+ intent.playerID,
intent.accept,
);
case "breakAlliance":
- return new BreakAllianceExecution(intent.requestor, intent.recipient);
+ return new BreakAllianceExecution(intent.playerID, intent.recipient);
case "targetPlayer":
- return new TargetPlayerExecution(intent.requestor, intent.target);
+ return new TargetPlayerExecution(intent.playerID, intent.target);
case "emoji":
return new EmojiExecution(
- intent.sender,
+ intent.playerID,
intent.recipient,
intent.emoji,
);
case "donate":
return new DonateExecution(
- intent.sender,
+ intent.playerID,
intent.recipient,
intent.troops,
);
case "troop_ratio":
- return new SetTargetTroopRatioExecution(intent.player, intent.ratio);
+ return new SetTargetTroopRatioExecution(intent.playerID, intent.ratio);
case "build_unit":
return new ConstructionExecution(
- intent.player,
+ intent.playerID,
this.mg.ref(intent.x, intent.y),
intent.unit,
);
diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts
index 682c3e44f..0b4a02abc 100644
--- a/src/core/execution/FakeHumanExecution.ts
+++ b/src/core/execution/FakeHumanExecution.ts
@@ -546,9 +546,6 @@ export class FakeHumanExecution implements Execution {
}
sendAttack(toAttack: Player | TerraNullius) {
- console.log(
- `${this.player.name()} sending troops ${renderTroops(this.player.troops() / 5)}`,
- );
this.mg.addExecution(
new AttackExecution(
this.player.troops() / 5,
diff --git a/src/core/execution/MIRVExecution.ts b/src/core/execution/MIRVExecution.ts
index 430df2e59..87638fe7b 100644
--- a/src/core/execution/MIRVExecution.ts
+++ b/src/core/execution/MIRVExecution.ts
@@ -43,6 +43,12 @@ export class MirvExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.senderID)) {
+ console.warn(`MIRVExecution: player ${this.senderID} not found`);
+ this.active = false;
+ return;
+ }
+
this.random = new PseudoRandom(mg.ticks() + simpleHash(this.senderID));
this.mg = mg;
this.pathFinder = PathFinder.Mini(mg, 10_000, true);
diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts
index dd3265262..3dc1a8714 100644
--- a/src/core/execution/MissileSiloExecution.ts
+++ b/src/core/execution/MissileSiloExecution.ts
@@ -22,6 +22,12 @@ export class MissileSiloExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this._owner)) {
+ console.warn(`MissileSiloExecution: owner ${this._owner} not found`);
+ this.active = false;
+ return;
+ }
+
this.mg = mg;
this.player = mg.player(this._owner);
}
diff --git a/src/core/execution/NoOpExecution.ts b/src/core/execution/NoOpExecution.ts
new file mode 100644
index 000000000..eefce20ed
--- /dev/null
+++ b/src/core/execution/NoOpExecution.ts
@@ -0,0 +1,15 @@
+import { Execution, Game, Player } from "../game/Game";
+
+export class NoOpExecution implements Execution {
+ isActive(): boolean {
+ return false;
+ }
+ activeDuringSpawnPhase(): boolean {
+ return false;
+ }
+ init(mg: Game, ticks: number): void {}
+ tick(ticks: number): void {}
+ owner(): Player {
+ return null;
+ }
+}
diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts
index 38f80a933..5f2906120 100644
--- a/src/core/execution/NukeExecution.ts
+++ b/src/core/execution/NukeExecution.ts
@@ -33,6 +33,12 @@ export class NukeExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.senderID)) {
+ console.warn(`NukeExecution: sender ${this.senderID} not found`);
+ this.active = false;
+ return;
+ }
+
this.mg = mg;
this.player = mg.player(this.senderID);
this.random = new PseudoRandom(ticks);
@@ -120,18 +126,13 @@ export class NukeExecution implements Execution {
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer;
});
- const ratio = Object.fromEntries(
- this.mg
- .players()
- .map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]),
- );
const attacked = new Map
();
for (const tile of toDestroy) {
const owner = this.mg.owner(tile);
if (owner.isPlayer()) {
const mp = this.mg.player(owner.id());
mp.relinquish(tile);
- mp.removeTroops(2 * ratio[mp.id()]);
+ mp.removeTroops((5 * mp.population()) / mp.numTilesOwned());
if (!attacked.has(mp)) {
attacked.set(mp, 0);
}
diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts
index 59a69386c..454bc03e4 100644
--- a/src/core/execution/PlayerExecution.ts
+++ b/src/core/execution/PlayerExecution.ts
@@ -28,6 +28,11 @@ export class PlayerExecution implements Execution {
}
init(mg: Game, ticks: number) {
+ if (!mg.hasPlayer(this.playerID)) {
+ console.warn(`PlayerExecution: player ${this.playerID} not found`);
+ this.active = false;
+ return;
+ }
this.mg = mg;
this.config = mg.config();
this.player = mg.player(this.playerID);
diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts
index 0d0b99815..18053299a 100644
--- a/src/core/execution/PortExecution.ts
+++ b/src/core/execution/PortExecution.ts
@@ -31,6 +31,11 @@ export class PortExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this._owner)) {
+ console.warn(`PortExecution: player ${this._owner} not found`);
+ this.active = false;
+ return;
+ }
this.mg = mg;
this.random = new PseudoRandom(mg.ticks());
}
diff --git a/src/core/execution/SetTargetTroopRatioExecution.ts b/src/core/execution/SetTargetTroopRatioExecution.ts
index 191e88f79..495c8ebe6 100644
--- a/src/core/execution/SetTargetTroopRatioExecution.ts
+++ b/src/core/execution/SetTargetTroopRatioExecution.ts
@@ -12,6 +12,11 @@ export class SetTargetTroopRatioExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.playerID)) {
+ console.warn(
+ `SetTargetTRoopRatioExecution: player ${this.playerID} not found`,
+ );
+ }
this.player = mg.player(this.playerID);
}
diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts
index 5ee3b0ebc..0c8cc5687 100644
--- a/src/core/execution/SpawnExecution.ts
+++ b/src/core/execution/SpawnExecution.ts
@@ -28,6 +28,7 @@ export class SpawnExecution implements Execution {
this.active = false;
if (!this.mg.inSpawnPhase()) {
+ this.active = false;
return;
}
diff --git a/src/core/execution/TargetPlayerExecution.ts b/src/core/execution/TargetPlayerExecution.ts
index f93989aee..ef738b774 100644
--- a/src/core/execution/TargetPlayerExecution.ts
+++ b/src/core/execution/TargetPlayerExecution.ts
@@ -12,6 +12,19 @@ export class TargetPlayerExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.requestorID)) {
+ console.warn(
+ `TargetPlayerExecution: requestor ${this.requestorID} not found`,
+ );
+ this.active = false;
+ return;
+ }
+ if (!mg.hasPlayer(this.targetID)) {
+ console.warn(`TargetPlayerExecution: target ${this.targetID} not found`);
+ this.active = false;
+ return;
+ }
+
this.requestor = mg.player(this.requestorID);
this.target = mg.player(this.targetID);
}
diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts
index fb4f7f4c0..7f9f6e682 100644
--- a/src/core/execution/TransportShipExecution.ts
+++ b/src/core/execution/TransportShipExecution.ts
@@ -50,6 +50,19 @@ export class TransportShipExecution implements Execution {
}
init(mg: Game, ticks: number) {
+ if (!mg.hasPlayer(this.attackerID)) {
+ console.warn(
+ `TransportShipExecution: attacker ${this.attackerID} not found`,
+ );
+ this.active = false;
+ return;
+ }
+ if (this.targetID != null && !mg.hasPlayer(this.targetID)) {
+ console.warn(`TransportShipExecution: target ${this.targetID} not found`);
+ this.active = false;
+ return;
+ }
+
this.lastMove = ticks;
this.mg = mg;
this.pathFinder = PathFinder.Mini(mg, 10_000, false, 2);
diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts
index c84cc9823..09f806d8a 100644
--- a/src/core/execution/WarshipExecution.ts
+++ b/src/core/execution/WarshipExecution.ts
@@ -43,6 +43,11 @@ export class WarshipExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.playerID)) {
+ console.log(`WarshipExecution: player ${this.playerID} not found`);
+ this.active = false;
+ return;
+ }
this.pathfinder = PathFinder.Mini(mg, 5000, false);
this._owner = mg.player(this.playerID);
this.mg = mg;
diff --git a/src/core/execution/alliance/AllianceRequestExecution.ts b/src/core/execution/alliance/AllianceRequestExecution.ts
index c5a20843a..21128d840 100644
--- a/src/core/execution/alliance/AllianceRequestExecution.ts
+++ b/src/core/execution/alliance/AllianceRequestExecution.ts
@@ -19,6 +19,21 @@ export class AllianceRequestExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.requestorID)) {
+ console.warn(
+ `AllianceRequestExecution requester ${this.requestorID} not found`,
+ );
+ this.active = false;
+ return;
+ }
+ if (!mg.hasPlayer(this.recipientID)) {
+ console.warn(
+ `AllianceRequestExecution recipient ${this.recipientID} not found`,
+ );
+ this.active = false;
+ return;
+ }
+
this.mg = mg;
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
diff --git a/src/core/execution/alliance/AllianceRequestReplyExecution.ts b/src/core/execution/alliance/AllianceRequestReplyExecution.ts
index 4250d476a..d0c0d4688 100644
--- a/src/core/execution/alliance/AllianceRequestReplyExecution.ts
+++ b/src/core/execution/alliance/AllianceRequestReplyExecution.ts
@@ -20,6 +20,20 @@ export class AllianceRequestReplyExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.requestorID)) {
+ console.warn(
+ `AllianceRequestReplyExecution requester ${this.requestorID} not found`,
+ );
+ this.active = false;
+ return;
+ }
+ if (!mg.hasPlayer(this.recipientID)) {
+ console.warn(
+ `AllianceRequestReplyExecution recipient ${this.recipientID} not found`,
+ );
+ this.active = false;
+ return;
+ }
this.mg = mg;
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
diff --git a/src/core/execution/alliance/BreakAllianceExecution.ts b/src/core/execution/alliance/BreakAllianceExecution.ts
index 2beff3d8a..074e72ab7 100644
--- a/src/core/execution/alliance/BreakAllianceExecution.ts
+++ b/src/core/execution/alliance/BreakAllianceExecution.ts
@@ -19,6 +19,20 @@ export class BreakAllianceExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
+ if (!mg.hasPlayer(this.requestorID)) {
+ console.warn(
+ `BreakAllianceExecution requester ${this.requestorID} not found`,
+ );
+ this.active = false;
+ return;
+ }
+ if (!mg.hasPlayer(this.recipientID)) {
+ console.warn(
+ `BreakAllianceExecution: recipient ${this.recipientID} not found`,
+ );
+ this.active = false;
+ return;
+ }
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
this.mg = mg;
diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts
index f2d212932..c949ead5f 100644
--- a/src/server/GameManager.ts
+++ b/src/server/GameManager.ts
@@ -9,6 +9,7 @@ import { PseudoRandom } from "../core/PseudoRandom";
export class GameManager {
private lastNewLobby: number = 0;
+ private mapsPlaylist: GameMapType[] = [];
private games: GameServer[] = [];
@@ -77,6 +78,31 @@ export class GameManager {
}
}
+ private getNextMap(): GameMapType {
+ if (this.mapsPlaylist.length > 0) {
+ return this.mapsPlaylist.shift();
+ }
+ while (true) {
+ this.mapsPlaylist = Object.values(GameMapType);
+ this.mapsPlaylist.push(GameMapType.World);
+ this.mapsPlaylist.push(GameMapType.Europe);
+ this.random.shuffleArray(this.mapsPlaylist);
+ if (this.allNonConsecutive(this.mapsPlaylist)) {
+ return this.mapsPlaylist.shift();
+ }
+ }
+ }
+
+ private allNonConsecutive(maps: GameMapType[]): boolean {
+ // Check for consecutive duplicates in the maps array
+ for (let i = 0; i < maps.length - 1; i++) {
+ if (maps[i] === maps[i + 1]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
tick() {
const lobbies = this.gamesByPhase(GamePhase.Lobby);
const active = this.gamesByPhase(GamePhase.Active);
@@ -87,7 +113,7 @@ export class GameManager {
this.lastNewLobby = now;
lobbies.push(
new GameServer(generateID(), now, true, this.config, {
- gameMap: this.random.randElement(Object.values(GameMapType)),
+ gameMap: this.getNextMap(),
gameType: GameType.Public,
difficulty: Difficulty.Medium,
disableBots: false,
diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts
index 1b77e274b..e7b2acafc 100644
--- a/src/server/GameServer.ts
+++ b/src/server/GameServer.ts
@@ -103,6 +103,19 @@ export class GameServer {
const clientMsg: ClientMessage = ClientMessageSchema.parse(
JSON.parse(message),
);
+ if (this.allClients.has(clientMsg.clientID)) {
+ const client = this.allClients.get(clientMsg.clientID);
+ if (client.persistentID != clientMsg.persistentID) {
+ console.warn(
+ `Client ID ${clientMsg.clientID} sent incorrect id ${clientMsg.persistentID}, does not match persistent id ${client.persistentID}`,
+ );
+ return;
+ }
+ }
+
+ // Clear out persistent id to make sure it doesn't get sent to other clients.
+ clientMsg.persistentID = null;
+
if (clientMsg.type == "intent") {
if (clientMsg.gameID == this.id) {
this.addIntent(clientMsg.intent);