package notify import ( flatbuffers "github.com/google/flatbuffers/go" "scrabble/backend/internal/engine" fb "scrabble/pkg/fbs/scrabblefb" ) // The builders below encode the nested wire tables embedded in enriched event // payloads (R4). They mirror the gateway's transcode encoders, but read the domain's // already-resolved values (notify.* input structs and the decoded engine.MoveRecord) // rather than the gateway's REST DTOs. Each returns the offset of the table it built; // callers must build every nested table before opening the parent event table. // buildGameView builds a GameView table from a GameSummary and returns its offset. func buildGameView(b *flatbuffers.Builder, g GameSummary) 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 a decoded engine move and returns // its offset. The values match the move-result DTO (Count is the engine count: the // number of tiles swapped on an exchange, zero otherwise). func buildMoveRecord(b *flatbuffers.Builder, m engine.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.String()) dir := b.CreateString(m.Dir.String()) 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) } // buildAlphabet builds the AlphabetEntry vector embedded in a StateView and returns // its offset. func buildAlphabet(b *flatbuffers.Builder, entries []AlphabetLetter) 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 a PlayerState and returns its offset. func buildStateView(b *flatbuffers.Builder, s PlayerState) 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 = 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) } return fb.StateViewEnd(b) } // buildAccountRef builds an AccountRef table 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 an InvitationSummary and returns its offset. func buildInvitation(b *flatbuffers.Builder, inv InvitationSummary) flatbuffers.UOffsetT { inviter := buildAccountRef(b, inv.Inviter) 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)) 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) }