// Reads back the player's stored order for the current turn through // `user.games.order.get`. Used by `OrderDraftStore` only when the // local cache row is absent (fresh install, cleared storage, or a // brand-new device): the local draft is the source of truth, so a // present-but-empty cache row means "no commands" and is honoured // over the server snapshot. import { Builder, ByteBuffer } from "flatbuffers"; import type { GalaxyClient } from "../api/galaxy-client"; import { uuidToHiLo } from "../api/game-state"; import { UUID } from "../proto/galaxy/fbs/common"; import { CommandPayload, CommandPlanetRename, UserGamesOrderGet, UserGamesOrderGetResponse, } from "../proto/galaxy/fbs/order"; import type { OrderCommand } from "./order-types"; const MESSAGE_TYPE = "user.games.order.get"; export class OrderLoadError extends Error { readonly resultCode: string; readonly code: string; constructor(resultCode: string, code: string, message: string) { super(message); this.name = "OrderLoadError"; this.resultCode = resultCode; this.code = code; } } export interface FetchedOrder { commands: OrderCommand[]; updatedAt: number; } /** * fetchOrder issues `user.games.order.get` for the given game and * turn, decodes the response, and returns the typed draft. A * `found = false` answer (no order stored on the server) surfaces as * an empty `commands` array — the caller treats this as a clean * draft. Unknown command kinds in the response are skipped with a * console warning so a backend-side schema bump never silently * corrupts the local draft. */ export async function fetchOrder( client: GalaxyClient, gameId: string, turn: number, ): Promise { if (turn < 0) { throw new OrderLoadError( "invalid_request", "invalid_request", `turn must be non-negative, got ${turn}`, ); } const payload = buildRequest(gameId, turn); const result = await client.executeCommand(MESSAGE_TYPE, payload); if (result.resultCode !== "ok") { const { code, message } = decodeError(result.payloadBytes, result.resultCode); throw new OrderLoadError(result.resultCode, code, message); } return decodeResponse(result.payloadBytes); } function buildRequest(gameId: string, turn: number): Uint8Array { const builder = new Builder(64); const [hi, lo] = uuidToHiLo(gameId); const gameIdOffset = UUID.createUUID(builder, hi, lo); UserGamesOrderGet.startUserGamesOrderGet(builder); UserGamesOrderGet.addGameId(builder, gameIdOffset); UserGamesOrderGet.addTurn(builder, BigInt(turn)); const offset = UserGamesOrderGet.endUserGamesOrderGet(builder); builder.finish(offset); return builder.asUint8Array(); } function decodeResponse(payload: Uint8Array): FetchedOrder { if (payload.length === 0) { throw new OrderLoadError( "internal_error", "internal_error", "empty user.games.order.get payload", ); } const buffer = new ByteBuffer(payload); const response = UserGamesOrderGetResponse.getRootAsUserGamesOrderGetResponse(buffer); if (!response.found()) { return { commands: [], updatedAt: 0 }; } const order = response.order(); if (order === null) { throw new OrderLoadError( "internal_error", "internal_error", "order missing while found=true", ); } const commands: OrderCommand[] = []; const length = order.commandsLength(); for (let i = 0; i < length; i++) { const item = order.commands(i); if (item === null) continue; const cmd = decodeCommand(item); if (cmd === null) continue; commands.push(cmd); } return { commands, updatedAt: Number(order.updatedAt()), }; } type CommandItemView = NonNullable< ReturnType>["commands"]> >; function decodeCommand(item: CommandItemView): OrderCommand | null { if (item === null) return null; const id = item.cmdId(); if (id === null) return null; const payloadType = item.payloadType(); switch (payloadType) { case CommandPayload.CommandPlanetRename: { const inner = new CommandPlanetRename(); item.payload(inner); return { kind: "planetRename", id, planetNumber: Number(inner.number()), name: inner.name() ?? "", }; } default: console.warn( `fetchOrder: skipping unknown command kind (payloadType=${payloadType})`, ); return null; } } function decodeError( payload: Uint8Array, resultCode: string, ): { code: string; message: string } { if (payload.length === 0) { return { code: resultCode, message: resultCode }; } try { const text = new TextDecoder().decode(payload); const parsed = JSON.parse(text) as { code?: string; message?: string }; return { code: typeof parsed.code === "string" ? parsed.code : resultCode, message: typeof parsed.message === "string" ? parsed.message : text, }; } catch { return { code: resultCode, message: resultCode }; } }