package transcode import ( flatbuffers "github.com/google/flatbuffers/go" "scrabble/gateway/internal/backendclient" fb "scrabble/pkg/fbs/scrabblefb" "scrabble/pkg/wire" ) // The encoders build the FlatBuffers response payloads from the backend's typed // responses. FlatBuffers is built bottom-up: every string and child vector is // created before the table that references it, and no two tables/vectors are // under construction at once. // buildSupportedLanguagesVector creates the Session.supported_languages [string] // vector from langs. FlatBuffers is built bottom-up, so the caller must invoke this // (which itself creates the element strings) before SessionStart and with no table // under construction. func buildSupportedLanguagesVector(b *flatbuffers.Builder, langs []string) flatbuffers.UOffsetT { offsets := make([]flatbuffers.UOffsetT, len(langs)) for i, lang := range langs { offsets[i] = b.CreateString(lang) } fb.SessionStartSupportedLanguagesVector(b, len(langs)) for i := len(offsets) - 1; i >= 0; i-- { b.PrependUOffsetT(offsets[i]) } return b.EndVector(len(langs)) } // encodeSession builds a Session payload. supportedLangs is the service's set of // offered game languages, which the UI gates the New Game variant choice by. func encodeSession(s backendclient.SessionResp, supportedLangs []string) []byte { b := flatbuffers.NewBuilder(128) token := b.CreateString(s.Token) uid := b.CreateString(s.UserID) name := b.CreateString(s.DisplayName) langs := buildSupportedLanguagesVector(b, supportedLangs) fb.SessionStart(b) fb.SessionAddToken(b, token) fb.SessionAddUserId(b, uid) fb.SessionAddIsGuest(b, s.IsGuest) fb.SessionAddDisplayName(b, name) fb.SessionAddSupportedLanguages(b, langs) b.Finish(fb.SessionEnd(b)) return b.FinishedBytes() } // encodeAck builds an Ack payload. func encodeAck(ok bool) []byte { b := flatbuffers.NewBuilder(16) fb.AckStart(b) fb.AckAddOk(b, ok) b.Finish(fb.AckEnd(b)) return b.FinishedBytes() } // encodeProfile builds a Profile payload. func encodeProfile(p backendclient.ProfileResp) []byte { b := flatbuffers.NewBuilder(192) uid := b.CreateString(p.UserID) name := b.CreateString(p.DisplayName) lang := b.CreateString(p.PreferredLanguage) tz := b.CreateString(p.TimeZone) awayStart := b.CreateString(p.AwayStart) awayEnd := b.CreateString(p.AwayEnd) fb.ProfileStart(b) fb.ProfileAddUserId(b, uid) fb.ProfileAddDisplayName(b, name) fb.ProfileAddPreferredLanguage(b, lang) fb.ProfileAddTimeZone(b, tz) fb.ProfileAddHintBalance(b, int32(p.HintBalance)) fb.ProfileAddBlockChat(b, p.BlockChat) fb.ProfileAddBlockFriendRequests(b, p.BlockFriendRequests) fb.ProfileAddIsGuest(b, p.IsGuest) fb.ProfileAddAwayStart(b, awayStart) fb.ProfileAddAwayEnd(b, awayEnd) fb.ProfileAddNotificationsInAppOnly(b, p.NotificationsInAppOnly) b.Finish(fb.ProfileEnd(b)) return b.FinishedBytes() } // encodeLinkResult builds a LinkResult payload. A switched-session token // (a guest initiator whose durable counterpart won) is carried as a nested Session // for the client to adopt; it is omitted otherwise. supportedLangs is the variant // gating set for that switched session — the link flows run on the web, so it is the // gateway's default (non-platform) set. func encodeLinkResult(r backendclient.LinkResultResp, supportedLangs []string) []byte { b := flatbuffers.NewBuilder(256) status := b.CreateString(r.Status) secID := b.CreateString(r.SecondaryUserID) secName := b.CreateString(r.SecondaryName) hasSession := r.Token != "" && r.Profile != nil var sess flatbuffers.UOffsetT if hasSession { token := b.CreateString(r.Token) uid := b.CreateString(r.Profile.UserID) name := b.CreateString(r.Profile.DisplayName) langs := buildSupportedLanguagesVector(b, supportedLangs) fb.SessionStart(b) fb.SessionAddToken(b, token) fb.SessionAddUserId(b, uid) fb.SessionAddIsGuest(b, r.Profile.IsGuest) fb.SessionAddDisplayName(b, name) fb.SessionAddSupportedLanguages(b, langs) sess = fb.SessionEnd(b) } fb.LinkResultStart(b) fb.LinkResultAddStatus(b, status) fb.LinkResultAddSecondaryUserId(b, secID) fb.LinkResultAddSecondaryDisplayName(b, secName) fb.LinkResultAddSecondaryGames(b, int32(r.SecondaryGames)) fb.LinkResultAddSecondaryFriends(b, int32(r.SecondaryFriends)) if hasSession { fb.LinkResultAddSession(b, sess) } b.Finish(fb.LinkResultEnd(b)) return b.FinishedBytes() } // encodeMoveResult builds a MoveResult payload. func encodeMoveResult(r backendclient.MoveResultResp) []byte { b := flatbuffers.NewBuilder(512) move := buildMoveRecord(b, r.Move) game := buildGameView(b, r.Game) rackBytes := make([]byte, len(r.Rack)) for i, v := range r.Rack { rackBytes[i] = byte(v) } rack := b.CreateByteVector(rackBytes) fb.MoveResultStart(b) fb.MoveResultAddMove(b, move) fb.MoveResultAddGame(b, game) fb.MoveResultAddRack(b, rack) fb.MoveResultAddBagLen(b, int32(r.BagLen)) b.Finish(fb.MoveResultEnd(b)) return b.FinishedBytes() } // encodeState builds a StateView payload. The rack is a vector of alphabet indices and the // alphabet display table is included only when the backend returned it (the // client requests it on a per-variant cache miss). func encodeState(s backendclient.StateResp) []byte { b := flatbuffers.NewBuilder(512) b.Finish(wire.BuildStateView(b, toWireState(s))) return b.FinishedBytes() } // toWireState maps a StateResp to the shared wire.StateView. func toWireState(s backendclient.StateResp) wire.StateView { alphabet := make([]wire.AlphabetEntry, len(s.Alphabet)) for i, e := range s.Alphabet { alphabet[i] = wire.AlphabetEntry{Index: e.Index, Letter: e.Letter, Value: e.Value} } return wire.StateView{ Game: toWireGame(s.Game), Seat: s.Seat, Rack: s.Rack, BagLen: s.BagLen, HintsRemaining: s.HintsRemaining, Alphabet: alphabet, } } // encodeMatch builds a MatchResult payload. func encodeMatch(m backendclient.MatchResp) []byte { b := flatbuffers.NewBuilder(512) matched := m.Matched && m.Game != nil var game flatbuffers.UOffsetT if matched { game = buildGameView(b, *m.Game) } fb.MatchResultStart(b) fb.MatchResultAddMatched(b, matched) if matched { fb.MatchResultAddGame(b, game) } b.Finish(fb.MatchResultEnd(b)) return b.FinishedBytes() } // buildChatMessage builds a ChatMessage table and returns its offset. func buildChatMessage(b *flatbuffers.Builder, c backendclient.ChatResp) flatbuffers.UOffsetT { id := b.CreateString(c.ID) gid := b.CreateString(c.GameID) sid := b.CreateString(c.SenderID) kind := b.CreateString(c.Kind) body := b.CreateString(c.Body) fb.ChatMessageStart(b) fb.ChatMessageAddId(b, id) fb.ChatMessageAddGameId(b, gid) fb.ChatMessageAddSenderId(b, sid) fb.ChatMessageAddKind(b, kind) fb.ChatMessageAddBody(b, body) fb.ChatMessageAddCreatedAtUnix(b, c.CreatedAtUnix) return fb.ChatMessageEnd(b) } // encodeChat builds a ChatMessage payload. func encodeChat(c backendclient.ChatResp) []byte { b := flatbuffers.NewBuilder(192) b.Finish(buildChatMessage(b, c)) return b.FinishedBytes() } // encodeHintResult builds a HintResult payload. func encodeHintResult(r backendclient.HintResultResp) []byte { b := flatbuffers.NewBuilder(512) move := buildMoveRecord(b, r.Move) fb.HintResultStart(b) fb.HintResultAddMove(b, move) fb.HintResultAddHintsRemaining(b, int32(r.HintsRemaining)) b.Finish(fb.HintResultEnd(b)) return b.FinishedBytes() } // encodeEvalResult builds an EvalResult payload. func encodeEvalResult(r backendclient.EvalResultResp) []byte { b := flatbuffers.NewBuilder(256) words := buildStringVector(b, r.Words, fb.EvalResultStartWordsVector) fb.EvalResultStart(b) fb.EvalResultAddLegal(b, r.Legal) fb.EvalResultAddScore(b, int32(r.Score)) fb.EvalResultAddWords(b, words) b.Finish(fb.EvalResultEnd(b)) return b.FinishedBytes() } // encodeWordCheck builds a WordCheckResult payload. func encodeWordCheck(r backendclient.WordCheckResp) []byte { b := flatbuffers.NewBuilder(64) word := b.CreateString(r.Word) fb.WordCheckResultStart(b) fb.WordCheckResultAddWord(b, word) fb.WordCheckResultAddLegal(b, r.Legal) b.Finish(fb.WordCheckResultEnd(b)) return b.FinishedBytes() } // encodeDraftView builds a DraftView payload wrapping the player's composition JSON. The // string is empty for the save acknowledgement (the client ignores that payload). func encodeDraftView(jsonStr string) []byte { b := flatbuffers.NewBuilder(256) j := b.CreateString(jsonStr) fb.DraftViewStart(b) fb.DraftViewAddJson(b, j) b.Finish(fb.DraftViewEnd(b)) return b.FinishedBytes() } // encodeHistory builds a History payload (the decoded move journal). func encodeHistory(r backendclient.HistoryResp) []byte { b := flatbuffers.NewBuilder(1024) moveOffs := make([]flatbuffers.UOffsetT, len(r.Moves)) for i, m := range r.Moves { moveOffs[i] = buildMoveRecord(b, m) } fb.HistoryStartMovesVector(b, len(moveOffs)) for i := len(moveOffs) - 1; i >= 0; i-- { b.PrependUOffsetT(moveOffs[i]) } moves := b.EndVector(len(moveOffs)) gid := b.CreateString(r.GameID) fb.HistoryStart(b) fb.HistoryAddGameId(b, gid) fb.HistoryAddMoves(b, moves) b.Finish(fb.HistoryEnd(b)) return b.FinishedBytes() } // encodeGameList builds a GameList payload (the caller's games). func encodeGameList(r backendclient.GameListResp) []byte { b := flatbuffers.NewBuilder(1024) gameOffs := make([]flatbuffers.UOffsetT, len(r.Games)) for i, g := range r.Games { gameOffs[i] = buildGameView(b, g) } fb.GameListStartGamesVector(b, len(gameOffs)) for i := len(gameOffs) - 1; i >= 0; i-- { b.PrependUOffsetT(gameOffs[i]) } games := b.EndVector(len(gameOffs)) fb.GameListStart(b) fb.GameListAddGames(b, games) b.Finish(fb.GameListEnd(b)) return b.FinishedBytes() } // encodeChatList builds a ChatList payload (a game's chat history). func encodeChatList(r backendclient.ChatListResp) []byte { b := flatbuffers.NewBuilder(512) msgOffs := make([]flatbuffers.UOffsetT, len(r.Messages)) for i, m := range r.Messages { msgOffs[i] = buildChatMessage(b, m) } fb.ChatListStartMessagesVector(b, len(msgOffs)) for i := len(msgOffs) - 1; i >= 0; i-- { b.PrependUOffsetT(msgOffs[i]) } msgs := b.EndVector(len(msgOffs)) fb.ChatListStart(b) fb.ChatListAddMessages(b, msgs) b.Finish(fb.ChatListEnd(b)) return b.FinishedBytes() } // buildGameView builds a GameView table and returns its offset. func buildGameView(b *flatbuffers.Builder, g backendclient.GameResp) flatbuffers.UOffsetT { return wire.BuildGameView(b, toWireGame(g)) } // toWireGame maps a GameResp to the shared wire.GameView. func toWireGame(g backendclient.GameResp) wire.GameView { seats := make([]wire.SeatView, len(g.Seats)) for i, s := range g.Seats { seats[i] = wire.SeatView{ Seat: s.Seat, AccountID: s.AccountID, Score: s.Score, HintsUsed: s.HintsUsed, IsWinner: s.IsWinner, DisplayName: s.DisplayName, } } return wire.GameView{ ID: g.ID, Variant: g.Variant, DictVersion: g.DictVersion, Status: g.Status, Players: g.Players, ToMove: g.ToMove, TurnTimeoutSecs: g.TurnTimeoutSecs, MoveCount: g.MoveCount, EndReason: g.EndReason, Seats: seats, LastActivityUnix: g.LastActivityUnix, } } // buildMoveRecord builds a MoveRecord table and returns its offset. func buildMoveRecord(b *flatbuffers.Builder, m backendclient.MoveRecordResp) flatbuffers.UOffsetT { tiles := make([]wire.TileRecord, len(m.Tiles)) for i, t := range m.Tiles { tiles[i] = wire.TileRecord{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank} } return wire.BuildMoveRecord(b, wire.MoveRecord{ Player: m.Player, Action: m.Action, Dir: m.Dir, MainRow: m.MainRow, MainCol: m.MainCol, Tiles: tiles, Words: m.Words, Count: m.Count, Score: m.Score, Total: m.Total, }) } // buildStringVector builds a vector of strings using the table-specific // StartXVector function and returns the vector offset. func buildStringVector(b *flatbuffers.Builder, items []string, start func(*flatbuffers.Builder, int) flatbuffers.UOffsetT) flatbuffers.UOffsetT { offs := make([]flatbuffers.UOffsetT, len(items)) for i, s := range items { offs[i] = b.CreateString(s) } start(b, len(offs)) for i := len(offs) - 1; i >= 0; i-- { b.PrependUOffsetT(offs[i]) } return b.EndVector(len(offs)) }