// FlatBuffers <-> domain-model codec. The real transport encodes each request table // and decodes each response/event table here, mirroring the gateway's Go encoders in // reverse. The screens only ever see the plain model (lib/model), never these wire // types. import { Builder, ByteBuffer, type Offset } from 'flatbuffers'; import * as fb from '../gen/fbs/scrabblefb'; import type { PlacedTile } from './client'; import type { AccountRef, ChatMessage, EvalResult, FriendCode, GameList, GameView, GcgExport, History, HintResult, Invitation, InvitationInvitee, InvitationSettings, LinkResult, MatchResult, MoveRecord, MoveResult, Profile, ProfileUpdate, PushEvent, Seat, Session, StateView, Stats, Tile, Variant, WordCheckResult, } from './model'; // --- request encoders --- function buildTile(b: Builder, t: PlacedTile): Offset { const letter = b.createString(t.letter); fb.TileRecord.startTileRecord(b); fb.TileRecord.addRow(b, t.row); fb.TileRecord.addCol(b, t.col); fb.TileRecord.addLetter(b, letter); fb.TileRecord.addBlank(b, t.blank); return fb.TileRecord.endTileRecord(b); } function finish(b: Builder, root: Offset): Uint8Array { b.finish(root); return b.asUint8Array(); } export const empty = (): Uint8Array => new Uint8Array(); export function encodeGameAction(gameId: string): Uint8Array { const b = new Builder(64); const gid = b.createString(gameId); fb.GameActionRequest.startGameActionRequest(b); fb.GameActionRequest.addGameId(b, gid); return finish(b, fb.GameActionRequest.endGameActionRequest(b)); } export function encodeStateRequest(gameId: string): Uint8Array { const b = new Builder(64); const gid = b.createString(gameId); fb.StateRequest.startStateRequest(b); fb.StateRequest.addGameId(b, gid); return finish(b, fb.StateRequest.endStateRequest(b)); } export function encodeSubmitPlay(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array { const b = new Builder(256); const tileOffs = tiles.map((t) => buildTile(b, t)); const vec = fb.SubmitPlayRequest.createTilesVector(b, tileOffs); const gid = b.createString(gameId); const d = b.createString(dir); fb.SubmitPlayRequest.startSubmitPlayRequest(b); fb.SubmitPlayRequest.addGameId(b, gid); fb.SubmitPlayRequest.addDir(b, d); fb.SubmitPlayRequest.addTiles(b, vec); return finish(b, fb.SubmitPlayRequest.endSubmitPlayRequest(b)); } export function encodeEval(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array { const b = new Builder(256); const tileOffs = tiles.map((t) => buildTile(b, t)); const vec = fb.EvalRequest.createTilesVector(b, tileOffs); const gid = b.createString(gameId); const d = b.createString(dir); fb.EvalRequest.startEvalRequest(b); fb.EvalRequest.addGameId(b, gid); fb.EvalRequest.addDir(b, d); fb.EvalRequest.addTiles(b, vec); return finish(b, fb.EvalRequest.endEvalRequest(b)); } export function encodeExchange(gameId: string, tiles: string[]): Uint8Array { const b = new Builder(128); const offs = tiles.map((s) => b.createString(s)); const vec = fb.ExchangeRequest.createTilesVector(b, offs); const gid = b.createString(gameId); fb.ExchangeRequest.startExchangeRequest(b); fb.ExchangeRequest.addGameId(b, gid); fb.ExchangeRequest.addTiles(b, vec); return finish(b, fb.ExchangeRequest.endExchangeRequest(b)); } export function encodeCheckWord(gameId: string, word: string): Uint8Array { const b = new Builder(128); const gid = b.createString(gameId); const w = b.createString(word); fb.CheckWordRequest.startCheckWordRequest(b); fb.CheckWordRequest.addGameId(b, gid); fb.CheckWordRequest.addWord(b, w); return finish(b, fb.CheckWordRequest.endCheckWordRequest(b)); } export function encodeComplaint(gameId: string, word: string, note: string): Uint8Array { const b = new Builder(256); const gid = b.createString(gameId); const w = b.createString(word); const n = b.createString(note); fb.ComplaintRequest.startComplaintRequest(b); fb.ComplaintRequest.addGameId(b, gid); fb.ComplaintRequest.addWord(b, w); fb.ComplaintRequest.addNote(b, n); return finish(b, fb.ComplaintRequest.endComplaintRequest(b)); } export function encodeEnqueue(variant: Variant): Uint8Array { const b = new Builder(64); const v = b.createString(variant); fb.EnqueueRequest.startEnqueueRequest(b); fb.EnqueueRequest.addVariant(b, v); return finish(b, fb.EnqueueRequest.endEnqueueRequest(b)); } export function encodeChatPost(gameId: string, body: string): Uint8Array { const b = new Builder(128); const gid = b.createString(gameId); const bd = b.createString(body); fb.ChatPostRequest.startChatPostRequest(b); fb.ChatPostRequest.addGameId(b, gid); fb.ChatPostRequest.addBody(b, bd); return finish(b, fb.ChatPostRequest.endChatPostRequest(b)); } export function encodeTelegramLogin(initData: string): Uint8Array { const b = new Builder(512); const d = b.createString(initData); fb.TelegramLoginRequest.startTelegramLoginRequest(b); fb.TelegramLoginRequest.addInitData(b, d); return finish(b, fb.TelegramLoginRequest.endTelegramLoginRequest(b)); } export function encodeGuestLogin(locale: string): Uint8Array { const b = new Builder(64); const l = b.createString(locale); fb.GuestLoginRequest.startGuestLoginRequest(b); fb.GuestLoginRequest.addLocale(b, l); return finish(b, fb.GuestLoginRequest.endGuestLoginRequest(b)); } export function encodeEmailRequest(email: string): Uint8Array { const b = new Builder(128); const e = b.createString(email); fb.EmailRequestRequest.startEmailRequestRequest(b); fb.EmailRequestRequest.addEmail(b, e); return finish(b, fb.EmailRequestRequest.endEmailRequestRequest(b)); } export function encodeEmailLogin(email: string, code: string): Uint8Array { const b = new Builder(128); const e = b.createString(email); const c = b.createString(code); fb.EmailLoginRequest.startEmailLoginRequest(b); fb.EmailLoginRequest.addEmail(b, e); fb.EmailLoginRequest.addCode(b, c); return finish(b, fb.EmailLoginRequest.endEmailLoginRequest(b)); } // --- response decoders --- function s(v: string | null): string { return v ?? ''; } function decodeTile(t: fb.TileRecord): Tile { return { row: t.row(), col: t.col(), letter: s(t.letter()), blank: t.blank() }; } function decodeSeat(v: fb.SeatView): Seat { return { seat: v.seat(), accountId: s(v.accountId()), displayName: s(v.displayName()), score: v.score(), hintsUsed: v.hintsUsed(), isWinner: v.isWinner(), }; } function decodeGameView(g: fb.GameView): GameView { const seats: Seat[] = []; for (let i = 0; i < g.seatsLength(); i++) { const sv = g.seats(i); if (sv) seats.push(decodeSeat(sv)); } return { id: s(g.id()), variant: s(g.variant()) as Variant, dictVersion: s(g.dictVersion()), status: s(g.status()), players: g.players(), toMove: g.toMove(), turnTimeoutSecs: g.turnTimeoutSecs(), moveCount: g.moveCount(), endReason: s(g.endReason()), seats, }; } function decodeMove(m: fb.MoveRecord): MoveRecord { const tiles: Tile[] = []; for (let i = 0; i < m.tilesLength(); i++) { const t = m.tiles(i); if (t) tiles.push(decodeTile(t)); } const words: string[] = []; for (let i = 0; i < m.wordsLength(); i++) words.push(s(m.words(i))); return { player: m.player(), action: s(m.action()), dir: s(m.dir()), mainRow: m.mainRow(), mainCol: m.mainCol(), tiles, words, count: m.count(), score: m.score(), total: m.total(), }; } function decodeChatMsg(m: fb.ChatMessage): ChatMessage { return { id: s(m.id()), gameId: s(m.gameId()), senderId: s(m.senderId()), kind: s(m.kind()), body: s(m.body()), createdAtUnix: Number(m.createdAtUnix()), }; } export function decodeSession(buf: Uint8Array): Session { const t = fb.Session.getRootAsSession(new ByteBuffer(buf)); return { token: s(t.token()), userId: s(t.userId()), isGuest: t.isGuest(), displayName: s(t.displayName()) }; } export function decodeProfile(buf: Uint8Array): Profile { const p = fb.Profile.getRootAsProfile(new ByteBuffer(buf)); return { userId: s(p.userId()), displayName: s(p.displayName()), preferredLanguage: s(p.preferredLanguage()), timeZone: s(p.timeZone()), awayStart: s(p.awayStart()), awayEnd: s(p.awayEnd()), hintBalance: p.hintBalance(), blockChat: p.blockChat(), blockFriendRequests: p.blockFriendRequests(), isGuest: p.isGuest(), notificationsInAppOnly: p.notificationsInAppOnly(), }; } export function decodeStateView(buf: Uint8Array): StateView { const v = fb.StateView.getRootAsStateView(new ByteBuffer(buf)); const g = v.game(); const rack: string[] = []; for (let i = 0; i < v.rackLength(); i++) rack.push(s(v.rack(i))); return { game: g ? decodeGameView(g) : emptyGame(), seat: v.seat(), rack, bagLen: v.bagLen(), hintsRemaining: v.hintsRemaining(), }; } export function decodeMoveResult(buf: Uint8Array): MoveResult { const r = fb.MoveResult.getRootAsMoveResult(new ByteBuffer(buf)); const m = r.move(); const g = r.game(); return { move: m ? decodeMove(m) : emptyMove(), game: g ? decodeGameView(g) : emptyGame() }; } export function decodeHintResult(buf: Uint8Array): HintResult { const r = fb.HintResult.getRootAsHintResult(new ByteBuffer(buf)); const m = r.move(); return { move: m ? decodeMove(m) : emptyMove(), hintsRemaining: r.hintsRemaining() }; } export function decodeEvalResult(buf: Uint8Array): EvalResult { const r = fb.EvalResult.getRootAsEvalResult(new ByteBuffer(buf)); const words: string[] = []; for (let i = 0; i < r.wordsLength(); i++) words.push(s(r.words(i))); return { legal: r.legal(), score: r.score(), words }; } export function decodeWordCheck(buf: Uint8Array): WordCheckResult { const r = fb.WordCheckResult.getRootAsWordCheckResult(new ByteBuffer(buf)); return { word: s(r.word()), legal: r.legal() }; } export function decodeHistory(buf: Uint8Array): History { const h = fb.History.getRootAsHistory(new ByteBuffer(buf)); const moves: MoveRecord[] = []; for (let i = 0; i < h.movesLength(); i++) { const m = h.moves(i); if (m) moves.push(decodeMove(m)); } return { gameId: s(h.gameId()), moves }; } export function decodeGameList(buf: Uint8Array): GameList { const gl = fb.GameList.getRootAsGameList(new ByteBuffer(buf)); const games: GameView[] = []; for (let i = 0; i < gl.gamesLength(); i++) { const g = gl.games(i); if (g) games.push(decodeGameView(g)); } return { games }; } export function decodeMatchResult(buf: Uint8Array): MatchResult { const m = fb.MatchResult.getRootAsMatchResult(new ByteBuffer(buf)); const g = m.game(); return { matched: m.matched(), game: m.matched() && g ? decodeGameView(g) : undefined }; } export function decodeChatMessage(buf: Uint8Array): ChatMessage { return decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(new ByteBuffer(buf))); } export function decodeChatList(buf: Uint8Array): ChatMessage[] { const cl = fb.ChatList.getRootAsChatList(new ByteBuffer(buf)); const out: ChatMessage[] = []; for (let i = 0; i < cl.messagesLength(); i++) { const m = cl.messages(i); if (m) out.push(decodeChatMsg(m)); } return out; } export function decodeEvent(kind: string, payload: Uint8Array): PushEvent | null { const bb = new ByteBuffer(payload); switch (kind) { case 'your_turn': { const e = fb.YourTurnEvent.getRootAsYourTurnEvent(bb); return { kind: 'your_turn', gameId: s(e.gameId()), deadlineUnix: Number(e.deadlineUnix()) }; } case 'opponent_moved': { const e = fb.OpponentMovedEvent.getRootAsOpponentMovedEvent(bb); return { kind: 'opponent_moved', gameId: s(e.gameId()), seat: e.seat(), action: s(e.action()), score: e.score(), total: e.total() }; } case 'chat_message': return { kind: 'chat_message', message: decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(bb)) }; case 'nudge': { const e = fb.NudgeEvent.getRootAsNudgeEvent(bb); return { kind: 'nudge', gameId: s(e.gameId()), fromUserId: s(e.fromUserId()) }; } case 'match_found': { const e = fb.MatchFoundEvent.getRootAsMatchFoundEvent(bb); return { kind: 'match_found', gameId: s(e.gameId()) }; } case 'notify': { const e = fb.NotificationEvent.getRootAsNotificationEvent(bb); return { kind: 'notify', sub: s(e.kind()) }; } case 'heartbeat': return { kind: 'heartbeat' }; default: return null; } } // --- Stage 8 encoders --- export function encodeTarget(accountId: string): Uint8Array { const b = new Builder(64); const id = b.createString(accountId); fb.TargetRequest.startTargetRequest(b); fb.TargetRequest.addAccountId(b, id); return finish(b, fb.TargetRequest.endTargetRequest(b)); } export function encodeFriendRespond(requesterId: string, accept: boolean): Uint8Array { const b = new Builder(64); const id = b.createString(requesterId); fb.FriendRespondRequest.startFriendRespondRequest(b); fb.FriendRespondRequest.addRequesterId(b, id); fb.FriendRespondRequest.addAccept(b, accept); return finish(b, fb.FriendRespondRequest.endFriendRespondRequest(b)); } export function encodeRedeemCode(code: string): Uint8Array { const b = new Builder(32); const c = b.createString(code); fb.RedeemCodeRequest.startRedeemCodeRequest(b); fb.RedeemCodeRequest.addCode(b, c); return finish(b, fb.RedeemCodeRequest.endRedeemCodeRequest(b)); } export function encodeCreateInvitation(inviteeIds: string[], st: InvitationSettings): Uint8Array { const b = new Builder(256); const idOffs = inviteeIds.map((id) => b.createString(id)); const ids = fb.CreateInvitationRequest.createInviteeIdsVector(b, idOffs); const variant = b.createString(st.variant); const dropout = b.createString(st.dropoutTiles); fb.CreateInvitationRequest.startCreateInvitationRequest(b); fb.CreateInvitationRequest.addInviteeIds(b, ids); fb.CreateInvitationRequest.addVariant(b, variant); fb.CreateInvitationRequest.addTurnTimeoutSecs(b, st.turnTimeoutSecs); fb.CreateInvitationRequest.addHintsAllowed(b, st.hintsAllowed); fb.CreateInvitationRequest.addHintsPerPlayer(b, st.hintsPerPlayer); fb.CreateInvitationRequest.addDropoutTiles(b, dropout); return finish(b, fb.CreateInvitationRequest.endCreateInvitationRequest(b)); } export function encodeInvitationAction(invitationId: string): Uint8Array { const b = new Builder(64); const id = b.createString(invitationId); fb.InvitationActionRequest.startInvitationActionRequest(b); fb.InvitationActionRequest.addInvitationId(b, id); return finish(b, fb.InvitationActionRequest.endInvitationActionRequest(b)); } export function encodeUpdateProfile(p: ProfileUpdate): Uint8Array { const b = new Builder(256); const name = b.createString(p.displayName); const lang = b.createString(p.preferredLanguage); const tz = b.createString(p.timeZone); const as = b.createString(p.awayStart); const ae = b.createString(p.awayEnd); fb.UpdateProfileRequest.startUpdateProfileRequest(b); fb.UpdateProfileRequest.addDisplayName(b, name); fb.UpdateProfileRequest.addPreferredLanguage(b, lang); fb.UpdateProfileRequest.addTimeZone(b, tz); fb.UpdateProfileRequest.addAwayStart(b, as); fb.UpdateProfileRequest.addAwayEnd(b, ae); fb.UpdateProfileRequest.addBlockChat(b, p.blockChat); fb.UpdateProfileRequest.addBlockFriendRequests(b, p.blockFriendRequests); fb.UpdateProfileRequest.addNotificationsInAppOnly(b, p.notificationsInAppOnly); return finish(b, fb.UpdateProfileRequest.endUpdateProfileRequest(b)); } // --- account linking & merge (Stage 11) --- export function encodeLinkEmailRequest(email: string): Uint8Array { const b = new Builder(128); const e = b.createString(email); fb.LinkEmailRequest.startLinkEmailRequest(b); fb.LinkEmailRequest.addEmail(b, e); return finish(b, fb.LinkEmailRequest.endLinkEmailRequest(b)); } export function encodeLinkEmailConfirm(email: string, code: string): Uint8Array { const b = new Builder(128); const e = b.createString(email); const c = b.createString(code); fb.LinkEmailConfirm.startLinkEmailConfirm(b); fb.LinkEmailConfirm.addEmail(b, e); fb.LinkEmailConfirm.addCode(b, c); return finish(b, fb.LinkEmailConfirm.endLinkEmailConfirm(b)); } export function encodeLinkTelegram(data: string): Uint8Array { const b = new Builder(256); const d = b.createString(data); fb.LinkTelegramRequest.startLinkTelegramRequest(b); fb.LinkTelegramRequest.addData(b, d); return finish(b, fb.LinkTelegramRequest.endLinkTelegramRequest(b)); } export function decodeLinkResult(buf: Uint8Array): LinkResult { const r = fb.LinkResult.getRootAsLinkResult(new ByteBuffer(buf)); const sess = r.session(); return { status: (s(r.status()) || 'linked') as LinkResult['status'], secondaryUserId: s(r.secondaryUserId()), secondaryDisplayName: s(r.secondaryDisplayName()), secondaryGames: r.secondaryGames(), secondaryFriends: r.secondaryFriends(), session: sess ? { token: s(sess.token()), userId: s(sess.userId()), isGuest: sess.isGuest(), displayName: s(sess.displayName()) } : null, }; } // --- Stage 8 decoders --- function decodeAccountRef(r: fb.AccountRef): AccountRef { return { accountId: s(r.accountId()), displayName: s(r.displayName()) }; } export function decodeFriendList(buf: Uint8Array): AccountRef[] { const l = fb.FriendList.getRootAsFriendList(new ByteBuffer(buf)); const out: AccountRef[] = []; for (let i = 0; i < l.friendsLength(); i++) { const r = l.friends(i); if (r) out.push(decodeAccountRef(r)); } return out; } export function decodeIncomingList(buf: Uint8Array): AccountRef[] { const l = fb.IncomingRequestList.getRootAsIncomingRequestList(new ByteBuffer(buf)); const out: AccountRef[] = []; for (let i = 0; i < l.requestsLength(); i++) { const r = l.requests(i); if (r) out.push(decodeAccountRef(r)); } return out; } export function decodeBlockList(buf: Uint8Array): AccountRef[] { const l = fb.BlockList.getRootAsBlockList(new ByteBuffer(buf)); const out: AccountRef[] = []; for (let i = 0; i < l.blockedLength(); i++) { const r = l.blocked(i); if (r) out.push(decodeAccountRef(r)); } return out; } export function decodeFriendCode(buf: Uint8Array): FriendCode { const c = fb.FriendCode.getRootAsFriendCode(new ByteBuffer(buf)); return { code: s(c.code()), expiresAtUnix: Number(c.expiresAtUnix()) }; } export function decodeRedeemResult(buf: Uint8Array): AccountRef { const r = fb.RedeemResult.getRootAsRedeemResult(new ByteBuffer(buf)); const f = r.friend(); return f ? decodeAccountRef(f) : { accountId: '', displayName: '' }; } export function decodeStats(buf: Uint8Array): Stats { const v = fb.StatsView.getRootAsStatsView(new ByteBuffer(buf)); return { wins: v.wins(), losses: v.losses(), draws: v.draws(), maxGamePoints: v.maxGamePoints(), maxWordPoints: v.maxWordPoints(), }; } function decodeInvitationTable(i: fb.Invitation): Invitation { const inviter = i.inviter(); const invitees: InvitationInvitee[] = []; for (let k = 0; k < i.inviteesLength(); k++) { const iv = i.invitees(k); if (iv) { invitees.push({ accountId: s(iv.accountId()), displayName: s(iv.displayName()), seat: iv.seat(), response: s(iv.response()), }); } } return { id: s(i.id()), inviter: inviter ? decodeAccountRef(inviter) : { accountId: '', displayName: '' }, invitees, variant: s(i.variant()) as Variant, turnTimeoutSecs: i.turnTimeoutSecs(), hintsAllowed: i.hintsAllowed(), hintsPerPlayer: i.hintsPerPlayer(), dropoutTiles: s(i.dropoutTiles()), status: s(i.status()), gameId: s(i.gameId()), expiresAtUnix: Number(i.expiresAtUnix()), }; } export function decodeInvitation(buf: Uint8Array): Invitation { return decodeInvitationTable(fb.Invitation.getRootAsInvitation(new ByteBuffer(buf))); } export function decodeInvitationList(buf: Uint8Array): Invitation[] { const l = fb.InvitationList.getRootAsInvitationList(new ByteBuffer(buf)); const out: Invitation[] = []; for (let i = 0; i < l.invitationsLength(); i++) { const inv = l.invitations(i); if (inv) out.push(decodeInvitationTable(inv)); } return out; } export function decodeGcg(buf: Uint8Array): GcgExport { const g = fb.GcgExport.getRootAsGcgExport(new ByteBuffer(buf)); return { gameId: s(g.gameId()), filename: s(g.filename()), content: s(g.content()) }; } function emptyGame(): GameView { return { id: '', variant: 'english', dictVersion: '', status: '', players: 0, toMove: 0, turnTimeoutSecs: 0, moveCount: 0, endReason: '', seats: [], }; } function emptyMove(): MoveRecord { return { player: 0, action: '', dir: '', mainRow: 0, mainCol: 0, tiles: [], words: [], count: 0, score: 0, total: 0 }; }