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. // 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() } // encodeWalletOrder builds a WalletOrderResponse payload: the created order id and the provider // launch URL the client opens. func encodeWalletOrder(o backendclient.WalletOrderResp) []byte { b := flatbuffers.NewBuilder(128) oid := b.CreateString(o.OrderID) url := b.CreateString(o.RedirectURL) fb.WalletOrderResponseStart(b) fb.WalletOrderResponseAddOrderId(b, oid) fb.WalletOrderResponseAddRedirectUrl(b, url) b.Finish(fb.WalletOrderResponseEnd(b)) return b.FinishedBytes() } // encodeDeleteRequestResult builds an AccountDeleteRequestResult payload reporting which // deletion step-up the account uses ("email" | "phrase"). func encodeDeleteRequestResult(method string) []byte { b := flatbuffers.NewBuilder(32) m := b.CreateString(method) fb.AccountDeleteRequestResultStart(b) fb.AccountDeleteRequestResultAddMethod(b, m) b.Finish(fb.AccountDeleteRequestResultEnd(b)) return b.FinishedBytes() } // encodeConfirmLinkResult builds an EmailConfirmLinkResult payload, embedding the // minted Session for a login. All strings and the nested Session table are built // before the result table is opened. func encodeConfirmLinkResult(r backendclient.ConfirmLinkResp) []byte { b := flatbuffers.NewBuilder(160) purpose := b.CreateString(r.Purpose) status := b.CreateString(r.Status) var session flatbuffers.UOffsetT if r.Session != nil { token := b.CreateString(r.Session.Token) uid := b.CreateString(r.Session.UserID) name := b.CreateString(r.Session.DisplayName) fb.SessionStart(b) fb.SessionAddToken(b, token) fb.SessionAddUserId(b, uid) fb.SessionAddIsGuest(b, r.Session.IsGuest) fb.SessionAddDisplayName(b, name) session = fb.SessionEnd(b) } fb.EmailConfirmLinkResultStart(b) fb.EmailConfirmLinkResultAddPurpose(b, purpose) fb.EmailConfirmLinkResultAddStatus(b, status) if r.Session != nil { fb.EmailConfirmLinkResultAddSession(b, session) } b.Finish(fb.EmailConfirmLinkResultEnd(b)) return b.FinishedBytes() } // encodeProfile builds a Profile payload, including the advertising-banner block // when the backend marked the viewer eligible. // encodeWallet builds the Wallet payload: the visible chip segments and the context-applicable // benefits. Each WalletSegment table (and its source string) is built before the segments vector // is opened, per FlatBuffers' rule against a nested table while another is under construction. func encodeWallet(w backendclient.WalletResp) []byte { b := flatbuffers.NewBuilder(128) segs := make([]flatbuffers.UOffsetT, len(w.Segments)) for i, seg := range w.Segments { src := b.CreateString(seg.Source) fb.WalletSegmentStart(b) fb.WalletSegmentAddSource(b, src) fb.WalletSegmentAddChips(b, int32(seg.Chips)) fb.WalletSegmentAddSpendable(b, seg.Spendable) segs[i] = fb.WalletSegmentEnd(b) } fb.WalletStartSegmentsVector(b, len(segs)) for i := len(segs) - 1; i >= 0; i-- { b.PrependUOffsetT(segs[i]) } segVec := b.EndVector(len(segs)) fb.WalletStart(b) fb.WalletAddSegments(b, segVec) fb.WalletAddAdsForever(b, w.AdsForever) fb.WalletAddAdsPaidUntilMs(b, w.AdsPaidUntil) fb.WalletAddHints(b, int32(w.Hints)) fb.WalletAddRewardChips(b, int32(w.RewardChips)) b.Finish(fb.WalletEnd(b)) return b.FinishedBytes() } // encodeCatalog builds a Catalog payload: the storefront products for the caller's context, each // with its atom composition (built bottom-up — every product's atoms vector is finished before // its product table, and every product before the products vector). func encodeCatalog(cat backendclient.CatalogResp) []byte { b := flatbuffers.NewBuilder(256) prods := make([]flatbuffers.UOffsetT, len(cat.Products)) for i, p := range cat.Products { atoms := make([]flatbuffers.UOffsetT, len(p.Atoms)) for j, a := range p.Atoms { at := b.CreateString(a.AtomType) fb.CatalogAtomStart(b) fb.CatalogAtomAddAtomType(b, at) fb.CatalogAtomAddQuantity(b, int32(a.Quantity)) atoms[j] = fb.CatalogAtomEnd(b) } fb.CatalogProductStartAtomsVector(b, len(atoms)) for j := len(atoms) - 1; j >= 0; j-- { b.PrependUOffsetT(atoms[j]) } atomsVec := b.EndVector(len(atoms)) kind := b.CreateString(p.Kind) pid := b.CreateString(p.ProductID) title := b.CreateString(p.Title) cur := b.CreateString(p.MoneyCurrency) fb.CatalogProductStart(b) fb.CatalogProductAddKind(b, kind) fb.CatalogProductAddProductId(b, pid) fb.CatalogProductAddTitle(b, title) fb.CatalogProductAddChips(b, int32(p.Chips)) fb.CatalogProductAddMoneyAmount(b, p.MoneyAmount) fb.CatalogProductAddMoneyCurrency(b, cur) fb.CatalogProductAddAtoms(b, atomsVec) prods[i] = fb.CatalogProductEnd(b) } fb.CatalogStartProductsVector(b, len(prods)) for i := len(prods) - 1; i >= 0; i-- { b.PrependUOffsetT(prods[i]) } prodVec := b.EndVector(len(prods)) fb.CatalogStart(b) fb.CatalogAddProducts(b, prodVec) b.Finish(fb.CatalogEnd(b)) return b.FinishedBytes() } 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) email := b.CreateString(p.Email) // Build the banner table (and its children) before opening Profile: FlatBuffers // forbids a nested table while another is under construction. var banner flatbuffers.UOffsetT if p.Banner != nil { banner = encodeBanner(b, *p.Banner) } prefs := buildStringVector(b, p.VariantPreferences, fb.ProfileStartVariantPreferencesVector) dictVersions := encodeDictVersions(b, p.DictVersions) // The interstitial-ad config table (scalars only), built before Profile is opened. var ads flatbuffers.UOffsetT if p.Ads != nil { fb.AdsInfoStart(b) fb.AdsInfoAddCooldownGlobalS(b, int32(p.Ads.CooldownGlobalS)) fb.AdsInfoAddCooldownVsAiS(b, int32(p.Ads.CooldownVsAiS)) fb.AdsInfoAddCooldownHintS(b, int32(p.Ads.CooldownHintS)) fb.AdsInfoAddSuppressed(b, p.Ads.Suppressed) ads = fb.AdsInfoEnd(b) } 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) fb.ProfileAddVariantPreferences(b, prefs) fb.ProfileAddEmail(b, email) fb.ProfileAddTelegramLinked(b, p.TelegramLinked) fb.ProfileAddVkLinked(b, p.VkLinked) if dictVersions != 0 { fb.ProfileAddDictVersions(b, dictVersions) } if p.Banner != nil { fb.ProfileAddBanner(b, banner) } if p.Ads != nil { fb.ProfileAddAds(b, ads) } b.Finish(fb.ProfileEnd(b)) return b.FinishedBytes() } // encodeDictVersions builds the Profile's dict_versions vector — one DictVersion table per // variant/version pair — and returns its offset, or 0 when there are none (the field is then // omitted). Every child table and its strings are created before the vector is opened, per the // FlatBuffers rule against nesting a table under one still being built; the caller invokes it // before ProfileStart for the same reason. func encodeDictVersions(b *flatbuffers.Builder, dvs []backendclient.DictVersion) flatbuffers.UOffsetT { if len(dvs) == 0 { return 0 } offsets := make([]flatbuffers.UOffsetT, len(dvs)) for i, dv := range dvs { variant := b.CreateString(dv.Variant) version := b.CreateString(dv.Version) fb.DictVersionStart(b) fb.DictVersionAddVariant(b, variant) fb.DictVersionAddVersion(b, version) offsets[i] = fb.DictVersionEnd(b) } fb.ProfileStartDictVersionsVector(b, len(offsets)) for i := len(offsets) - 1; i >= 0; i-- { b.PrependUOffsetT(offsets[i]) } return b.EndVector(len(offsets)) } // optString creates a FlatBuffers string for a non-empty value, or 0 to omit the // optional field (the client then reads it as absent). func optString(b *flatbuffers.Builder, s string) flatbuffers.UOffsetT { if s == "" { return 0 } return b.CreateString(s) } // addOptString adds an optional string slot only when it was created (a 0 offset // leaves the field absent). add is the generated BannerCampaignAdd* setter. func addOptString(b *flatbuffers.Builder, add func(*flatbuffers.Builder, flatbuffers.UOffsetT), off flatbuffers.UOffsetT) { if off != 0 { add(b, off) } } // encodeBanner builds a BannerInfo table from the resolved banner block and // returns its offset. It is bottom-up: each campaign's messages vector and table // are built first, then the campaigns vector, then the BannerInfo table. The // caller must invoke it with no table under construction. func encodeBanner(b *flatbuffers.Builder, banner backendclient.BannerResp) flatbuffers.UOffsetT { campOffsets := make([]flatbuffers.UOffsetT, len(banner.Campaigns)) for i, c := range banner.Campaigns { msgOffsets := make([]flatbuffers.UOffsetT, len(c.Messages)) for j, m := range c.Messages { msgOffsets[j] = b.CreateString(m) } fb.BannerCampaignStartMessagesVector(b, len(msgOffsets)) for j := len(msgOffsets) - 1; j >= 0; j-- { b.PrependUOffsetT(msgOffsets[j]) } msgs := b.EndVector(len(msgOffsets)) // The optional colour strings must be created before the table starts; an // empty colour is omitted (offset 0), so the client reads it as absent. obg := optString(b, c.OverrideBg) ofg := optString(b, c.OverrideFg) olink := optString(b, c.OverrideLink) obgD := optString(b, c.OverrideBgDark) ofgD := optString(b, c.OverrideFgDark) olinkD := optString(b, c.OverrideLinkDark) fb.BannerCampaignStart(b) fb.BannerCampaignAddWeight(b, int32(c.Weight)) fb.BannerCampaignAddMessages(b, msgs) addOptString(b, fb.BannerCampaignAddOverrideBg, obg) addOptString(b, fb.BannerCampaignAddOverrideFg, ofg) addOptString(b, fb.BannerCampaignAddOverrideLink, olink) addOptString(b, fb.BannerCampaignAddOverrideBgDark, obgD) addOptString(b, fb.BannerCampaignAddOverrideFgDark, ofgD) addOptString(b, fb.BannerCampaignAddOverrideLinkDark, olinkD) campOffsets[i] = fb.BannerCampaignEnd(b) } fb.BannerInfoStartCampaignsVector(b, len(campOffsets)) for i := len(campOffsets) - 1; i >= 0; i-- { b.PrependUOffsetT(campOffsets[i]) } camps := b.EndVector(len(campOffsets)) t := banner.Timings fb.BannerInfoStart(b) fb.BannerInfoAddCampaigns(b, camps) fb.BannerInfoAddHoldMs(b, int32(t.HoldMs)) fb.BannerInfoAddEdgePauseMs(b, int32(t.EdgePauseMs)) fb.BannerInfoAddScrollPxPerSec(b, int32(t.ScrollPxPerSec)) fb.BannerInfoAddFadeOutMs(b, int32(t.FadeOutMs)) fb.BannerInfoAddGapMs(b, int32(t.GapMs)) fb.BannerInfoAddFadeInMs(b, int32(t.FadeInMs)) return fb.BannerInfoEnd(b) } // encodeBlockStatus builds a BlockStatus payload. func encodeBlockStatus(s backendclient.BlockStatusResp) []byte { b := flatbuffers.NewBuilder(128) until := b.CreateString(s.Until) reason := b.CreateString(s.Reason) fb.BlockStatusStart(b) fb.BlockStatusAddBlocked(b, s.Blocked) fb.BlockStatusAddPermanent(b, s.Permanent) fb.BlockStatusAddUntil(b, until) fb.BlockStatusAddReason(b, reason) b.Finish(fb.BlockStatusEnd(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. 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) 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, WalletBalance: s.WalletBalance, HintUnlockLeftSeconds: s.HintUnlockLeftSeconds, Alphabet: alphabet, } } // encodeMatch builds a MatchResult payload. func encodeMatch(m backendclient.MatchResp) []byte { b := flatbuffers.NewBuilder(512) // Enqueue always lands the caller in a game; an open game awaiting an opponent reports // matched=false but still carries it, so encode the game whenever it is present (else the // client never receives it and cannot navigate in). hasGame := m.Game != nil var game flatbuffers.UOffsetT if hasGame { game = buildGameView(b, *m.Game) } fb.MatchResultStart(b) fb.MatchResultAddMatched(b, m.Matched) if hasGame { 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)) fb.HintResultAddWalletBalance(b, int32(r.WalletBalance)) 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) dir := b.CreateString(r.Dir) fb.EvalResultStart(b) fb.EvalResultAddLegal(b, r.Legal) fb.EvalResultAddScore(b, int32(r.Score)) fb.EvalResultAddWords(b, words) fb.EvalResultAddDir(b, dir) 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) fb.GameListAddAtGameLimit(b, r.AtGameLimit) 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() } // encodeFeedbackState builds a FeedbackState payload, nesting the reply when present. func encodeFeedbackState(st backendclient.FeedbackStateResp) []byte { b := flatbuffers.NewBuilder(128) var reply flatbuffers.UOffsetT if st.Reply != nil { body := b.CreateString(st.Reply.Body) fb.FeedbackReplyStart(b) fb.FeedbackReplyAddBody(b, body) fb.FeedbackReplyAddRepliedAtUnix(b, st.Reply.RepliedAtUnix) reply = fb.FeedbackReplyEnd(b) } reason := b.CreateString(st.BlockedReason) fb.FeedbackStateStart(b) fb.FeedbackStateAddCanSend(b, st.CanSend) fb.FeedbackStateAddBlockedReason(b, reason) if st.Reply != nil { fb.FeedbackStateAddReply(b, reply) } b.Finish(fb.FeedbackStateEnd(b)) return b.FinishedBytes() } // encodeFeedbackUnread builds a FeedbackUnread payload. func encodeFeedbackUnread(u backendclient.FeedbackUnreadResp) []byte { b := flatbuffers.NewBuilder(16) fb.FeedbackUnreadStart(b) fb.FeedbackUnreadAddReplyUnread(b, u.ReplyUnread) b.Finish(fb.FeedbackUnreadEnd(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, MultipleWordsPerTurn: g.MultipleWordsPerTurn, VsAI: g.VsAI, MoveCount: g.MoveCount, EndReason: g.EndReason, Seats: seats, LastActivityUnix: g.LastActivityUnix, UnreadChat: g.UnreadChat, UnreadMessages: g.UnreadMessages, } } // 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)) }