From 56d59074b4dd0659a676a99847b653e0433482ed Mon Sep 17 00:00:00 2001 From: Marek Rusinowski Date: Thu, 9 Jan 2025 21:48:17 +0100 Subject: [PATCH] Implement conversion from engine to tachyon events --- src/autohost.test.ts | 217 ++++++++++++++++++++++++++++++++++++++++++- src/autohost.ts | 139 +++++++++++++++++++++++++++ 2 files changed, 354 insertions(+), 2 deletions(-) diff --git a/src/autohost.test.ts b/src/autohost.test.ts index 487fca9..cd9b27a 100644 --- a/src/autohost.test.ts +++ b/src/autohost.test.ts @@ -3,10 +3,44 @@ import assert from 'node:assert/strict'; import { randomUUID } from 'node:crypto'; import { once } from 'node:events'; import { GamesManager } from './games.js'; -import { Autohost, _getPlayerIds } from './autohost.js'; +import { Autohost, _getPlayerIds, engineEventToTachyonUpdate } from './autohost.js'; import { fakeRunEngine, EngineRunnerFake } from './engineRunner.fake.js'; -import { AutohostStartRequestData, AutohostStatusEventData } from 'tachyon-protocol/types'; +import { + AutohostStartRequestData, + AutohostStatusEventData, + StartUpdate, + FinishedUpdate, + EngineMessageUpdate, + EngineWarningUpdate, + EngineQuitUpdate, + PlayerJoinedUpdate, + PlayerLeftUpdate, + PlayerChatUpdate, + PlayerDefeatedUpdate, + LuaMsgUpdate, +} from 'tachyon-protocol/types'; import { scriptGameFromStartRequest } from './startScriptGen.js'; +import { + EvServerStarted, + EvServerQuit, + EvServerStartPlaying, + EvServerGameOver, + EvServerMessage, + EvServerWarning, + EvPlayerJoined, + EvPlayerLeft, + EvPlayerReady, + EvPlayerChat, + EvPlayerDefeated, + EvGameLuaMsg, + EvGameTeamStat, + EventType, + ReadyState, + LeaveReason, + ChatDestination, + LuaMsgScript, + LuaMsgUIMode, +} from './engineAutohostInterface.js'; function createStartRequest(players: { name: string; userId: string }[]): AutohostStartRequestData { return { @@ -406,3 +440,182 @@ suite('Autohost', async () => { assert.equal(er.sendPacket.mock.callCount(), 2); }); }); + +suite('engine event to tachyon event translation', async () => { + function toUserId(playerNumber: number): string { + return `id:${playerNumber}`; + } + + test('SERVER_STARTED event', () => { + const ev: EvServerStarted = { + type: EventType.SERVER_STARTED, + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), null); + }); + + test('SERVER_QUIT event', () => { + const ev: EvServerQuit = { + type: EventType.SERVER_QUIT, + }; + const expected: EngineQuitUpdate = { + type: 'engine_quit', + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('SERVER_STARTPLAYING event', () => { + const ev: EvServerStartPlaying = { + type: EventType.SERVER_STARTPLAYING, + gameId: 'asd', + demoPath: 'asd2', + }; + const expected: StartUpdate = { + type: 'start', + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('SERVER_GAMEOVER event', () => { + const ev: EvServerGameOver = { + type: EventType.SERVER_GAMEOVER, + player: 0, + winningAllyTeams: [0], + }; + const expected: FinishedUpdate = { + type: 'finished', + userId: 'id:0', + winningAllyTeams: [0], + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('SERVER_MESSAGE event', () => { + const ev: EvServerMessage = { + type: EventType.SERVER_MESSAGE, + message: 'some message', + }; + const expected: EngineMessageUpdate = { + type: 'engine_message', + message: 'some message', + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('SERVER_WARNING event', () => { + const ev: EvServerWarning = { + type: EventType.SERVER_WARNING, + message: 'warning', + }; + const expected: EngineWarningUpdate = { + type: 'engine_warning', + message: 'warning', + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('PLAYER_JOINED event', () => { + const ev: EvPlayerJoined = { + type: EventType.PLAYER_JOINED, + player: 1, + name: 'john', + }; + const expected: PlayerJoinedUpdate = { + type: 'player_joined', + userId: 'id:1', + playerNumber: 1, + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('PLAYER_LEFT event', () => { + const ev: EvPlayerLeft = { + type: EventType.PLAYER_LEFT, + player: 3, + reason: LeaveReason.KICKED, + }; + const expected: PlayerLeftUpdate = { + type: 'player_left', + userId: 'id:3', + reason: 'kicked', + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('PLAYER_READY event', () => { + const ev: EvPlayerReady = { + type: EventType.PLAYER_READY, + player: 0, + state: ReadyState.NOT_READY, + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), null); + }); + + test('PLAYER_CHAT event', () => { + const ev1: EvPlayerChat = { + type: EventType.PLAYER_CHAT, + message: 'lool', + fromPlayer: 10, + destination: ChatDestination.TO_ALLIES, + }; + const expected1: PlayerChatUpdate = { + type: 'player_chat', + message: 'lool', + userId: 'id:10', + destination: 'allies', + }; + assert.deepEqual(engineEventToTachyonUpdate(ev1, toUserId), expected1); + + const ev2: EvPlayerChat = { + type: EventType.PLAYER_CHAT, + message: 'lool', + fromPlayer: 10, + toPlayer: 11, + destination: ChatDestination.TO_PLAYER, + }; + const expected2: PlayerChatUpdate = { + type: 'player_chat', + message: 'lool', + userId: 'id:10', + toUserId: 'id:11', + destination: 'player', + }; + assert.deepEqual(engineEventToTachyonUpdate(ev2, toUserId), expected2); + }); + + test('PLAYER_DEFEATED event', () => { + const ev: EvPlayerDefeated = { + type: EventType.PLAYER_DEFEATED, + player: 1, + }; + const expected: PlayerDefeatedUpdate = { + type: 'player_defeated', + userId: 'id:1', + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('GAME_LUAMSG event', () => { + const ev: EvGameLuaMsg = { + type: EventType.GAME_LUAMSG, + player: 2, + script: LuaMsgScript.UI, + uiMode: LuaMsgUIMode.ALL, + data: Buffer.from('2983X7RNMQ74'), + }; + const expected: LuaMsgUpdate = { + type: 'luamsg', + userId: 'id:2', + script: 'ui', + uiMode: 'all', + data: Buffer.from('2983X7RNMQ74').toString('base64'), + }; + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), expected); + }); + + test('GAME_TEAMSTAT event', () => { + const ev = { + type: EventType.GAME_TEAMSTAT, + } as EvGameTeamStat; // Yep, putting all in yet as we expect null anyway. + assert.deepEqual(engineEventToTachyonUpdate(ev, toUserId), null); + }); +}); diff --git a/src/autohost.ts b/src/autohost.ts index 8a2298c..82b3948 100644 --- a/src/autohost.ts +++ b/src/autohost.ts @@ -15,11 +15,21 @@ import { AutohostStartOkResponseData, AutohostStartRequestData, AutohostSubscribeUpdatesRequestData, + AutohostUpdateEventData, + PlayerLeftUpdate, + PlayerChatUpdate, + LuaMsgUpdate, } from 'tachyon-protocol/types'; import { serializeMessagePacket, serializeCommandPacket, PacketSerializeError, + type Event, + EventType, + LeaveReason, + ChatDestination, + LuaMsgUIMode, + LuaMsgScript, } from './engineAutohostInterface.js'; import { type GamesManager } from './games.js'; import { MultiIndex } from './multiIndex.js'; @@ -212,3 +222,132 @@ export class Autohost implements TachyonAutohost { return playerId.name; } } + +function toTachyonLeaveReason(reason: LeaveReason): PlayerLeftUpdate['reason'] { + switch (reason) { + case LeaveReason.KICKED: + return 'kicked'; + case LeaveReason.LEFT: + return 'left'; + case LeaveReason.LOST_CONNECTION: + return 'lost_connection'; + } +} + +function toTachyonDestination(destination: ChatDestination): PlayerChatUpdate['destination'] { + switch (destination) { + case ChatDestination.TO_PLAYER: + return 'player'; + case ChatDestination.TO_ALLIES: + return 'allies'; + case ChatDestination.TO_EVERYONE: + return 'all'; + case ChatDestination.TO_SPECTATORS: + return 'spectators'; + } +} + +function toTachyonLuaMsgScript(script: LuaMsgScript): LuaMsgUpdate['script'] { + switch (script) { + case LuaMsgScript.GAIA: + return 'game'; + case LuaMsgScript.RULES: + return 'rules'; + case LuaMsgScript.UI: + return 'ui'; + } +} + +function toTachyonLuaMsgUIMode(uiMode?: LuaMsgUIMode): LuaMsgUpdate['uiMode'] { + switch (uiMode) { + case undefined: + return undefined; + case LuaMsgUIMode.ALL: + return 'all'; + case LuaMsgUIMode.ALLIES: + return 'allies'; + case LuaMsgUIMode.SPECTATORS: + return 'spectators'; + } +} + +/** + * Convert the engine event to tachyon event update data. + * + * @param ev Event + * @param toUserId Function to map from player number in game to userId + * @returns Tachyon update data + */ +export function engineEventToTachyonUpdate( + ev: Event, + toUserId: (playerNumber: number) => string, +): AutohostUpdateEventData['update'] | null { + switch (ev.type) { + case EventType.GAME_LUAMSG: + return { + type: 'luamsg', + userId: toUserId(ev.player), + script: toTachyonLuaMsgScript(ev.script), + uiMode: toTachyonLuaMsgUIMode(ev.uiMode), + data: ev.data.toString('base64'), + }; + case EventType.PLAYER_CHAT: { + const destination = toTachyonDestination(ev.destination); + if (destination === 'player') { + return { + type: 'player_chat', + userId: toUserId(ev.fromPlayer), + destination, + message: ev.message, + toUserId: toUserId(ev.toPlayer!), + }; + } else { + return { + type: 'player_chat', + userId: toUserId(ev.fromPlayer), + destination, + message: ev.message, + }; + } + } + case EventType.PLAYER_DEFEATED: + return { + type: 'player_defeated', + userId: toUserId(ev.player), + }; + case EventType.PLAYER_JOINED: { + return { + type: 'player_joined', + userId: toUserId(ev.player), + playerNumber: ev.player, + }; + } + case EventType.PLAYER_LEFT: + return { + type: 'player_left', + userId: toUserId(ev.player), + reason: toTachyonLeaveReason(ev.reason), + }; + case EventType.SERVER_GAMEOVER: + if (ev.winningAllyTeams.length < 1) { + throw new Error('winning ally teams must be at least 1'); + } + return { + type: 'finished', + userId: toUserId(ev.player), + winningAllyTeams: ev.winningAllyTeams as [number, ...number[]], + }; + case EventType.SERVER_MESSAGE: + return { type: 'engine_message', message: ev.message }; + case EventType.SERVER_STARTPLAYING: + return { type: 'start' }; + case EventType.SERVER_WARNING: + return { type: 'engine_warning', message: ev.message }; + case EventType.SERVER_QUIT: + return { type: 'engine_quit' }; + case EventType.SERVER_STARTED: // The return of start call indicates that server started, Tachyon doens't have this message. + case EventType.GAME_TEAMSTAT: // At the moment Tachyon lacks definition of this message. + case EventType.PLAYER_READY: // In my testing, it didn't behave as expected. Tachyon lacks definition for this message. + return null; + } +}