mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
369 lines
9.0 KiB
TypeScript
369 lines
9.0 KiB
TypeScript
import { EventEmitter } from "events";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../../src/server/Archive", () => ({
|
|
archive: vi.fn(),
|
|
finalizeGameRecord: vi.fn((record) => record),
|
|
}));
|
|
|
|
import {
|
|
binaryContextFromGameStartInfo,
|
|
decodeBinaryServerGameplayMessage,
|
|
encodeBinaryClientGameplayMessage,
|
|
} from "../../src/core/BinaryCodec";
|
|
import {
|
|
Difficulty,
|
|
GameMapSize,
|
|
GameMapType,
|
|
GameMode,
|
|
GameType,
|
|
} from "../../src/core/game/Game";
|
|
import type { GameConfig } from "../../src/core/Schemas";
|
|
import { Client } from "../../src/server/Client";
|
|
import { GameServer } from "../../src/server/GameServer";
|
|
|
|
class MockWebSocket extends EventEmitter {
|
|
public readonly sent: Array<string | Uint8Array> = [];
|
|
public readyState = 1;
|
|
|
|
send(message: string | Uint8Array) {
|
|
this.sent.push(message);
|
|
}
|
|
|
|
close(_code?: number, _reason?: string) {
|
|
this.readyState = 3;
|
|
this.emit("close");
|
|
}
|
|
}
|
|
|
|
function createMockLogger() {
|
|
return {
|
|
child: vi.fn().mockReturnThis(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function createGameConfig(): GameConfig {
|
|
return {
|
|
donateGold: false,
|
|
donateTroops: false,
|
|
gameMap: GameMapType.World,
|
|
gameType: GameType.Private,
|
|
gameMapSize: GameMapSize.Normal,
|
|
difficulty: Difficulty.Easy,
|
|
nations: "default",
|
|
infiniteGold: false,
|
|
infiniteTroops: false,
|
|
instantBuild: false,
|
|
randomSpawn: false,
|
|
gameMode: GameMode.FFA,
|
|
bots: 0,
|
|
disabledUnits: [],
|
|
};
|
|
}
|
|
|
|
function createClient(
|
|
clientID: string,
|
|
persistentID: string,
|
|
username: string,
|
|
) {
|
|
const ws = new MockWebSocket();
|
|
const client = new Client(
|
|
clientID as any,
|
|
persistentID,
|
|
null,
|
|
undefined,
|
|
undefined,
|
|
"127.0.0.1",
|
|
username,
|
|
null,
|
|
ws as any,
|
|
undefined,
|
|
);
|
|
return { client, ws };
|
|
}
|
|
|
|
describe("GameServer binary gameplay protocol", () => {
|
|
it("keeps start JSON and emits live turns as binary", async () => {
|
|
const logger = createMockLogger();
|
|
const game = new GameServer(
|
|
"TEST0001",
|
|
logger as any,
|
|
Date.now(),
|
|
{
|
|
turnIntervalMs: () => 100,
|
|
env: () => 0,
|
|
} as any,
|
|
createGameConfig(),
|
|
);
|
|
|
|
const clientA = createClient(
|
|
"P0000001",
|
|
"11111111-1111-4111-8111-111111111111",
|
|
"Alice",
|
|
);
|
|
const clientB = createClient(
|
|
"P0000002",
|
|
"22222222-2222-4222-8222-222222222222",
|
|
"Bob",
|
|
);
|
|
|
|
expect(game.joinClient(clientA.client)).toBe("joined");
|
|
expect(game.joinClient(clientB.client)).toBe("joined");
|
|
|
|
game.start();
|
|
|
|
const startPayload = clientA.ws.sent.find(
|
|
(message): message is string =>
|
|
typeof message === "string" && message.includes('"type":"start"'),
|
|
);
|
|
expect(startPayload).toBeDefined();
|
|
|
|
const binaryContext = binaryContextFromGameStartInfo(
|
|
JSON.parse(startPayload!).gameStartInfo,
|
|
);
|
|
const spawnMessage = encodeBinaryClientGameplayMessage(
|
|
{
|
|
type: "intent",
|
|
intent: {
|
|
type: "spawn",
|
|
tile: 123,
|
|
},
|
|
},
|
|
binaryContext,
|
|
);
|
|
|
|
clientA.ws.emit("message", spawnMessage, true);
|
|
(game as any).endTurn();
|
|
|
|
const binaryTurn = clientA.ws.sent.find(
|
|
(message): message is Uint8Array =>
|
|
message instanceof Uint8Array && message[1] === 2,
|
|
);
|
|
expect(binaryTurn).toBeDefined();
|
|
|
|
const decodedTurn = decodeBinaryServerGameplayMessage(
|
|
binaryTurn!,
|
|
binaryContext,
|
|
);
|
|
expect(decodedTurn.type).toBe("turn");
|
|
if (decodedTurn.type !== "turn") {
|
|
throw new Error("Expected binary turn message");
|
|
}
|
|
expect(decodedTurn.turn.intents).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
type: "spawn",
|
|
tile: 123,
|
|
clientID: "P0000001",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("round-trips binary attacks that target bot-style ids", () => {
|
|
const logger = createMockLogger();
|
|
const game = new GameServer(
|
|
"TEST0003",
|
|
logger as any,
|
|
Date.now(),
|
|
{
|
|
turnIntervalMs: () => 100,
|
|
env: () => 0,
|
|
} as any,
|
|
createGameConfig(),
|
|
);
|
|
|
|
const clientA = createClient(
|
|
"P0000001",
|
|
"44444444-4444-4444-8444-444444444444",
|
|
"Alice",
|
|
);
|
|
|
|
expect(game.joinClient(clientA.client)).toBe("joined");
|
|
game.start();
|
|
|
|
const startPayload = clientA.ws.sent.find(
|
|
(message): message is string =>
|
|
typeof message === "string" && message.includes('"type":"start"'),
|
|
);
|
|
expect(startPayload).toBeDefined();
|
|
|
|
const binaryContext = binaryContextFromGameStartInfo(
|
|
JSON.parse(startPayload!).gameStartInfo,
|
|
);
|
|
const attackMessage = encodeBinaryClientGameplayMessage(
|
|
{
|
|
type: "intent",
|
|
intent: {
|
|
type: "attack",
|
|
targetID: "kli0dx59",
|
|
troops: 25,
|
|
},
|
|
},
|
|
binaryContext,
|
|
);
|
|
|
|
clientA.ws.emit("message", attackMessage, true);
|
|
(game as any).endTurn();
|
|
|
|
const binaryTurn = [...clientA.ws.sent]
|
|
.reverse()
|
|
.find(
|
|
(message): message is Uint8Array =>
|
|
message instanceof Uint8Array && message[1] === 2,
|
|
);
|
|
expect(binaryTurn).toBeDefined();
|
|
|
|
const decodedTurn = decodeBinaryServerGameplayMessage(
|
|
binaryTurn!,
|
|
binaryContext,
|
|
);
|
|
expect(decodedTurn.type).toBe("turn");
|
|
if (decodedTurn.type !== "turn") {
|
|
throw new Error("Expected binary turn message");
|
|
}
|
|
expect(decodedTurn.turn.intents).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
type: "attack",
|
|
targetID: "kli0dx59",
|
|
troops: 25,
|
|
clientID: "P0000001",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("supports host kicks via binary gameplay intents after start", () => {
|
|
const logger = createMockLogger();
|
|
const hostPersistentId = "66666666-6666-4666-8666-666666666666";
|
|
const game = new GameServer(
|
|
"TEST0005",
|
|
logger as any,
|
|
Date.now(),
|
|
{
|
|
turnIntervalMs: () => 100,
|
|
env: () => 0,
|
|
} as any,
|
|
createGameConfig(),
|
|
hostPersistentId,
|
|
);
|
|
|
|
const host = createClient("P0000001", hostPersistentId, "Host");
|
|
const target = createClient(
|
|
"P0000002",
|
|
"77777777-7777-4777-8777-777777777777",
|
|
"Target",
|
|
);
|
|
|
|
expect(game.joinClient(host.client)).toBe("joined");
|
|
expect(game.joinClient(target.client)).toBe("joined");
|
|
game.start();
|
|
|
|
const startPayload = host.ws.sent.find(
|
|
(message): message is string =>
|
|
typeof message === "string" && message.includes('"type":"start"'),
|
|
);
|
|
expect(startPayload).toBeDefined();
|
|
|
|
const binaryContext = binaryContextFromGameStartInfo(
|
|
JSON.parse(startPayload!).gameStartInfo,
|
|
);
|
|
const kickMessage = encodeBinaryClientGameplayMessage(
|
|
{
|
|
type: "intent",
|
|
intent: {
|
|
type: "kick_player",
|
|
target: "P0000002",
|
|
},
|
|
},
|
|
binaryContext,
|
|
);
|
|
|
|
host.ws.emit("message", kickMessage, true);
|
|
|
|
expect(target.ws.sent).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringContaining('"error":"kick_reason.lobby_creator"'),
|
|
]),
|
|
);
|
|
expect(target.ws.readyState).toBe(3);
|
|
expect(game.activeClients.map((client) => client.clientID)).toEqual([
|
|
"P0000001",
|
|
]);
|
|
});
|
|
|
|
it("kicks malformed binary gameplay messages after start", () => {
|
|
const logger = createMockLogger();
|
|
const game = new GameServer(
|
|
"TEST0004",
|
|
logger as any,
|
|
Date.now(),
|
|
{
|
|
turnIntervalMs: () => 100,
|
|
env: () => 0,
|
|
} as any,
|
|
createGameConfig(),
|
|
);
|
|
|
|
const clientA = createClient(
|
|
"P0000001",
|
|
"55555555-5555-4555-8555-555555555555",
|
|
"Alice",
|
|
);
|
|
|
|
expect(game.joinClient(clientA.client)).toBe("joined");
|
|
game.start();
|
|
|
|
clientA.ws.sent.length = 0;
|
|
clientA.ws.emit("message", new Uint8Array([99, 1, 0, 0]), true);
|
|
|
|
expect(clientA.ws.sent).toEqual([
|
|
expect.stringContaining('"error":"kick_reason.invalid_message"'),
|
|
]);
|
|
expect(clientA.ws.readyState).toBe(3);
|
|
expect(game.activeClients).toHaveLength(0);
|
|
});
|
|
|
|
it("accepts JSON rejoin after start and responds with JSON start", () => {
|
|
const logger = createMockLogger();
|
|
const game = new GameServer(
|
|
"TEST0002",
|
|
logger as any,
|
|
Date.now(),
|
|
{
|
|
turnIntervalMs: () => 100,
|
|
env: () => 0,
|
|
} as any,
|
|
createGameConfig(),
|
|
);
|
|
|
|
const clientA = createClient(
|
|
"P0000001",
|
|
"33333333-3333-4333-8333-333333333333",
|
|
"Alice",
|
|
);
|
|
expect(game.joinClient(clientA.client)).toBe("joined");
|
|
game.start();
|
|
|
|
clientA.ws.sent.length = 0;
|
|
clientA.ws.emit(
|
|
"message",
|
|
JSON.stringify({
|
|
type: "rejoin",
|
|
gameID: "TEST0002",
|
|
lastTurn: 0,
|
|
token: "33333333-3333-4333-8333-333333333333",
|
|
}),
|
|
false,
|
|
);
|
|
|
|
expect(clientA.ws.sent).toHaveLength(1);
|
|
expect(typeof clientA.ws.sent[0]).toBe("string");
|
|
expect(clientA.ws.sent[0]).toContain('"type":"start"');
|
|
});
|
|
});
|