Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,8 @@
"admin": "Kicked by an admin",
"duplicate_session": "Kicked from game (you may have been playing on another tab)",
"host_left": "The host has left the lobby.",
"lobby_creator": "Kicked by lobby creator"
"lobby_creator": "Kicked by lobby creator",
"too_much_data": "Kicked for sending too much data"
},
"lang": {
"en": "English",
Expand Down
13 changes: 8 additions & 5 deletions src/client/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { TileRef } from "../core/game/GameMap";
import {
AllPlayersStats,
batchMoveWarshipUnitIds,
ClientHashMessage,
ClientIntentMessage,
ClientJoinMessage,
Expand Down Expand Up @@ -649,11 +650,13 @@ export class Transport {
}

private onMoveWarshipEvent(event: MoveWarshipIntentEvent) {
this.sendIntent({
type: "move_warship",
unitIds: event.unitIds,
tile: event.tile,
});
for (const unitIds of batchMoveWarshipUnitIds(event.unitIds, event.tile)) {
Comment thread
bighurdan-cell marked this conversation as resolved.
this.sendIntent({
type: "move_warship",
unitIds,
tile: event.tile,
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private onSendDeleteUnitIntent(event: SendDeleteUnitIntentEvent) {
Expand Down
27 changes: 26 additions & 1 deletion src/core/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,10 +534,35 @@ export const CancelBoatIntentSchema = z.object({

export const MoveWarshipIntentSchema = z.object({
type: z.literal("move_warship"),
unitIds: z.array(z.number().int()).nonempty(),
unitIds: z.array(z.number().int()).nonempty().max(1000),
tile: z.number(),
});

// Client messages larger than this get the client kicked by ClientMsgRateLimiter.
export const MAX_INTENT_SIZE = 2000;

export function batchMoveWarshipUnitIds(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this funciton shouldn't be in Schemas.ts

unitIds: readonly number[],
tile: number,
): number[][] {
const overhead = JSON.stringify({
type: "intent",
intent: { type: "move_warship", unitIds: [], tile },
} satisfies ClientIntentMessage).length;
const batches: number[][] = [];
let size = overhead;
for (const unitId of unitIds) {
const unitIdSize = String(unitId).length + 1;
if (batches.length === 0 || size + unitIdSize > MAX_INTENT_SIZE) {
batches.push([]);
size = overhead;
}
batches[batches.length - 1].push(unitId);
size += unitIdSize;
}
return batches;
}

export const DeleteUnitIntentSchema = z.object({
type: z.literal("delete_unit"),
unitId: z.number(),
Expand Down
3 changes: 1 addition & 2 deletions src/server/ClientMsgRateLimiter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { RateLimiter } from "limiter";
import { ClientID } from "../core/Schemas";
import { ClientID, MAX_INTENT_SIZE } from "../core/Schemas";

const INTENTS_PER_SECOND = 10;
const INTENTS_PER_MINUTE = 150;
const MAX_INTENT_SIZE = 2000;
const TOTAL_BYTES = 5 * 1024 * 1024; // 5MB per client
export type RateLimitResult = "ok" | "limit" | "kick";

Expand Down
56 changes: 56 additions & 0 deletions tests/MoveWarshipIntentBatching.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
batchMoveWarshipUnitIds,
ClientIntentMessage,
ClientMessageSchema,
MAX_INTENT_SIZE,
} from "../src/core/Schemas";
import { replacer } from "../src/core/Util";

const TILE = 250_000;

function frame(unitIds: number[], tile: number): string {
return JSON.stringify(
{
type: "intent",
intent: { type: "move_warship", unitIds, tile },
} satisfies ClientIntentMessage,
replacer,
);
}

describe("batchMoveWarshipUnitIds", () => {
test("sends a small fleet as a single intent", () => {
const unitIds = [10000, 10003, 10006];
expect(batchMoveWarshipUnitIds(unitIds, TILE)).toEqual([unitIds]);
});

test("returns no batches for an empty selection", () => {
expect(batchMoveWarshipUnitIds([], TILE)).toEqual([]);
});

test.each([1, 400, 4000])(
"keeps every batch under the server cap (%i warships)",
(count) => {
const unitIds = Array.from({ length: count }, (_, i) => 900_000 + i * 3);
const batches = batchMoveWarshipUnitIds(unitIds, TILE);

for (const batch of batches) {
expect(batch.length).toBeGreaterThan(0);
expect(
Buffer.byteLength(frame(batch, TILE), "utf8"),
).toBeLessThanOrEqual(MAX_INTENT_SIZE);
}
expect(batches.flat()).toEqual(unitIds);
},
);

test("every batch is a valid client intent message", () => {
const unitIds = Array.from({ length: 450 }, (_, i) => 10_000 + i * 3);

for (const batch of batchMoveWarshipUnitIds(unitIds, TILE)) {
expect(
ClientMessageSchema.safeParse(JSON.parse(frame(batch, TILE))).success,
).toBe(true);
}
});
});
Loading