Files
OpenFrontIO/tests/core/BinaryCodec.test.ts

444 lines
9.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
binaryContextFromGameStartInfo,
decodeBinaryClientGameplayMessage,
decodeBinaryServerGameplayMessage,
encodeBinaryClientGameplayMessage,
encodeBinaryServerGameplayMessage,
isBinaryGameplayClientMessage,
} 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,
},
},
{
type: "intent",
intent: {
type: "kick_player",
target: "P0000002",
},
},
];
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("only classifies supported intents as binary gameplay messages", () => {
expect(
isBinaryGameplayClientMessage({
type: "intent",
intent: {
type: "kick_player",
target: "P0000002",
},
} as ClientIntentMessage),
).toBe(true);
expect(
isBinaryGameplayClientMessage({
type: "intent",
intent: {
type: "update_game_config",
config: {},
},
} as ClientIntentMessage),
).toBe(false);
});
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/,
);
});
});