// Package wire holds the FlatBuffers builders for the nested wire tables that both // the backend's push-event encoder (backend/internal/notify) and the gateway's edge // transcoder (gateway/internal/transcode) emit. The two callers resolve their own // source types (the backend's domain payloads, the gateway's REST DTOs) into the // neutral structs below and delegate the actual FlatBuffers construction here, so the // shared tables (GameView, MoveRecord, StateView, AccountRef, Invitation) have a // single encoding definition that cannot drift between the two paths. // // 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. // Each builder returns the offset of the table (or vector) it built; the caller embeds // that offset in a parent table or finishes the buffer with it. package wire import ( flatbuffers "github.com/google/flatbuffers/go" fb "scrabble/pkg/fbs/scrabblefb" ) // SeatView is one seat's public standing in a GameView. type SeatView struct { Seat int AccountID string Score int HintsUsed int IsWinner bool DisplayName string } // GameView is the shared, non-private game summary. type GameView struct { ID string Variant string DictVersion string Status string Players int ToMove int TurnTimeoutSecs int MoveCount int EndReason string Seats []SeatView LastActivityUnix int64 } // TileRecord is one tile in a decoded MoveRecord (the concrete letter, "?" for a blank // read from a hand). type TileRecord struct { Row int Col int Letter string Blank bool } // MoveRecord is one decoded move (a committed play, a hint preview). Action and Dir are // the already-stringified move kind and direction. type MoveRecord struct { Player int Action string Dir string MainRow int MainCol int Tiles []TileRecord Words []string Count int Score int Total int } // AlphabetEntry is one letter of a variant's alphabet (Index is the wire alphabet-index // byte; a value of 255 is the blank sentinel elsewhere). type AlphabetEntry struct { Index int Letter string Value int } // StateView is a player's view of a game: the shared summary plus their private rack // (wire alphabet indices), bag size and hint budget. Alphabet is set only when the // recipient may not have cached the variant's display table yet. type StateView struct { Game GameView Seat int Rack []int BagLen int HintsRemaining int Alphabet []AlphabetEntry } // AccountRef is a referenced account with its display name resolved. type AccountRef struct { AccountID string DisplayName string } // InvitationInvitee is one invited player's seat and response inside an Invitation. type InvitationInvitee struct { AccountID string DisplayName string Seat int Response string } // Invitation is a friend-game invitation with its settings and invitees. type Invitation struct { ID string Inviter AccountRef Invitees []InvitationInvitee Variant string TurnTimeoutSecs int HintsAllowed bool HintsPerPlayer int DropoutTiles string Status string GameID string ExpiresAtUnix int64 } // BuildGameView builds a GameView table from g and returns its offset. func BuildGameView(b *flatbuffers.Builder, g GameView) 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) fb.GameViewAddLastActivityUnix(b, g.LastActivityUnix) return fb.GameViewEnd(b) } // BuildMoveRecord builds a MoveRecord table from m and returns its offset. func BuildMoveRecord(b *flatbuffers.Builder, m MoveRecord) 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)) wordOffs := make([]flatbuffers.UOffsetT, len(m.Words)) for i, w := range m.Words { wordOffs[i] = b.CreateString(w) } fb.MoveRecordStartWordsVector(b, len(wordOffs)) for i := len(wordOffs) - 1; i >= 0; i-- { b.PrependUOffsetT(wordOffs[i]) } words := b.EndVector(len(wordOffs)) 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) } // BuildStateViewAlphabet builds the AlphabetEntry vector embedded in a StateView and // returns its offset. func BuildStateViewAlphabet(b *flatbuffers.Builder, entries []AlphabetEntry) 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)) } // BuildStateView builds a StateView table from s and returns its offset. The alphabet // table is embedded only when s.Alphabet is non-empty. func BuildStateView(b *flatbuffers.Builder, s StateView) flatbuffers.UOffsetT { 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 = BuildStateViewAlphabet(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) } return fb.StateViewEnd(b) } // BuildAccountRef builds an AccountRef table from a and returns its offset. func BuildAccountRef(b *flatbuffers.Builder, a AccountRef) flatbuffers.UOffsetT { aid := b.CreateString(a.AccountID) name := b.CreateString(a.DisplayName) fb.AccountRefStart(b) fb.AccountRefAddAccountId(b, aid) fb.AccountRefAddDisplayName(b, name) return fb.AccountRefEnd(b) } // BuildInvitation builds an Invitation table from inv and returns its offset. func BuildInvitation(b *flatbuffers.Builder, inv Invitation) flatbuffers.UOffsetT { inviteeOffs := make([]flatbuffers.UOffsetT, len(inv.Invitees)) for i, iv := range inv.Invitees { aid := b.CreateString(iv.AccountID) name := b.CreateString(iv.DisplayName) resp := b.CreateString(iv.Response) fb.InvitationInviteeStart(b) fb.InvitationInviteeAddAccountId(b, aid) fb.InvitationInviteeAddDisplayName(b, name) fb.InvitationInviteeAddSeat(b, int32(iv.Seat)) fb.InvitationInviteeAddResponse(b, resp) inviteeOffs[i] = fb.InvitationInviteeEnd(b) } fb.InvitationStartInviteesVector(b, len(inviteeOffs)) for i := len(inviteeOffs) - 1; i >= 0; i-- { b.PrependUOffsetT(inviteeOffs[i]) } invitees := b.EndVector(len(inviteeOffs)) inviter := BuildAccountRef(b, inv.Inviter) id := b.CreateString(inv.ID) variant := b.CreateString(inv.Variant) dropout := b.CreateString(inv.DropoutTiles) status := b.CreateString(inv.Status) gameID := b.CreateString(inv.GameID) fb.InvitationStart(b) fb.InvitationAddId(b, id) fb.InvitationAddInviter(b, inviter) fb.InvitationAddInvitees(b, invitees) fb.InvitationAddVariant(b, variant) fb.InvitationAddTurnTimeoutSecs(b, int32(inv.TurnTimeoutSecs)) fb.InvitationAddHintsAllowed(b, inv.HintsAllowed) fb.InvitationAddHintsPerPlayer(b, int32(inv.HintsPerPlayer)) fb.InvitationAddDropoutTiles(b, dropout) fb.InvitationAddStatus(b, status) fb.InvitationAddGameId(b, gameID) fb.InvitationAddExpiresAtUnix(b, inv.ExpiresAtUnix) return fb.InvitationEnd(b) }