fix(protocol): support kick intents in binary gameplay

This commit is contained in:
scamiv
2026-03-25 19:00:46 +01:00
parent 69db6c1ac7
commit 18ed283243
4 changed files with 106 additions and 2 deletions
+10 -1
View File
@@ -9,6 +9,7 @@ import {
INTENT_FLAG_OPTION_A,
INTENT_FLAG_OPTION_B,
createBinaryProtocolContext,
hasBinaryIntentOpcode,
intentTypeToOpcode,
opcodeToIntentType,
opcodeToUnitType,
@@ -554,6 +555,8 @@ function encodeIntent(
writer.writeBoolean(intent.paused);
return;
case "kick_player":
writeRequiredPlayerRef(writer, intent.target, context);
return;
case "update_game_config":
throw new Error(`Unsupported binary intent type: ${intent.type}`);
}
@@ -750,6 +753,12 @@ function decodeIntent(
type: "toggle_pause",
paused: reader.readBoolean(),
};
case "kick_player":
assertIntentFlags(intentType, flags, 0);
return {
type: "kick_player",
target: readRequiredPlayerRef(reader, context),
};
}
throw new Error(`Unhandled binary intent type: ${intentType}`);
@@ -824,7 +833,7 @@ export function isBinaryGameplayClientMessage(
message: ClientMessage,
): message is BinaryClientGameplayMessage {
return (
message.type === "intent" ||
(message.type === "intent" && hasBinaryIntentOpcode(message.intent.type)) ||
message.type === "hash" ||
message.type === "ping"
);
+7 -1
View File
@@ -49,6 +49,7 @@ export enum BinaryIntentType {
AllianceExtension = 20,
DeleteUnit = 21,
TogglePause = 22,
KickPlayer = 23,
}
export const INTENT_FLAG_OPTION_A = 1 << 0;
@@ -88,7 +89,7 @@ const INTENT_TYPE_TO_OPCODE: Record<
quick_chat: BinaryIntentType.QuickChat,
allianceExtension: BinaryIntentType.AllianceExtension,
delete_unit: BinaryIntentType.DeleteUnit,
kick_player: undefined,
kick_player: BinaryIntentType.KickPlayer,
toggle_pause: BinaryIntentType.TogglePause,
update_game_config: undefined,
};
@@ -116,6 +117,7 @@ const OPCODE_TO_INTENT_TYPE: Record<BinaryIntentType, Intent["type"]> = {
[BinaryIntentType.AllianceExtension]: "allianceExtension",
[BinaryIntentType.DeleteUnit]: "delete_unit",
[BinaryIntentType.TogglePause]: "toggle_pause",
[BinaryIntentType.KickPlayer]: "kick_player",
};
const UNIT_TYPE_TO_OPCODE = new Map<UnitType, number>(
@@ -155,6 +157,10 @@ export function intentTypeToOpcode(
return opcode;
}
export function hasBinaryIntentOpcode(intentType: Intent["type"]): boolean {
return INTENT_TYPE_TO_OPCODE[intentType] !== undefined;
}
export function opcodeToIntentType(opcode: number): Intent["type"] {
const intentType = OPCODE_TO_INTENT_TYPE[opcode as BinaryIntentType];
if (intentType === undefined) {
+30
View File
@@ -5,6 +5,7 @@ import {
decodeBinaryServerGameplayMessage,
encodeBinaryClientGameplayMessage,
encodeBinaryServerGameplayMessage,
isBinaryGameplayClientMessage,
} from "../../src/core/BinaryCodec";
import { BINARY_PROTOCOL_VERSION } from "../../src/core/BinaryProtocol";
import {
@@ -251,6 +252,13 @@ const clientIntentMessages: ClientIntentMessage[] = [
paused: true,
},
},
{
type: "intent",
intent: {
type: "kick_player",
target: "P0000002",
},
},
];
describe("BinaryCodec", () => {
@@ -283,6 +291,28 @@ describe("BinaryCodec", () => {
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",
@@ -237,6 +237,65 @@ describe("GameServer binary gameplay protocol", () => {
);
});
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(