d7337d24ea
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m16s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m48s
Russian "Эрудит" treats a word laid on the board as belonging to the game: it cannot be laid again. Neither the solver, the backend nor the offline JS port knew the rule, so a player (and the robot) could replay a word freely. Official Scrabble places no such restriction, so both Scrabble variants keep playing unrestricted. The rule applies in two ways. A play whose main word is already on the board is illegal, and is neither accepted nor generated. A play whose perpendicular cross-word is already there stands — that word is incidental to laying the main word — but scores nothing. The set of played words is the game's own move journal, main words and cross-words alike, compared decoded, so a word spelled with a blank is the same word. It lives in the game layer, not the solver: only a game knows its history, and the solver stays stateless and standard-rules. The backend applies it at submit, at the move preview and over generated moves (filtering and re-ranking them, so neither the robot nor the hint can offer a play the engine would then refuse); the client port does the same for the offline engine and for the on-device preview of an online game. The rule is pinned per game (games.no_repeat_words, set from the variant at creation) rather than keyed on the variant, because a game is replayed from its journal on every open. Applied retroactively it would make an already-played repeat illegal — closing that game as a draw — and would rescore a play whose cross-word repeats an earlier word, shifting a live game's totals. Games created before the rule keep playing without it. The flag rides the wire as a trailing field because the client's preview must score the way the server does, and offline games pin the same answer in their own record.
678 lines
23 KiB
Go
678 lines
23 KiB
Go
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)
|
|
}
|
|
// The active-game limits table (scalars only), built before Profile is opened.
|
|
var gameLimits flatbuffers.UOffsetT
|
|
if p.GameLimits != nil {
|
|
fb.GameLimitsStart(b)
|
|
fb.GameLimitsAddVsAi(b, int32(p.GameLimits.VsAI))
|
|
fb.GameLimitsAddRandom(b, int32(p.GameLimits.Random))
|
|
fb.GameLimitsAddFriends(b, int32(p.GameLimits.Friends))
|
|
gameLimits = fb.GameLimitsEnd(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)
|
|
}
|
|
if p.GameLimits != nil {
|
|
fb.ProfileAddGameLimits(b, gameLimits)
|
|
}
|
|
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,
|
|
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,
|
|
Deleted: s.Deleted,
|
|
}
|
|
}
|
|
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,
|
|
Kind: g.Kind,
|
|
NoRepeatWords: g.NoRepeatWords,
|
|
}
|
|
}
|
|
|
|
// 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))
|
|
}
|