diff --git a/backend/internal/notify/encode.go b/backend/internal/notify/encode.go index d455b5f..47a4fae 100644 --- a/backend/internal/notify/encode.go +++ b/backend/internal/notify/encode.go @@ -4,194 +4,114 @@ import ( flatbuffers "github.com/google/flatbuffers/go" "scrabble/backend/internal/engine" - fb "scrabble/pkg/fbs/scrabblefb" + "scrabble/pkg/wire" ) // The builders below encode the nested wire tables embedded in enriched event -// payloads. 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. +// payloads. They map the domain's already-resolved values (notify.* payload structs +// and the decoded engine.MoveRecord) to the neutral scrabble/pkg/wire structs and +// delegate the FlatBuffers construction to package wire — the single definition of the +// nested-table layout shared with the gateway transcoder. Each returns the offset of +// the table it built; callers must build every nested table before opening the parent. + +// toWireGame maps a GameSummary to the shared wire.GameView. +func toWireGame(g GameSummary) 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, + } +} // 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) + return wire.BuildGameView(b, toWireGame(g)) } -// 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). +// buildMoveRecord builds a MoveRecord table from a decoded engine move and returns its +// offset (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)) + tiles := make([]wire.TileRecord, 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) + tiles[i] = wire.TileRecord{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank} } - 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)) + return wire.BuildMoveRecord(b, wire.MoveRecord{ + Player: m.Player, + Action: m.Action.String(), + Dir: m.Dir.String(), + MainRow: m.MainRow, + MainCol: m.MainCol, + Tiles: tiles, + Words: m.Words, + Count: m.Count, + Score: m.Score, + Total: m.Total, + }) } // 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) + 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} } - 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) + return wire.BuildStateView(b, wire.StateView{ + Game: toWireGame(s.Game), + Seat: s.Seat, + Rack: s.Rack, + BagLen: s.BagLen, + HintsRemaining: s.HintsRemaining, + Alphabet: alphabet, + }) } // 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) + return wire.BuildAccountRef(b, wire.AccountRef{AccountID: a.AccountID, DisplayName: a.DisplayName}) } // 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)) + invitees := make([]wire.InvitationInvitee, 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) + invitees[i] = wire.InvitationInvitee{ + AccountID: iv.AccountID, + DisplayName: iv.DisplayName, + Seat: iv.Seat, + Response: iv.Response, + } } - 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) + return wire.BuildInvitation(b, wire.Invitation{ + ID: inv.ID, + Inviter: wire.AccountRef{AccountID: inv.Inviter.AccountID, DisplayName: inv.Inviter.DisplayName}, + Invitees: invitees, + Variant: inv.Variant, + TurnTimeoutSecs: inv.TurnTimeoutSecs, + HintsAllowed: inv.HintsAllowed, + HintsPerPlayer: inv.HintsPerPlayer, + DropoutTiles: inv.DropoutTiles, + Status: inv.Status, + GameID: inv.GameID, + ExpiresAtUnix: inv.ExpiresAtUnix, + }) } diff --git a/gateway/internal/transcode/encode.go b/gateway/internal/transcode/encode.go index f87e551..584a925 100644 --- a/gateway/internal/transcode/encode.go +++ b/gateway/internal/transcode/encode.go @@ -5,6 +5,7 @@ import ( "scrabble/gateway/internal/backendclient" fb "scrabble/pkg/fbs/scrabblefb" + "scrabble/pkg/wire" ) // The encoders build the FlatBuffers response payloads from the backend's typed @@ -142,46 +143,24 @@ func encodeMoveResult(r backendclient.MoveResultResp) []byte { // 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)) + b.Finish(wire.BuildStateView(b, toWireState(s))) 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) +// 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} } - fb.StateViewStartAlphabetVector(b, len(offs)) - for i := len(offs) - 1; i >= 0; i-- { - b.PrependUOffsetT(offs[i]) + return wire.StateView{ + Game: toWireGame(s.Game), + Seat: s.Seat, + Rack: s.Rack, + BagLen: s.BagLen, + HintsRemaining: s.HintsRemaining, + Alphabet: alphabet, } - return b.EndVector(len(offs)) } // encodeMatch builds a MatchResult payload. @@ -328,80 +307,55 @@ func encodeChatList(r backendclient.ChatListResp) []byte { // 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)) + 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 { - 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) + seats[i] = wire.SeatView{ + Seat: s.Seat, + AccountID: s.AccountID, + Score: s.Score, + HintsUsed: s.HintsUsed, + IsWinner: s.IsWinner, + DisplayName: s.DisplayName, + } } - fb.GameViewStartSeatsVector(b, len(seatOffs)) - for i := len(seatOffs) - 1; i >= 0; i-- { - b.PrependUOffsetT(seatOffs[i]) + 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, } - 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 and returns its offset. func buildMoveRecord(b *flatbuffers.Builder, m backendclient.MoveRecordResp) flatbuffers.UOffsetT { - tileOffs := make([]flatbuffers.UOffsetT, len(m.Tiles)) + tiles := make([]wire.TileRecord, 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) + tiles[i] = wire.TileRecord{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank} } - 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) + 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 diff --git a/gateway/internal/transcode/encode_social.go b/gateway/internal/transcode/encode_social.go index 4c976de..eabcbe8 100644 --- a/gateway/internal/transcode/encode_social.go +++ b/gateway/internal/transcode/encode_social.go @@ -5,6 +5,7 @@ import ( "scrabble/gateway/internal/backendclient" fb "scrabble/pkg/fbs/scrabblefb" + "scrabble/pkg/wire" ) // Social encoders: friends, blocks, invitations, statistics and GCG. They follow @@ -12,12 +13,7 @@ import ( // buildAccountRef builds an AccountRef table and returns its offset. func buildAccountRef(b *flatbuffers.Builder, r backendclient.AccountRefResp) flatbuffers.UOffsetT { - id := b.CreateString(r.AccountID) - name := b.CreateString(r.DisplayName) - fb.AccountRefStart(b) - fb.AccountRefAddAccountId(b, id) - fb.AccountRefAddDisplayName(b, name) - return fb.AccountRefEnd(b) + return wire.BuildAccountRef(b, wire.AccountRef{AccountID: r.AccountID, DisplayName: r.DisplayName}) } // buildAccountRefVector builds a [AccountRef] vector using the table-specific @@ -110,43 +106,28 @@ func encodeStats(r backendclient.StatsResp) []byte { // buildInvitation builds an Invitation table and returns its offset. func buildInvitation(b *flatbuffers.Builder, inv backendclient.InvitationResp) flatbuffers.UOffsetT { - inviteeOffs := make([]flatbuffers.UOffsetT, len(inv.Invitees)) + invitees := make([]wire.InvitationInvitee, 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) + invitees[i] = wire.InvitationInvitee{ + AccountID: iv.AccountID, + DisplayName: iv.DisplayName, + Seat: iv.Seat, + Response: iv.Response, + } } - 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) + return wire.BuildInvitation(b, wire.Invitation{ + ID: inv.ID, + Inviter: wire.AccountRef{AccountID: inv.Inviter.AccountID, DisplayName: inv.Inviter.DisplayName}, + Invitees: invitees, + Variant: inv.Variant, + TurnTimeoutSecs: inv.TurnTimeoutSecs, + HintsAllowed: inv.HintsAllowed, + HintsPerPlayer: inv.HintsPerPlayer, + DropoutTiles: inv.DropoutTiles, + Status: inv.Status, + GameID: inv.GameID, + ExpiresAtUnix: inv.ExpiresAtUnix, + }) } // encodeInvitation builds an Invitation payload. diff --git a/pkg/wire/build.go b/pkg/wire/build.go new file mode 100644 index 0000000..8837a49 --- /dev/null +++ b/pkg/wire/build.go @@ -0,0 +1,299 @@ +// 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) +}