import { describe, expect, it } from "vitest"; import { binaryContextFromGameStartInfo, decodeBinaryClientGameplayMessage, decodeBinaryServerGameplayMessage, encodeBinaryClientGameplayMessage, encodeBinaryServerGameplayMessage, } from "../../src/core/BinaryCodec"; import { BINARY_PROTOCOL_VERSION } from "../../src/core/BinaryProtocol"; import { ClientHashMessage, ClientIntentMessage, ClientPingMessage, QuickChatKeySchema, ServerDesyncMessage, ServerTurnMessage, } from "../../src/core/Schemas"; import { AllPlayers, UnitType } from "../../src/core/game/Game"; const quickChatKey = QuickChatKeySchema.options[0]; const context = binaryContextFromGameStartInfo({ players: [ { clientID: "P0000001" }, { clientID: "P0000002" }, { clientID: "P0000003" }, ], } as any); const clientIntentMessages: ClientIntentMessage[] = [ { type: "intent", intent: { type: "attack", targetID: "P0000002", troops: 12.5, }, }, { type: "intent", intent: { type: "attack", targetID: "kli0dx59", troops: 18, }, }, { type: "intent", intent: { type: "attack", targetID: null, troops: null, }, }, { type: "intent", intent: { type: "cancel_attack", attackID: "attack-123", }, }, { type: "intent", intent: { type: "spawn", tile: 42, }, }, { type: "intent", intent: { type: "mark_disconnected", clientID: "P0000002", isDisconnected: true, }, }, { type: "intent", intent: { type: "boat", troops: 99, dst: 123, }, }, { type: "intent", intent: { type: "cancel_boat", unitID: 77, }, }, { type: "intent", intent: { type: "allianceRequest", recipient: "P0000002", }, }, { type: "intent", intent: { type: "allianceReject", requestor: "P0000002", }, }, { type: "intent", intent: { type: "breakAlliance", recipient: "P0000002", }, }, { type: "intent", intent: { type: "targetPlayer", target: "P0000002", }, }, { type: "intent", intent: { type: "emoji", recipient: "P0000002", emoji: 3, }, }, { type: "intent", intent: { type: "emoji", recipient: AllPlayers, emoji: 4, }, }, { type: "intent", intent: { type: "donate_gold", recipient: "P0000002", gold: 250, }, }, { type: "intent", intent: { type: "donate_gold", recipient: "P0000002", gold: null, }, }, { type: "intent", intent: { type: "donate_troops", recipient: "P0000002", troops: 25, }, }, { type: "intent", intent: { type: "donate_troops", recipient: "P0000002", troops: null, }, }, { type: "intent", intent: { type: "build_unit", unit: UnitType.Warship, tile: 7, rocketDirectionUp: true, }, }, { type: "intent", intent: { type: "build_unit", unit: UnitType.Port, tile: 8, }, }, { type: "intent", intent: { type: "upgrade_structure", unit: UnitType.City, unitId: 9, }, }, { type: "intent", intent: { type: "embargo", targetID: "P0000002", action: "start", }, }, { type: "intent", intent: { type: "embargo_all", action: "stop", }, }, { type: "intent", intent: { type: "move_warship", unitId: 55, tile: 88, }, }, { type: "intent", intent: { type: "quick_chat", recipient: "P0000002", quickChatKey, target: "P0000003", }, }, { type: "intent", intent: { type: "quick_chat", recipient: "P0000002", quickChatKey, }, }, { type: "intent", intent: { type: "allianceExtension", recipient: "P0000002", }, }, { type: "intent", intent: { type: "delete_unit", unitId: 101, }, }, { type: "intent", intent: { type: "toggle_pause", paused: true, }, }, ]; describe("BinaryCodec", () => { it.each(clientIntentMessages)( "round-trips client gameplay intent %#", (message) => { const encoded = encodeBinaryClientGameplayMessage(message, context); const decoded = decodeBinaryClientGameplayMessage(encoded, context); expect(decoded).toEqual(message); }, ); it("round-trips hash messages", () => { const message: ClientHashMessage = { type: "hash", turnNumber: 12, hash: 34567, }; const encoded = encodeBinaryClientGameplayMessage(message, context); const decoded = decodeBinaryClientGameplayMessage(encoded, context); expect(decoded).toEqual(message); }); it("round-trips ping messages", () => { const message: ClientPingMessage = { type: "ping", }; const encoded = encodeBinaryClientGameplayMessage(message, context); const decoded = decodeBinaryClientGameplayMessage(encoded, context); expect(decoded).toEqual(message); }); it("round-trips server turn messages", () => { const message: ServerTurnMessage = { type: "turn", turn: { turnNumber: 5, intents: [ { type: "spawn", tile: 10, clientID: "P0000001", }, { type: "emoji", recipient: AllPlayers, emoji: 2, clientID: "P0000002", }, ], }, }; const encoded = encodeBinaryServerGameplayMessage(message, context); const decoded = decodeBinaryServerGameplayMessage(encoded, context); expect(decoded).toEqual(message); }); it("round-trips server turn messages with non-lobby target ids", () => { const message: ServerTurnMessage = { type: "turn", turn: { turnNumber: 6, intents: [ { type: "attack", targetID: "kli0dx59", troops: 9, clientID: "P0000001", }, ], }, }; const encoded = encodeBinaryServerGameplayMessage(message, context); const decoded = decodeBinaryServerGameplayMessage(encoded, context); expect(decoded).toEqual(message); }); it("round-trips server desync messages", () => { const message: ServerDesyncMessage = { type: "desync", turn: 9, correctHash: 777, clientsWithCorrectHash: 3, totalActiveClients: 4, }; const encoded = encodeBinaryServerGameplayMessage(message, context); const decoded = decodeBinaryServerGameplayMessage(encoded, context); expect(decoded).toEqual(message); }); it("rejects unknown protocol versions", () => { const encoded = encodeBinaryClientGameplayMessage( { type: "ping", }, context, ); encoded[0] = BINARY_PROTOCOL_VERSION + 1; expect(() => decodeBinaryClientGameplayMessage(encoded, context)).toThrow( /Unsupported binary protocol version/, ); }); it("rejects invalid player indexes", () => { const encoded = encodeBinaryServerGameplayMessage( { type: "turn", turn: { turnNumber: 1, intents: [ { type: "spawn", tile: 3, clientID: "P0000001", }, ], }, }, context, ); encoded[10] = 99; expect(() => decodeBinaryServerGameplayMessage(encoded, context)).toThrow( /Invalid player index/, ); }); it("rejects invalid intent flags", () => { const encoded = encodeBinaryClientGameplayMessage( { type: "intent", intent: { type: "spawn", tile: 1, }, }, context, ); encoded[5] = 0x04; expect(() => decodeBinaryClientGameplayMessage(encoded, context)).toThrow( /Unsupported flags/, ); }); it("rejects truncated frames", () => { const encoded = encodeBinaryServerGameplayMessage( { type: "desync", turn: 4, correctHash: null, clientsWithCorrectHash: 1, totalActiveClients: 2, }, context, ); const truncated = encoded.subarray(0, encoded.length - 1); expect(() => decodeBinaryServerGameplayMessage(truncated, context)).toThrow( /Unexpected end of binary frame/, ); }); });