// Typed wrappers around `GalaxyClient.executeCommand` for the Phase 8 // lobby command catalog. Each wrapper builds a FlatBuffers request // payload via the generated TS bindings, calls `executeCommand`, then // decodes the FlatBuffers response payload. Errors carrying a non-`ok` // `result_code` are surfaced as a thrown `LobbyError` so callers can // branch on canonical lobby error codes (`invalid_request`, // `subject_not_found`, `forbidden`, `conflict`, `internal_error`). import { Builder, ByteBuffer } from "flatbuffers"; import type { GalaxyClient } from "./galaxy-client"; import { ApplicationSubmitRequest, ApplicationSubmitResponse, ApplicationSummary as FbsApplicationSummary, ErrorResponse as FbsErrorResponse, GameCreateRequest, GameCreateResponse, GameSummary as FbsGameSummary, InviteDeclineRequest, InviteDeclineResponse, InviteRedeemRequest, InviteRedeemResponse, InviteSummary as FbsInviteSummary, MyApplicationsListRequest, MyApplicationsListResponse, MyGamesListRequest, MyGamesListResponse, MyInvitesListRequest, MyInvitesListResponse, PublicGamesListRequest, PublicGamesListResponse, } from "../proto/galaxy/fbs/lobby"; export class LobbyError extends Error { readonly code: string; readonly resultCode: string; constructor(resultCode: string, code: string, message: string) { super(message); this.name = "LobbyError"; this.resultCode = resultCode; this.code = code; } } export interface GameSummary { gameId: string; gameName: string; gameType: string; status: string; ownerUserId: string; minPlayers: number; maxPlayers: number; enrollmentEndsAt: Date; createdAt: Date; updatedAt: Date; } export interface PublicGamesPage { items: GameSummary[]; page: number; pageSize: number; total: number; } export interface ApplicationSummary { applicationId: string; gameId: string; applicantUserId: string; raceName: string; status: string; createdAt: Date; decidedAt: Date | null; } export interface InviteSummary { inviteId: string; gameId: string; inviterUserId: string; invitedUserId: string; code: string; raceName: string; status: string; createdAt: Date; expiresAt: Date; decidedAt: Date | null; } export interface CreateGameInput { gameName: string; description: string; minPlayers: number; maxPlayers: number; startGapHours: number; startGapPlayers: number; enrollmentEndsAt: Date; turnSchedule: string; targetEngineVersion: string; } const RESULT_CODE_OK = "ok"; export async function listMyGames(client: GalaxyClient): Promise { const builder = new Builder(32); MyGamesListRequest.startMyGamesListRequest(builder); builder.finish(MyGamesListRequest.endMyGamesListRequest(builder)); const payload = await execute(client, "lobby.my.games.list", builder.asUint8Array()); const response = MyGamesListResponse.getRootAsMyGamesListResponse(new ByteBuffer(payload)); const out: GameSummary[] = []; for (let i = 0; i < response.itemsLength(); i++) { const item = response.items(i); if (item) { out.push(decodeGameSummary(item)); } } return out; } export async function listPublicGames( client: GalaxyClient, options: { page?: number; pageSize?: number } = {}, ): Promise { const page = options.page ?? 1; const pageSize = options.pageSize ?? 50; const builder = new Builder(32); PublicGamesListRequest.startPublicGamesListRequest(builder); PublicGamesListRequest.addPage(builder, page); PublicGamesListRequest.addPageSize(builder, pageSize); builder.finish(PublicGamesListRequest.endPublicGamesListRequest(builder)); const payload = await execute(client, "lobby.public.games.list", builder.asUint8Array()); const response = PublicGamesListResponse.getRootAsPublicGamesListResponse( new ByteBuffer(payload), ); const out: GameSummary[] = []; for (let i = 0; i < response.itemsLength(); i++) { const item = response.items(i); if (item) { out.push(decodeGameSummary(item)); } } return { items: out, page: response.page(), pageSize: response.pageSize(), total: response.total(), }; } export async function listMyApplications( client: GalaxyClient, ): Promise { const builder = new Builder(32); MyApplicationsListRequest.startMyApplicationsListRequest(builder); builder.finish(MyApplicationsListRequest.endMyApplicationsListRequest(builder)); const payload = await execute(client, "lobby.my.applications.list", builder.asUint8Array()); const response = MyApplicationsListResponse.getRootAsMyApplicationsListResponse( new ByteBuffer(payload), ); const out: ApplicationSummary[] = []; for (let i = 0; i < response.itemsLength(); i++) { const item = response.items(i); if (item) { out.push(decodeApplicationSummary(item)); } } return out; } export async function listMyInvites(client: GalaxyClient): Promise { const builder = new Builder(32); MyInvitesListRequest.startMyInvitesListRequest(builder); builder.finish(MyInvitesListRequest.endMyInvitesListRequest(builder)); const payload = await execute(client, "lobby.my.invites.list", builder.asUint8Array()); const response = MyInvitesListResponse.getRootAsMyInvitesListResponse(new ByteBuffer(payload)); const out: InviteSummary[] = []; for (let i = 0; i < response.itemsLength(); i++) { const item = response.items(i); if (item) { out.push(decodeInviteSummary(item)); } } return out; } export async function createGame( client: GalaxyClient, input: CreateGameInput, ): Promise { const builder = new Builder(256); const gameNameOff = builder.createString(input.gameName); const descriptionOff = builder.createString(input.description); const turnScheduleOff = builder.createString(input.turnSchedule); const targetEngineVersionOff = builder.createString(input.targetEngineVersion); GameCreateRequest.startGameCreateRequest(builder); GameCreateRequest.addGameName(builder, gameNameOff); GameCreateRequest.addDescription(builder, descriptionOff); GameCreateRequest.addMinPlayers(builder, input.minPlayers); GameCreateRequest.addMaxPlayers(builder, input.maxPlayers); GameCreateRequest.addStartGapHours(builder, input.startGapHours); GameCreateRequest.addStartGapPlayers(builder, input.startGapPlayers); GameCreateRequest.addEnrollmentEndsAtMs(builder, BigInt(input.enrollmentEndsAt.getTime())); GameCreateRequest.addTurnSchedule(builder, turnScheduleOff); GameCreateRequest.addTargetEngineVersion(builder, targetEngineVersionOff); builder.finish(GameCreateRequest.endGameCreateRequest(builder)); const payload = await execute(client, "lobby.game.create", builder.asUint8Array()); const response = GameCreateResponse.getRootAsGameCreateResponse(new ByteBuffer(payload)); const game = response.game(); if (game === null) { throw new LobbyError("internal_error", "internal_error", "game missing in response"); } return decodeGameSummary(game); } export async function submitApplication( client: GalaxyClient, gameId: string, raceName: string, ): Promise { const builder = new Builder(128); const gameIdOff = builder.createString(gameId); const raceNameOff = builder.createString(raceName); ApplicationSubmitRequest.startApplicationSubmitRequest(builder); ApplicationSubmitRequest.addGameId(builder, gameIdOff); ApplicationSubmitRequest.addRaceName(builder, raceNameOff); builder.finish(ApplicationSubmitRequest.endApplicationSubmitRequest(builder)); const payload = await execute(client, "lobby.application.submit", builder.asUint8Array()); const response = ApplicationSubmitResponse.getRootAsApplicationSubmitResponse( new ByteBuffer(payload), ); const application = response.application(); if (application === null) { throw new LobbyError("internal_error", "internal_error", "application missing in response"); } return decodeApplicationSummary(application); } export async function redeemInvite( client: GalaxyClient, gameId: string, inviteId: string, ): Promise { const builder = new Builder(128); const gameIdOff = builder.createString(gameId); const inviteIdOff = builder.createString(inviteId); InviteRedeemRequest.startInviteRedeemRequest(builder); InviteRedeemRequest.addGameId(builder, gameIdOff); InviteRedeemRequest.addInviteId(builder, inviteIdOff); builder.finish(InviteRedeemRequest.endInviteRedeemRequest(builder)); const payload = await execute(client, "lobby.invite.redeem", builder.asUint8Array()); const response = InviteRedeemResponse.getRootAsInviteRedeemResponse(new ByteBuffer(payload)); const invite = response.invite(); if (invite === null) { throw new LobbyError("internal_error", "internal_error", "invite missing in response"); } return decodeInviteSummary(invite); } export async function declineInvite( client: GalaxyClient, gameId: string, inviteId: string, ): Promise { const builder = new Builder(128); const gameIdOff = builder.createString(gameId); const inviteIdOff = builder.createString(inviteId); InviteDeclineRequest.startInviteDeclineRequest(builder); InviteDeclineRequest.addGameId(builder, gameIdOff); InviteDeclineRequest.addInviteId(builder, inviteIdOff); builder.finish(InviteDeclineRequest.endInviteDeclineRequest(builder)); const payload = await execute(client, "lobby.invite.decline", builder.asUint8Array()); const response = InviteDeclineResponse.getRootAsInviteDeclineResponse( new ByteBuffer(payload), ); const invite = response.invite(); if (invite === null) { throw new LobbyError("internal_error", "internal_error", "invite missing in response"); } return decodeInviteSummary(invite); } async function execute( client: GalaxyClient, messageType: string, payloadBytes: Uint8Array, ): Promise { const result = await client.executeCommand(messageType, payloadBytes); if (result.resultCode !== RESULT_CODE_OK) { throw decodeLobbyError(result.resultCode, result.payloadBytes); } return result.payloadBytes; } function decodeLobbyError(resultCode: string, payload: Uint8Array): LobbyError { let code = resultCode; let message = resultCode; try { const errorResponse = FbsErrorResponse.getRootAsErrorResponse(new ByteBuffer(payload)); const body = errorResponse.error(); if (body) { code = body.code() ?? resultCode; message = body.message() ?? resultCode; } } catch (_err) { // fall through to use raw resultCode as both code and message } return new LobbyError(resultCode, code, message); } function decodeGameSummary(summary: FbsGameSummary): GameSummary { return { gameId: summary.gameId() ?? "", gameName: summary.gameName() ?? "", gameType: summary.gameType() ?? "", status: summary.status() ?? "", ownerUserId: summary.ownerUserId() ?? "", minPlayers: summary.minPlayers(), maxPlayers: summary.maxPlayers(), enrollmentEndsAt: dateFromMs(summary.enrollmentEndsAtMs()), createdAt: dateFromMs(summary.createdAtMs()), updatedAt: dateFromMs(summary.updatedAtMs()), }; } function decodeApplicationSummary(app: FbsApplicationSummary): ApplicationSummary { return { applicationId: app.applicationId() ?? "", gameId: app.gameId() ?? "", applicantUserId: app.applicantUserId() ?? "", raceName: app.raceName() ?? "", status: app.status() ?? "", createdAt: dateFromMs(app.createdAtMs()), decidedAt: optionalDateFromMs(app.decidedAtMs()), }; } function decodeInviteSummary(invite: FbsInviteSummary): InviteSummary { return { inviteId: invite.inviteId() ?? "", gameId: invite.gameId() ?? "", inviterUserId: invite.inviterUserId() ?? "", invitedUserId: invite.invitedUserId() ?? "", code: invite.code() ?? "", raceName: invite.raceName() ?? "", status: invite.status() ?? "", createdAt: dateFromMs(invite.createdAtMs()), expiresAt: dateFromMs(invite.expiresAtMs()), decidedAt: optionalDateFromMs(invite.decidedAtMs()), }; } function dateFromMs(ms: bigint): Date { return new Date(Number(ms)); } function optionalDateFromMs(ms: bigint): Date | null { if (ms === 0n) { return null; } return new Date(Number(ms)); }