From 9c1f7b407c90950a23820831a0e1d772e415e805 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Sun, 16 Nov 2025 01:00:17 +0100 Subject: [PATCH 1/5] Fix nation relation exploit --- src/core/execution/DonateGoldExecution.ts | 36 +++++++++++++-- src/core/execution/DonateTroopExecution.ts | 51 ++++++++++++++++++++-- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts index 51b60ce0a1..00e1b422db 100644 --- a/src/core/execution/DonateGoldExecution.ts +++ b/src/core/execution/DonateGoldExecution.ts @@ -1,8 +1,18 @@ -import { Execution, Game, Gold, Player, PlayerID } from "../game/Game"; +import { + Difficulty, + Execution, + Game, + Gold, + Player, + PlayerID, +} from "../game/Game"; +import { PseudoRandom } from "../PseudoRandom"; import { toInt } from "../Util"; export class DonateGoldExecution implements Execution { private recipient: Player; + private random: PseudoRandom; + private mg: Game; private active = true; private gold: Gold; @@ -16,8 +26,13 @@ export class DonateGoldExecution implements Execution { } init(mg: Game, ticks: number): void { + this.mg = mg; + this.random = new PseudoRandom(mg.ticks()); + if (!mg.hasPlayer(this.recipientID)) { - console.warn(`DonateExecution recipient ${this.recipientID} not found`); + console.warn( + `DonateGoldExecution recipient ${this.recipientID} not found`, + ); this.active = false; return; } @@ -32,7 +47,10 @@ export class DonateGoldExecution implements Execution { this.sender.canDonateGold(this.recipient) && this.sender.donateGold(this.recipient, this.gold) ) { - this.recipient.updateRelation(this.sender, 50); + // Prevent players from just buying a good relation by sending 1% gold. Instead, a minimum is needed, and it's random. + if (this.gold >= BigInt(this.getMinGoldForRelationUpdate())) { + this.recipient.updateRelation(this.sender, 50); + } } else { console.warn( `cannot send gold from ${this.sender.name()} to ${this.recipient.name()}`, @@ -41,6 +59,18 @@ export class DonateGoldExecution implements Execution { this.active = false; } + getMinGoldForRelationUpdate(): number { + const { difficulty } = this.mg.config().gameConfig(); + if (difficulty === Difficulty.Easy) return this.random.nextInt(0, 25_000); + if (difficulty === Difficulty.Medium) + return this.random.nextInt(25_000, 50_000); + if (difficulty === Difficulty.Hard) + return this.random.nextInt(50_000, 125_000); + if (difficulty === Difficulty.Impossible) + return this.random.nextInt(125_000, 250_000); + return 0; + } + isActive(): boolean { return this.active; } diff --git a/src/core/execution/DonateTroopExecution.ts b/src/core/execution/DonateTroopExecution.ts index 00af5de7c1..89b0cebff1 100644 --- a/src/core/execution/DonateTroopExecution.ts +++ b/src/core/execution/DonateTroopExecution.ts @@ -1,7 +1,10 @@ -import { Execution, Game, Player, PlayerID } from "../game/Game"; +import { Difficulty, Execution, Game, Player, PlayerID } from "../game/Game"; +import { PseudoRandom } from "../PseudoRandom"; export class DonateTroopsExecution implements Execution { private recipient: Player; + private random: PseudoRandom; + private mg: Game; private active = true; @@ -12,8 +15,13 @@ export class DonateTroopsExecution implements Execution { ) {} init(mg: Game, ticks: number): void { + this.mg = mg; + this.random = new PseudoRandom(mg.ticks()); + if (!mg.hasPlayer(this.recipientID)) { - console.warn(`DonateExecution recipient ${this.recipientID} not found`); + console.warn( + `DonateTroopExecution recipient ${this.recipientID} not found`, + ); this.active = false; return; } @@ -31,7 +39,10 @@ export class DonateTroopsExecution implements Execution { this.sender.canDonateTroops(this.recipient) && this.sender.donateTroops(this.recipient, this.troops) ) { - this.recipient.updateRelation(this.sender, 50); + // Prevent players from just buying a good relation by sending 1% troops. Instead, a minimum is needed, and it's random. + if (this.troops >= this.getMinTroopsForRelationUpdate()) { + this.recipient.updateRelation(this.sender, 50); + } } else { console.warn( `cannot send troops from ${this.sender} to ${this.recipient}`, @@ -40,6 +51,40 @@ export class DonateTroopsExecution implements Execution { this.active = false; } + getMinTroopsForRelationUpdate(): number { + const { difficulty } = this.mg.config().gameConfig(); + + // ~7.7k - ~9.1k troops (for 100k troops) + if (difficulty === Difficulty.Easy) + return this.random.nextInt( + this.sender.troops() / 13, + this.sender.troops() / 11, + ); + + // ~9.1k - ~11.1k troops (for 100k troops) + if (difficulty === Difficulty.Medium) + return this.random.nextInt( + this.sender.troops() / 11, + this.sender.troops() / 9, + ); + + // ~11.1k - ~14.3k troops (for 100k troops) + if (difficulty === Difficulty.Hard) + return this.random.nextInt( + this.sender.troops() / 9, + this.sender.troops() / 7, + ); + + // ~14.3k - ~20k troops (for 100k troops) + if (difficulty === Difficulty.Impossible) + return this.random.nextInt( + this.sender.troops() / 7, + this.sender.troops() / 5, + ); + + return 0; + } + isActive(): boolean { return this.active; } From f3d4ab2a8ee49d0b653c027ba16174888c1d5d1c Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Wed, 26 Nov 2025 23:58:25 +0100 Subject: [PATCH 2/5] CodeRabbit idea implemented --- src/core/execution/DonateTroopExecution.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/execution/DonateTroopExecution.ts b/src/core/execution/DonateTroopExecution.ts index 89b0cebff1..79c9a9e386 100644 --- a/src/core/execution/DonateTroopExecution.ts +++ b/src/core/execution/DonateTroopExecution.ts @@ -35,12 +35,15 @@ export class DonateTroopsExecution implements Execution { tick(ticks: number): void { if (this.troops === null) throw new Error("not initialized"); + + const minTroops = this.getMinTroopsForRelationUpdate(); + if ( this.sender.canDonateTroops(this.recipient) && this.sender.donateTroops(this.recipient, this.troops) ) { // Prevent players from just buying a good relation by sending 1% troops. Instead, a minimum is needed, and it's random. - if (this.troops >= this.getMinTroopsForRelationUpdate()) { + if (this.troops >= minTroops) { this.recipient.updateRelation(this.sender, 50); } } else { From 1ce1eae1353825f2c3b71e300df323dd8e2c97ee Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Thu, 27 Nov 2025 19:03:02 +0100 Subject: [PATCH 3/5] Added evan recommendations --- src/core/execution/DonateGoldExecution.ts | 40 ++++++++++----- src/core/execution/DonateTroopExecution.ts | 60 +++++++++++----------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts index 00e1b422db..33771cc7b9 100644 --- a/src/core/execution/DonateGoldExecution.ts +++ b/src/core/execution/DonateGoldExecution.ts @@ -47,10 +47,9 @@ export class DonateGoldExecution implements Execution { this.sender.canDonateGold(this.recipient) && this.sender.donateGold(this.recipient, this.gold) ) { - // Prevent players from just buying a good relation by sending 1% gold. Instead, a minimum is needed, and it's random. - if (this.gold >= BigInt(this.getMinGoldForRelationUpdate())) { - this.recipient.updateRelation(this.sender, 50); - } + // Give relation points based on how much gold was donated + const relationUpdate = this.calculateRelationUpdate(Number(this.gold)); + this.recipient.updateRelation(this.sender, relationUpdate); } else { console.warn( `cannot send gold from ${this.sender.name()} to ${this.recipient.name()}`, @@ -59,16 +58,31 @@ export class DonateGoldExecution implements Execution { this.active = false; } - getMinGoldForRelationUpdate(): number { + getGoldChunkSize(): number { const { difficulty } = this.mg.config().gameConfig(); - if (difficulty === Difficulty.Easy) return this.random.nextInt(0, 25_000); - if (difficulty === Difficulty.Medium) - return this.random.nextInt(25_000, 50_000); - if (difficulty === Difficulty.Hard) - return this.random.nextInt(50_000, 125_000); - if (difficulty === Difficulty.Impossible) - return this.random.nextInt(125_000, 250_000); - return 0; + switch (difficulty) { + case Difficulty.Easy: + return this.random.nextInt(1, 2_500); + case Difficulty.Medium: + return this.random.nextInt(2_500, 5_000); + case Difficulty.Hard: + return this.random.nextInt(5_000, 12_500); + case Difficulty.Impossible: + return this.random.nextInt(12_500, 25_000); + default: + return 2_500; + } + } + + calculateRelationUpdate(goldSent: number): number { + const chunkSize = this.getGoldChunkSize(); + // Calculate how many complete chunks were donated + const chunks = Math.floor(goldSent / chunkSize); + // Each chunk gives 5 relation points + const relationUpdate = chunks * 5; + // Cap at 100 relation points + if (relationUpdate > 100) return 100; + return relationUpdate; } isActive(): boolean { diff --git a/src/core/execution/DonateTroopExecution.ts b/src/core/execution/DonateTroopExecution.ts index 79c9a9e386..29701d7bb5 100644 --- a/src/core/execution/DonateTroopExecution.ts +++ b/src/core/execution/DonateTroopExecution.ts @@ -56,36 +56,36 @@ export class DonateTroopsExecution implements Execution { getMinTroopsForRelationUpdate(): number { const { difficulty } = this.mg.config().gameConfig(); - - // ~7.7k - ~9.1k troops (for 100k troops) - if (difficulty === Difficulty.Easy) - return this.random.nextInt( - this.sender.troops() / 13, - this.sender.troops() / 11, - ); - - // ~9.1k - ~11.1k troops (for 100k troops) - if (difficulty === Difficulty.Medium) - return this.random.nextInt( - this.sender.troops() / 11, - this.sender.troops() / 9, - ); - - // ~11.1k - ~14.3k troops (for 100k troops) - if (difficulty === Difficulty.Hard) - return this.random.nextInt( - this.sender.troops() / 9, - this.sender.troops() / 7, - ); - - // ~14.3k - ~20k troops (for 100k troops) - if (difficulty === Difficulty.Impossible) - return this.random.nextInt( - this.sender.troops() / 7, - this.sender.troops() / 5, - ); - - return 0; + const recipientMaxTroops = this.mg.config().maxTroops(this.recipient); + + switch (difficulty) { + // ~7.7k - ~9.1k troops (for 100k troops) + case Difficulty.Easy: + return this.random.nextInt( + recipientMaxTroops / 13, + recipientMaxTroops / 11, + ); + // ~9.1k - ~11.1k troops (for 100k troops) + case Difficulty.Medium: + return this.random.nextInt( + recipientMaxTroops / 11, + recipientMaxTroops / 9, + ); + // ~11.1k - ~14.3k troops (for 100k troops) + case Difficulty.Hard: + return this.random.nextInt( + recipientMaxTroops / 9, + recipientMaxTroops / 7, + ); + // ~14.3k - ~20k troops (for 100k troops) + case Difficulty.Impossible: + return this.random.nextInt( + recipientMaxTroops / 7, + recipientMaxTroops / 5, + ); + default: + return 0; + } } isActive(): boolean { From 90dff3100e44f1066a18fbd8a6005a01614598b0 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:09:15 +0100 Subject: [PATCH 4/5] Add coderabbit recommendation --- src/core/execution/DonateGoldExecution.ts | 23 ++++++++++++---------- src/core/execution/DonateTroopExecution.ts | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts index 33771cc7b9..d63fe8a013 100644 --- a/src/core/execution/DonateGoldExecution.ts +++ b/src/core/execution/DonateGoldExecution.ts @@ -48,8 +48,10 @@ export class DonateGoldExecution implements Execution { this.sender.donateGold(this.recipient, this.gold) ) { // Give relation points based on how much gold was donated - const relationUpdate = this.calculateRelationUpdate(Number(this.gold)); - this.recipient.updateRelation(this.sender, relationUpdate); + const relationUpdate = this.calculateRelationUpdate(this.gold); + if (relationUpdate > 0) { + this.recipient.updateRelation(this.sender, relationUpdate); + } } else { console.warn( `cannot send gold from ${this.sender.name()} to ${this.recipient.name()}`, @@ -58,26 +60,27 @@ export class DonateGoldExecution implements Execution { this.active = false; } - getGoldChunkSize(): number { + getGoldChunkSize(): Gold { const { difficulty } = this.mg.config().gameConfig(); switch (difficulty) { case Difficulty.Easy: - return this.random.nextInt(1, 2_500); + return BigInt(this.random.nextInt(1, 2_500)); case Difficulty.Medium: - return this.random.nextInt(2_500, 5_000); + return BigInt(this.random.nextInt(2_500, 5_000)); case Difficulty.Hard: - return this.random.nextInt(5_000, 12_500); + return BigInt(this.random.nextInt(5_000, 12_500)); case Difficulty.Impossible: - return this.random.nextInt(12_500, 25_000); + return BigInt(this.random.nextInt(12_500, 25_000)); default: - return 2_500; + return 2_500n; } } - calculateRelationUpdate(goldSent: number): number { + calculateRelationUpdate(goldSent: Gold): number { const chunkSize = this.getGoldChunkSize(); // Calculate how many complete chunks were donated - const chunks = Math.floor(goldSent / chunkSize); + // BigInt division automatically truncates (integer division) + const chunks = Number(goldSent / chunkSize); // Each chunk gives 5 relation points const relationUpdate = chunks * 5; // Cap at 100 relation points diff --git a/src/core/execution/DonateTroopExecution.ts b/src/core/execution/DonateTroopExecution.ts index 29701d7bb5..72696b41ba 100644 --- a/src/core/execution/DonateTroopExecution.ts +++ b/src/core/execution/DonateTroopExecution.ts @@ -84,7 +84,7 @@ export class DonateTroopsExecution implements Execution { recipientMaxTroops / 5, ); default: - return 0; + return recipientMaxTroops / 11; } } From 6a83b350c470a018e5a6e3392acc3ee2f2298c52 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Thu, 27 Nov 2025 22:40:06 +0100 Subject: [PATCH 5/5] Remove comment --- src/core/execution/DonateGoldExecution.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts index d63fe8a013..cc2e1a3491 100644 --- a/src/core/execution/DonateGoldExecution.ts +++ b/src/core/execution/DonateGoldExecution.ts @@ -79,7 +79,6 @@ export class DonateGoldExecution implements Execution { calculateRelationUpdate(goldSent: Gold): number { const chunkSize = this.getGoldChunkSize(); // Calculate how many complete chunks were donated - // BigInt division automatically truncates (integer division) const chunks = Number(goldSent / chunkSize); // Each chunk gives 5 relation points const relationUpdate = chunks * 5;