9471341a0e
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 29s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m17s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m49s
A deleted account keeps its seats in every shared game, so its opponents
still saw the add-friend and block controls on the scoreboard (deletion
drops the friendship, so the 🤝 even reappeared for a former friend) and
a chat composer nobody was behind. A friend request sent that way was
accepted by the server and stayed pending forever.
The per-viewer game views now mark such a seat (SeatView.deleted,
resolved beside the seat display names by a batch accounts.deleted_at
lookup) and the client hides every control aimed at it: add-friend,
block, and the chat composer (message + nudge) once no reachable
opponent is left. Live events carry the game domain's seat standings and
so leave the mark unset, so the delta reducers preserve the cached one.
SendFriendRequest and Block against a tombstone are refused with
social.ErrAccountDeleted (410 account_deleted) — the source of truth for
an older client.
324 lines
11 KiB
Go
324 lines
11 KiB
Go
// 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
|
|
// Deleted marks a seat whose account has been deleted (tombstoned): the client hides
|
|
// every social control aimed at it — add-friend, block, chat and nudge.
|
|
Deleted bool
|
|
}
|
|
|
|
// 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
|
|
MultipleWordsPerTurn bool
|
|
MoveCount int
|
|
EndReason string
|
|
Seats []SeatView
|
|
LastActivityUnix int64
|
|
// VsAI marks an honest-AI game (the opponent is shown as 🤖, chat/nudge/add-friend off).
|
|
VsAI bool
|
|
// UnreadChat is a per-viewer flag: the requesting player has unread chat in this game.
|
|
UnreadChat bool
|
|
// UnreadMessages is a per-viewer flag: at least one unread entry is a real message (not just
|
|
// a nudge), so the badge can be coloured apart from the nudge-only case.
|
|
UnreadMessages bool
|
|
// Kind is the game's origin for the active-game limits: 0 unknown (a pre-existing game), 1 vs_ai,
|
|
// 2 random, 3 friends. The lobby counts active games per kind to lock a capped New-Game start.
|
|
Kind int
|
|
}
|
|
|
|
// 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
|
|
HintUnlockLeftSeconds 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
|
|
MultipleWordsPerTurn bool
|
|
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)
|
|
fb.SeatViewAddDeleted(b, s.Deleted)
|
|
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.GameViewAddMultipleWordsPerTurn(b, g.MultipleWordsPerTurn)
|
|
fb.GameViewAddMoveCount(b, int32(g.MoveCount))
|
|
fb.GameViewAddEndReason(b, endReason)
|
|
fb.GameViewAddSeats(b, seats)
|
|
fb.GameViewAddLastActivityUnix(b, g.LastActivityUnix)
|
|
fb.GameViewAddVsAi(b, g.VsAI)
|
|
fb.GameViewAddUnreadChat(b, g.UnreadChat)
|
|
fb.GameViewAddUnreadMessages(b, g.UnreadMessages)
|
|
fb.GameViewAddKind(b, int32(g.Kind))
|
|
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))
|
|
fb.StateViewAddHintUnlockLeftSeconds(b, int32(s.HintUnlockLeftSeconds))
|
|
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.InvitationAddMultipleWordsPerTurn(b, inv.MultipleWordsPerTurn)
|
|
fb.InvitationAddDropoutTiles(b, dropout)
|
|
fb.InvitationAddStatus(b, status)
|
|
fb.InvitationAddGameId(b, gameID)
|
|
fb.InvitationAddExpiresAtUnix(b, inv.ExpiresAtUnix)
|
|
return fb.InvitationEnd(b)
|
|
}
|