package transcode import ( flatbuffers "github.com/google/flatbuffers/go" "scrabble/gateway/internal/backendclient" fb "scrabble/pkg/fbs/scrabblefb" ) // 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. // encodeSession builds a Session payload. func encodeSession(s backendclient.SessionResp) []byte { b := flatbuffers.NewBuilder(128) token := b.CreateString(s.Token) uid := b.CreateString(s.UserID) name := b.CreateString(s.DisplayName) fb.SessionStart(b) fb.SessionAddToken(b, token) fb.SessionAddUserId(b, uid) fb.SessionAddIsGuest(b, s.IsGuest) fb.SessionAddDisplayName(b, name) 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 (Stage 11). 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. func encodeLinkResult(r backendclient.LinkResultResp) []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) fb.SessionStart(b) fb.SessionAddToken(b, token) fb.SessionAddUserId(b, uid) fb.SessionAddIsGuest(b, r.Profile.IsGuest) fb.SessionAddDisplayName(b, name) 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) fb.MoveResultStart(b) fb.MoveResultAddMove(b, move) fb.MoveResultAddGame(b, game) 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 (Stage 13: the // client requests it on a per-variant cache miss). func encodeState(s backendclient.StateResp) []byte { b := flatbuffers.NewBuilder(512) game := buildGameView(b, s.Game) rackBytes := make([]byte, len(s.Rack)) for i, v := range s.Rack { rackBytes[i] = byte(v) } rack := b.CreateByteVector(rackBytes) hasAlphabet := len(s.Alphabet) > 0 var alphabet flatbuffers.UOffsetT if hasAlphabet { alphabet = buildAlphabet(b, s.Alphabet) } fb.StateViewStart(b) fb.StateViewAddGame(b, game) fb.StateViewAddSeat(b, int32(s.Seat)) fb.StateViewAddRack(b, rack) fb.StateViewAddBagLen(b, int32(s.BagLen)) fb.StateViewAddHintsRemaining(b, int32(s.HintsRemaining)) if hasAlphabet { fb.StateViewAddAlphabet(b, alphabet) } b.Finish(fb.StateViewEnd(b)) return b.FinishedBytes() } // buildAlphabet builds the AlphabetEntry vector for a StateView and returns its offset. func buildAlphabet(b *flatbuffers.Builder, entries []backendclient.AlphabetEntryJSON) flatbuffers.UOffsetT { offs := make([]flatbuffers.UOffsetT, len(entries)) for i, e := range entries { letter := b.CreateString(e.Letter) fb.AlphabetEntryStart(b) fb.AlphabetEntryAddIndex(b, byte(e.Index)) fb.AlphabetEntryAddLetter(b, letter) fb.AlphabetEntryAddValue(b, int32(e.Value)) offs[i] = fb.AlphabetEntryEnd(b) } fb.StateViewStartAlphabetVector(b, len(offs)) for i := len(offs) - 1; i >= 0; i-- { b.PrependUOffsetT(offs[i]) } return b.EndVector(len(offs)) } // 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() } // 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 { seatOffs := make([]flatbuffers.UOffsetT, len(g.Seats)) for i, s := range g.Seats { aid := b.CreateString(s.AccountID) dname := b.CreateString(s.DisplayName) fb.SeatViewStart(b) fb.SeatViewAddSeat(b, int32(s.Seat)) fb.SeatViewAddAccountId(b, aid) fb.SeatViewAddScore(b, int32(s.Score)) fb.SeatViewAddHintsUsed(b, int32(s.HintsUsed)) fb.SeatViewAddIsWinner(b, s.IsWinner) fb.SeatViewAddDisplayName(b, dname) seatOffs[i] = fb.SeatViewEnd(b) } fb.GameViewStartSeatsVector(b, len(seatOffs)) for i := len(seatOffs) - 1; i >= 0; i-- { b.PrependUOffsetT(seatOffs[i]) } seats := b.EndVector(len(seatOffs)) id := b.CreateString(g.ID) variant := b.CreateString(g.Variant) dictVer := b.CreateString(g.DictVersion) status := b.CreateString(g.Status) endReason := b.CreateString(g.EndReason) fb.GameViewStart(b) fb.GameViewAddId(b, id) fb.GameViewAddVariant(b, variant) fb.GameViewAddDictVersion(b, dictVer) fb.GameViewAddStatus(b, status) fb.GameViewAddPlayers(b, int32(g.Players)) fb.GameViewAddToMove(b, int32(g.ToMove)) fb.GameViewAddTurnTimeoutSecs(b, int32(g.TurnTimeoutSecs)) fb.GameViewAddMoveCount(b, int32(g.MoveCount)) fb.GameViewAddEndReason(b, endReason) fb.GameViewAddSeats(b, seats) return fb.GameViewEnd(b) } // buildMoveRecord builds a MoveRecord table and returns its offset. func buildMoveRecord(b *flatbuffers.Builder, m backendclient.MoveRecordResp) flatbuffers.UOffsetT { tileOffs := make([]flatbuffers.UOffsetT, len(m.Tiles)) for i, t := range m.Tiles { letter := b.CreateString(t.Letter) fb.TileRecordStart(b) fb.TileRecordAddRow(b, int32(t.Row)) fb.TileRecordAddCol(b, int32(t.Col)) fb.TileRecordAddLetter(b, letter) fb.TileRecordAddBlank(b, t.Blank) tileOffs[i] = fb.TileRecordEnd(b) } fb.MoveRecordStartTilesVector(b, len(tileOffs)) for i := len(tileOffs) - 1; i >= 0; i-- { b.PrependUOffsetT(tileOffs[i]) } tiles := b.EndVector(len(tileOffs)) words := buildStringVector(b, m.Words, fb.MoveRecordStartWordsVector) action := b.CreateString(m.Action) dir := b.CreateString(m.Dir) fb.MoveRecordStart(b) fb.MoveRecordAddPlayer(b, int32(m.Player)) fb.MoveRecordAddAction(b, action) fb.MoveRecordAddDir(b, dir) fb.MoveRecordAddMainRow(b, int32(m.MainRow)) fb.MoveRecordAddMainCol(b, int32(m.MainCol)) fb.MoveRecordAddTiles(b, tiles) fb.MoveRecordAddWords(b, words) fb.MoveRecordAddCount(b, int32(m.Count)) fb.MoveRecordAddScore(b, int32(m.Score)) fb.MoveRecordAddTotal(b, int32(m.Total)) return fb.MoveRecordEnd(b) } // 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)) }