936a70ab94
Replace the disabled "Soon" pack action with a real purchase: the Wallet opens a money order (wallet.order) and sends the player to the provider's hosted-payment page (window.open via openExternalUrl); the chips are credited later by the verified server callback. Add a public-offer link under the packs (paying accepts the offer). Codec order round-trip unit test + a mock-e2e purchase test; the Google Play stub and the chip-spend paths are unchanged.
1122 lines
38 KiB
TypeScript
1122 lines
38 KiB
TypeScript
// FlatBuffers <-> domain-model codec. The real transport encodes each request table
|
|
// and decodes each response/event table here, mirroring the gateway's Go encoders in
|
|
// reverse. The screens only ever see the plain model (lib/model), never these wire
|
|
// types.
|
|
|
|
import { Builder, ByteBuffer, type Offset } from 'flatbuffers';
|
|
import * as fb from '../gen/fbs/scrabblefb';
|
|
import { indexForLetter, letterForIndex, setAlphabet, type AlphabetEntryWire } from './alphabet';
|
|
import type { PlacedTile } from './client';
|
|
import type {
|
|
AccountRef,
|
|
BlockList,
|
|
OutgoingList,
|
|
RobotBlockEntry,
|
|
RobotFriendRequestEntry,
|
|
Banner,
|
|
BannerCampaign,
|
|
BestMove,
|
|
BestMoveTile,
|
|
BlockStatus,
|
|
ChatMessage,
|
|
ColorTriple,
|
|
EvalResult,
|
|
FeedbackState,
|
|
FriendCode,
|
|
ExportUrl,
|
|
GameList,
|
|
GameView,
|
|
GcgExport,
|
|
History,
|
|
HintResult,
|
|
Invitation,
|
|
InvitationInvitee,
|
|
InvitationSettings,
|
|
ConfirmLinkResult,
|
|
LinkResult,
|
|
MatchResult,
|
|
MoveRecord,
|
|
MoveResult,
|
|
Profile,
|
|
Wallet,
|
|
WalletOrder,
|
|
WalletSegment,
|
|
Catalog,
|
|
CatalogProduct,
|
|
CatalogAtom,
|
|
ProfileUpdate,
|
|
PushEvent,
|
|
Seat,
|
|
Session,
|
|
StateView,
|
|
Stats,
|
|
Tile,
|
|
Variant,
|
|
WordCheckResult,
|
|
} from './model';
|
|
|
|
// --- request encoders ---
|
|
|
|
// buildPlayTile encodes one to-place tile by its alphabet index; a placed blank
|
|
// carries its designated letter's index with blank set.
|
|
function buildPlayTile(b: Builder, t: PlacedTile, variant: Variant): Offset {
|
|
fb.PlayTile.startPlayTile(b);
|
|
fb.PlayTile.addRow(b, t.row);
|
|
fb.PlayTile.addCol(b, t.col);
|
|
fb.PlayTile.addLetter(b, indexForLetter(variant, t.letter));
|
|
fb.PlayTile.addBlank(b, t.blank);
|
|
return fb.PlayTile.endPlayTile(b);
|
|
}
|
|
|
|
function finish(b: Builder, root: Offset): Uint8Array {
|
|
b.finish(root);
|
|
return b.asUint8Array();
|
|
}
|
|
|
|
export const empty = (): Uint8Array => new Uint8Array();
|
|
|
|
export function encodeGameAction(gameId: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const gid = b.createString(gameId);
|
|
fb.GameActionRequest.startGameActionRequest(b);
|
|
fb.GameActionRequest.addGameId(b, gid);
|
|
return finish(b, fb.GameActionRequest.endGameActionRequest(b));
|
|
}
|
|
|
|
export function encodeStateRequest(gameId: string, includeAlphabet: boolean): Uint8Array {
|
|
const b = new Builder(64);
|
|
const gid = b.createString(gameId);
|
|
fb.StateRequest.startStateRequest(b);
|
|
fb.StateRequest.addGameId(b, gid);
|
|
fb.StateRequest.addIncludeAlphabet(b, includeAlphabet);
|
|
return finish(b, fb.StateRequest.endStateRequest(b));
|
|
}
|
|
|
|
// encodeDraftSave wraps the player's composition JSON. The string is opaque on the
|
|
// wire — the gateway forwards it verbatim and only the client reads {rack_order, board_tiles}.
|
|
export function encodeDraftSave(gameId: string, json: string): Uint8Array {
|
|
const b = new Builder(256);
|
|
const gid = b.createString(gameId);
|
|
const j = b.createString(json);
|
|
fb.DraftRequest.startDraftRequest(b);
|
|
fb.DraftRequest.addGameId(b, gid);
|
|
fb.DraftRequest.addJson(b, j);
|
|
return finish(b, fb.DraftRequest.endDraftRequest(b));
|
|
}
|
|
|
|
export function encodeSubmitPlay(
|
|
gameId: string,
|
|
tiles: PlacedTile[],
|
|
variant: Variant,
|
|
): Uint8Array {
|
|
const b = new Builder(256);
|
|
const tileOffs = tiles.map((t) => buildPlayTile(b, t, variant));
|
|
const vec = fb.SubmitPlayRequest.createTilesVector(b, tileOffs);
|
|
const gid = b.createString(gameId);
|
|
fb.SubmitPlayRequest.startSubmitPlayRequest(b);
|
|
fb.SubmitPlayRequest.addGameId(b, gid);
|
|
fb.SubmitPlayRequest.addTiles(b, vec);
|
|
return finish(b, fb.SubmitPlayRequest.endSubmitPlayRequest(b));
|
|
}
|
|
|
|
export function encodeEval(gameId: string, tiles: PlacedTile[], variant: Variant): Uint8Array {
|
|
const b = new Builder(256);
|
|
const tileOffs = tiles.map((t) => buildPlayTile(b, t, variant));
|
|
const vec = fb.EvalRequest.createTilesVector(b, tileOffs);
|
|
const gid = b.createString(gameId);
|
|
fb.EvalRequest.startEvalRequest(b);
|
|
fb.EvalRequest.addGameId(b, gid);
|
|
fb.EvalRequest.addTiles(b, vec);
|
|
return finish(b, fb.EvalRequest.endEvalRequest(b));
|
|
}
|
|
|
|
export function encodeExchange(gameId: string, tiles: string[], variant: Variant): Uint8Array {
|
|
const b = new Builder(128);
|
|
const vec = fb.ExchangeRequest.createTilesVector(
|
|
b,
|
|
tiles.map((l) => indexForLetter(variant, l)),
|
|
);
|
|
const gid = b.createString(gameId);
|
|
fb.ExchangeRequest.startExchangeRequest(b);
|
|
fb.ExchangeRequest.addGameId(b, gid);
|
|
fb.ExchangeRequest.addTiles(b, vec);
|
|
return finish(b, fb.ExchangeRequest.endExchangeRequest(b));
|
|
}
|
|
|
|
export function encodeCheckWord(gameId: string, word: string, variant: Variant): Uint8Array {
|
|
const b = new Builder(128);
|
|
const vec = fb.CheckWordRequest.createWordVector(
|
|
b,
|
|
Array.from(word).map((ch) => indexForLetter(variant, ch)),
|
|
);
|
|
const gid = b.createString(gameId);
|
|
fb.CheckWordRequest.startCheckWordRequest(b);
|
|
fb.CheckWordRequest.addGameId(b, gid);
|
|
fb.CheckWordRequest.addWord(b, vec);
|
|
return finish(b, fb.CheckWordRequest.endCheckWordRequest(b));
|
|
}
|
|
|
|
export function encodeComplaint(gameId: string, word: string, note: string): Uint8Array {
|
|
const b = new Builder(256);
|
|
const gid = b.createString(gameId);
|
|
const w = b.createString(word);
|
|
const n = b.createString(note);
|
|
fb.ComplaintRequest.startComplaintRequest(b);
|
|
fb.ComplaintRequest.addGameId(b, gid);
|
|
fb.ComplaintRequest.addWord(b, w);
|
|
fb.ComplaintRequest.addNote(b, n);
|
|
return finish(b, fb.ComplaintRequest.endComplaintRequest(b));
|
|
}
|
|
|
|
export function encodeEnqueue(variant: Variant, multipleWords: boolean, vsAi: boolean): Uint8Array {
|
|
const b = new Builder(64);
|
|
const v = b.createString(variant);
|
|
fb.EnqueueRequest.startEnqueueRequest(b);
|
|
fb.EnqueueRequest.addVariant(b, v);
|
|
fb.EnqueueRequest.addMultipleWordsPerTurn(b, multipleWords);
|
|
fb.EnqueueRequest.addVsAi(b, vsAi);
|
|
return finish(b, fb.EnqueueRequest.endEnqueueRequest(b));
|
|
}
|
|
|
|
export function encodeChatPost(gameId: string, body: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const gid = b.createString(gameId);
|
|
const bd = b.createString(body);
|
|
fb.ChatPostRequest.startChatPostRequest(b);
|
|
fb.ChatPostRequest.addGameId(b, gid);
|
|
fb.ChatPostRequest.addBody(b, bd);
|
|
return finish(b, fb.ChatPostRequest.endChatPostRequest(b));
|
|
}
|
|
|
|
export function encodeTelegramLogin(
|
|
initData: string,
|
|
browserTz: string,
|
|
subtype: string,
|
|
): Uint8Array {
|
|
const b = new Builder(512);
|
|
const d = b.createString(initData);
|
|
const tz = b.createString(browserTz);
|
|
const st = b.createString(subtype);
|
|
fb.TelegramLoginRequest.startTelegramLoginRequest(b);
|
|
fb.TelegramLoginRequest.addInitData(b, d);
|
|
fb.TelegramLoginRequest.addBrowserTz(b, tz);
|
|
fb.TelegramLoginRequest.addSubtype(b, st);
|
|
return finish(b, fb.TelegramLoginRequest.endTelegramLoginRequest(b));
|
|
}
|
|
|
|
export function encodeVKLogin(params: string, browserTz: string, displayName: string): Uint8Array {
|
|
const b = new Builder(512);
|
|
const p = b.createString(params);
|
|
const tz = b.createString(browserTz);
|
|
const dn = b.createString(displayName);
|
|
fb.VKLoginRequest.startVKLoginRequest(b);
|
|
fb.VKLoginRequest.addParams(b, p);
|
|
fb.VKLoginRequest.addBrowserTz(b, tz);
|
|
fb.VKLoginRequest.addDisplayName(b, dn);
|
|
return finish(b, fb.VKLoginRequest.endVKLoginRequest(b));
|
|
}
|
|
|
|
export function encodeGuestLogin(
|
|
locale: string,
|
|
browserTz: string,
|
|
subtype: string,
|
|
): Uint8Array {
|
|
const b = new Builder(64);
|
|
const l = b.createString(locale);
|
|
const tz = b.createString(browserTz);
|
|
const st = b.createString(subtype);
|
|
fb.GuestLoginRequest.startGuestLoginRequest(b);
|
|
fb.GuestLoginRequest.addLocale(b, l);
|
|
fb.GuestLoginRequest.addBrowserTz(b, tz);
|
|
fb.GuestLoginRequest.addSubtype(b, st);
|
|
return finish(b, fb.GuestLoginRequest.endGuestLoginRequest(b));
|
|
}
|
|
|
|
export function encodeEmailRequest(
|
|
email: string,
|
|
browserTz: string,
|
|
language: string,
|
|
pwa: boolean,
|
|
): Uint8Array {
|
|
const b = new Builder(128);
|
|
const e = b.createString(email);
|
|
const tz = b.createString(browserTz);
|
|
const l = b.createString(language);
|
|
fb.EmailRequestRequest.startEmailRequestRequest(b);
|
|
fb.EmailRequestRequest.addEmail(b, e);
|
|
fb.EmailRequestRequest.addBrowserTz(b, tz);
|
|
fb.EmailRequestRequest.addLanguage(b, l);
|
|
fb.EmailRequestRequest.addPwa(b, pwa);
|
|
return finish(b, fb.EmailRequestRequest.endEmailRequestRequest(b));
|
|
}
|
|
|
|
export function encodeEmailLogin(email: string, code: string, subtype: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const e = b.createString(email);
|
|
const c = b.createString(code);
|
|
const st = b.createString(subtype);
|
|
fb.EmailLoginRequest.startEmailLoginRequest(b);
|
|
fb.EmailLoginRequest.addEmail(b, e);
|
|
fb.EmailLoginRequest.addCode(b, c);
|
|
fb.EmailLoginRequest.addSubtype(b, st);
|
|
return finish(b, fb.EmailLoginRequest.endEmailLoginRequest(b));
|
|
}
|
|
|
|
export function encodeEmailConfirmLink(token: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const t = b.createString(token);
|
|
fb.EmailConfirmLinkRequest.startEmailConfirmLinkRequest(b);
|
|
fb.EmailConfirmLinkRequest.addToken(b, t);
|
|
return finish(b, fb.EmailConfirmLinkRequest.endEmailConfirmLinkRequest(b));
|
|
}
|
|
|
|
// --- response decoders ---
|
|
|
|
function s(v: string | null): string {
|
|
return v ?? '';
|
|
}
|
|
|
|
function decodeTile(t: fb.TileRecord): Tile {
|
|
// The wire keeps the move journal in the solver's lower case; the UI renders upper case.
|
|
return { row: t.row(), col: t.col(), letter: s(t.letter()).toUpperCase(), blank: t.blank() };
|
|
}
|
|
|
|
function decodeSeat(v: fb.SeatView): Seat {
|
|
return {
|
|
seat: v.seat(),
|
|
accountId: s(v.accountId()),
|
|
displayName: s(v.displayName()),
|
|
score: v.score(),
|
|
hintsUsed: v.hintsUsed(),
|
|
isWinner: v.isWinner(),
|
|
};
|
|
}
|
|
|
|
function decodeGameView(g: fb.GameView): GameView {
|
|
const seats: Seat[] = [];
|
|
for (let i = 0; i < g.seatsLength(); i++) {
|
|
const sv = g.seats(i);
|
|
if (sv) seats.push(decodeSeat(sv));
|
|
}
|
|
return {
|
|
id: s(g.id()),
|
|
variant: s(g.variant()) as Variant,
|
|
dictVersion: s(g.dictVersion()),
|
|
status: s(g.status()),
|
|
players: g.players(),
|
|
toMove: g.toMove(),
|
|
turnTimeoutSecs: g.turnTimeoutSecs(),
|
|
multipleWordsPerTurn: g.multipleWordsPerTurn(),
|
|
moveCount: g.moveCount(),
|
|
endReason: s(g.endReason()),
|
|
lastActivityUnix: Number(g.lastActivityUnix()),
|
|
vsAi: g.vsAi(),
|
|
unreadChat: g.unreadChat(),
|
|
unreadMessages: g.unreadMessages(),
|
|
seats,
|
|
};
|
|
}
|
|
|
|
function decodeMove(m: fb.MoveRecord): MoveRecord {
|
|
const tiles: Tile[] = [];
|
|
for (let i = 0; i < m.tilesLength(); i++) {
|
|
const t = m.tiles(i);
|
|
if (t) tiles.push(decodeTile(t));
|
|
}
|
|
const words: string[] = [];
|
|
for (let i = 0; i < m.wordsLength(); i++) words.push(s(m.words(i)).toUpperCase());
|
|
return {
|
|
player: m.player(),
|
|
action: s(m.action()),
|
|
dir: s(m.dir()),
|
|
mainRow: m.mainRow(),
|
|
mainCol: m.mainCol(),
|
|
tiles,
|
|
words,
|
|
count: m.count(),
|
|
score: m.score(),
|
|
total: m.total(),
|
|
};
|
|
}
|
|
|
|
function decodeChatMsg(m: fb.ChatMessage): ChatMessage {
|
|
return {
|
|
id: s(m.id()),
|
|
gameId: s(m.gameId()),
|
|
senderId: s(m.senderId()),
|
|
kind: s(m.kind()),
|
|
body: s(m.body()),
|
|
createdAtUnix: Number(m.createdAtUnix()),
|
|
};
|
|
}
|
|
|
|
// sessionFromTable projects a FlatBuffers Session table (a root or a nested one) to
|
|
// the Session model.
|
|
function sessionFromTable(t: fb.Session): Session {
|
|
return {
|
|
token: s(t.token()),
|
|
userId: s(t.userId()),
|
|
isGuest: t.isGuest(),
|
|
displayName: s(t.displayName()),
|
|
};
|
|
}
|
|
|
|
export function decodeSession(buf: Uint8Array): Session {
|
|
return sessionFromTable(fb.Session.getRootAsSession(new ByteBuffer(buf)));
|
|
}
|
|
|
|
// encodeWalletBuy wraps the product id for a chip spend (POST /user/wallet/buy).
|
|
export function encodeWalletBuy(productId: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const pid = b.createString(productId);
|
|
fb.WalletBuyRequest.startWalletBuyRequest(b);
|
|
fb.WalletBuyRequest.addProductId(b, pid);
|
|
return finish(b, fb.WalletBuyRequest.endWalletBuyRequest(b));
|
|
}
|
|
|
|
// encodeWalletOrder wraps the pack id for a money order (POST /user/wallet/order).
|
|
export function encodeWalletOrder(productId: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const pid = b.createString(productId);
|
|
fb.WalletOrderRequest.startWalletOrderRequest(b);
|
|
fb.WalletOrderRequest.addProductId(b, pid);
|
|
return finish(b, fb.WalletOrderRequest.endWalletOrderRequest(b));
|
|
}
|
|
|
|
// decodeWallet reads the wallet payload: the context-visible chip segments and the
|
|
// context-applicable benefits.
|
|
export function decodeWallet(buf: Uint8Array): Wallet {
|
|
const w = fb.Wallet.getRootAsWallet(new ByteBuffer(buf));
|
|
const segments: WalletSegment[] = [];
|
|
for (let i = 0; i < w.segmentsLength(); i++) {
|
|
const seg = w.segments(i);
|
|
if (!seg) continue;
|
|
segments.push({ source: s(seg.source()), chips: seg.chips(), spendable: seg.spendable() });
|
|
}
|
|
return {
|
|
segments,
|
|
adsForever: w.adsForever(),
|
|
adsPaidUntilMs: Number(w.adsPaidUntilMs()),
|
|
hints: w.hints(),
|
|
};
|
|
}
|
|
|
|
// decodeWalletOrder reads the created order: its id and the provider launch URL the client opens.
|
|
export function decodeWalletOrder(buf: Uint8Array): WalletOrder {
|
|
const o = fb.WalletOrderResponse.getRootAsWalletOrderResponse(new ByteBuffer(buf));
|
|
return { orderId: s(o.orderId()), redirectUrl: s(o.redirectUrl()) };
|
|
}
|
|
|
|
// decodeCatalog reads the storefront payload: the context-visible products, each a chip-priced
|
|
// value or a chip pack priced in the context's payment method, with its atom composition.
|
|
export function decodeCatalog(buf: Uint8Array): Catalog {
|
|
const c = fb.Catalog.getRootAsCatalog(new ByteBuffer(buf));
|
|
const products: CatalogProduct[] = [];
|
|
for (let i = 0; i < c.productsLength(); i++) {
|
|
const p = c.products(i);
|
|
if (!p) continue;
|
|
const atoms: CatalogAtom[] = [];
|
|
for (let j = 0; j < p.atomsLength(); j++) {
|
|
const a = p.atoms(j);
|
|
if (!a) continue;
|
|
atoms.push({ atomType: s(a.atomType()), quantity: a.quantity() });
|
|
}
|
|
products.push({
|
|
kind: s(p.kind()) === 'pack' ? 'pack' : 'value',
|
|
productId: s(p.productId()),
|
|
title: s(p.title()),
|
|
chips: p.chips(),
|
|
moneyAmount: Number(p.moneyAmount()),
|
|
moneyCurrency: s(p.moneyCurrency()),
|
|
atoms,
|
|
});
|
|
}
|
|
return { products };
|
|
}
|
|
|
|
export function decodeProfile(buf: Uint8Array): Profile {
|
|
const p = fb.Profile.getRootAsProfile(new ByteBuffer(buf));
|
|
return {
|
|
userId: s(p.userId()),
|
|
displayName: s(p.displayName()),
|
|
preferredLanguage: s(p.preferredLanguage()),
|
|
timeZone: s(p.timeZone()),
|
|
awayStart: s(p.awayStart()),
|
|
awayEnd: s(p.awayEnd()),
|
|
hintBalance: p.hintBalance(),
|
|
blockChat: p.blockChat(),
|
|
blockFriendRequests: p.blockFriendRequests(),
|
|
isGuest: p.isGuest(),
|
|
notificationsInAppOnly: p.notificationsInAppOnly(),
|
|
variantPreferences: decodeVariantPreferences(p),
|
|
banner: decodeBanner(p),
|
|
email: s(p.email()),
|
|
telegramLinked: p.telegramLinked(),
|
|
vkLinked: p.vkLinked(),
|
|
dictVersions: decodeDictVersions(p),
|
|
};
|
|
}
|
|
|
|
// decodeDictVersions reads the profile's per-variant current dictionary versions into a
|
|
// variant-keyed map, so the offline preloader can look up the version for each enabled
|
|
// variant. An entry with an unknown variant or empty version is skipped.
|
|
function decodeDictVersions(p: fb.Profile): Partial<Record<Variant, string>> {
|
|
const out: Partial<Record<Variant, string>> = {};
|
|
for (let i = 0; i < p.dictVersionsLength(); i++) {
|
|
const dv = p.dictVersions(i);
|
|
if (!dv) continue;
|
|
const variant = s(dv.variant()) as Variant;
|
|
const version = s(dv.version());
|
|
if (variant && version) out[variant] = version;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// decodeVariantPreferences reads the Profile.variant_preferences vector (the variants
|
|
// the player enabled in Settings).
|
|
function decodeVariantPreferences(p: fb.Profile): Variant[] {
|
|
const out: Variant[] = [];
|
|
for (let i = 0; i < p.variantPreferencesLength(); i++) out.push(s(p.variantPreferences(i)) as Variant);
|
|
return out;
|
|
}
|
|
|
|
// bannerTriple builds a colour override from the three wire fields, or null when the
|
|
// set is absent (any field empty/missing) — the strip then keeps the neutral tokens.
|
|
function bannerTriple(bg: string | null, fg: string | null, link: string | null): ColorTriple | null {
|
|
return bg && fg && link ? { bg, fg, link } : null;
|
|
}
|
|
|
|
// decodeBanner projects the optional advertising-banner block of a Profile, or
|
|
// undefined when the viewer is not eligible (the field is absent).
|
|
function decodeBanner(p: fb.Profile): Banner | undefined {
|
|
const b = p.banner();
|
|
if (!b) return undefined;
|
|
const campaigns: BannerCampaign[] = [];
|
|
for (let i = 0; i < b.campaignsLength(); i++) {
|
|
const c = b.campaigns(i);
|
|
if (!c) continue;
|
|
const messages: string[] = [];
|
|
for (let j = 0; j < c.messagesLength(); j++) messages.push(c.messages(j));
|
|
campaigns.push({
|
|
weight: c.weight(),
|
|
messages,
|
|
overrideAll: bannerTriple(c.overrideBg(), c.overrideFg(), c.overrideLink()),
|
|
overrideDark: bannerTriple(c.overrideBgDark(), c.overrideFgDark(), c.overrideLinkDark()),
|
|
});
|
|
}
|
|
return {
|
|
campaigns,
|
|
timings: {
|
|
holdMs: b.holdMs(),
|
|
edgePauseMs: b.edgePauseMs(),
|
|
scrollPxPerSec: b.scrollPxPerSec(),
|
|
fadeOutMs: b.fadeOutMs(),
|
|
gapMs: b.gapMs(),
|
|
fadeInMs: b.fadeInMs(),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function decodeBlockStatus(buf: Uint8Array): BlockStatus {
|
|
const b = fb.BlockStatus.getRootAsBlockStatus(new ByteBuffer(buf));
|
|
return {
|
|
blocked: b.blocked(),
|
|
permanent: b.permanent(),
|
|
until: s(b.until()),
|
|
reason: s(b.reason()),
|
|
};
|
|
}
|
|
|
|
// decodeStateViewTable projects a StateView table (a root or one nested in an event) to the
|
|
// model. It caches the alphabet when present (a per-variant cache miss) and decodes the index
|
|
// rack to display letters with it.
|
|
function decodeStateViewTable(v: fb.StateView): StateView {
|
|
const g = v.game();
|
|
const variant = (g ? s(g.variant()) : 'scrabble_en') as Variant;
|
|
if (v.alphabetLength() > 0) {
|
|
const entries: AlphabetEntryWire[] = [];
|
|
for (let i = 0; i < v.alphabetLength(); i++) {
|
|
const e = v.alphabet(i);
|
|
if (e) entries.push({ index: e.index(), letter: s(e.letter()), value: e.value() });
|
|
}
|
|
setAlphabet(variant, entries);
|
|
}
|
|
const rack: string[] = [];
|
|
for (let i = 0; i < v.rackLength(); i++) rack.push(letterForIndex(variant, v.rack(i) ?? 0));
|
|
return {
|
|
game: g ? decodeGameView(g) : emptyGame(),
|
|
seat: v.seat(),
|
|
rack,
|
|
bagLen: v.bagLen(),
|
|
hintsRemaining: v.hintsRemaining(),
|
|
walletBalance: v.walletBalance(),
|
|
hintUnlockLeftSeconds: v.hintUnlockLeftSeconds(),
|
|
};
|
|
}
|
|
|
|
export function decodeStateView(buf: Uint8Array): StateView {
|
|
return decodeStateViewTable(fb.StateView.getRootAsStateView(new ByteBuffer(buf)));
|
|
}
|
|
|
|
export function decodeMoveResult(buf: Uint8Array): MoveResult {
|
|
const r = fb.MoveResult.getRootAsMoveResult(new ByteBuffer(buf));
|
|
const m = r.move();
|
|
const g = r.game();
|
|
// The actor's refilled rack rides back as alphabet indices; decode it with the game's variant.
|
|
const variant = (g ? s(g.variant()) : 'scrabble_en') as Variant;
|
|
const rack: string[] = [];
|
|
for (let i = 0; i < r.rackLength(); i++) rack.push(letterForIndex(variant, r.rack(i) ?? 0));
|
|
return {
|
|
move: m ? decodeMove(m) : emptyMove(),
|
|
game: g ? decodeGameView(g) : emptyGame(),
|
|
rack,
|
|
bagLen: r.bagLen(),
|
|
};
|
|
}
|
|
|
|
export function decodeHintResult(buf: Uint8Array): HintResult {
|
|
const r = fb.HintResult.getRootAsHintResult(new ByteBuffer(buf));
|
|
const m = r.move();
|
|
return { move: m ? decodeMove(m) : emptyMove(), hintsRemaining: r.hintsRemaining(), walletBalance: r.walletBalance() };
|
|
}
|
|
|
|
export function decodeEvalResult(buf: Uint8Array): EvalResult {
|
|
const r = fb.EvalResult.getRootAsEvalResult(new ByteBuffer(buf));
|
|
const words: string[] = [];
|
|
for (let i = 0; i < r.wordsLength(); i++) words.push(s(r.words(i)));
|
|
return { legal: r.legal(), score: r.score(), words, dir: s(r.dir()) };
|
|
}
|
|
|
|
export function decodeWordCheck(buf: Uint8Array): WordCheckResult {
|
|
const r = fb.WordCheckResult.getRootAsWordCheckResult(new ByteBuffer(buf));
|
|
return { word: s(r.word()), legal: r.legal() };
|
|
}
|
|
|
|
// decodeDraftView returns the player's stored composition JSON (empty when none is stored or
|
|
// for the save acknowledgement); the caller parses {rack_order, board_tiles}.
|
|
export function decodeDraftView(buf: Uint8Array): string {
|
|
const v = fb.DraftView.getRootAsDraftView(new ByteBuffer(buf));
|
|
return v.json() ?? '';
|
|
}
|
|
|
|
export function decodeHistory(buf: Uint8Array): History {
|
|
const h = fb.History.getRootAsHistory(new ByteBuffer(buf));
|
|
const moves: MoveRecord[] = [];
|
|
for (let i = 0; i < h.movesLength(); i++) {
|
|
const m = h.moves(i);
|
|
if (m) moves.push(decodeMove(m));
|
|
}
|
|
return { gameId: s(h.gameId()), moves };
|
|
}
|
|
|
|
export function decodeGameList(buf: Uint8Array): GameList {
|
|
const gl = fb.GameList.getRootAsGameList(new ByteBuffer(buf));
|
|
const games: GameView[] = [];
|
|
for (let i = 0; i < gl.gamesLength(); i++) {
|
|
const g = gl.games(i);
|
|
if (g) games.push(decodeGameView(g));
|
|
}
|
|
return { games, atGameLimit: gl.atGameLimit() };
|
|
}
|
|
|
|
export function decodeMatchResult(buf: Uint8Array): MatchResult {
|
|
const m = fb.MatchResult.getRootAsMatchResult(new ByteBuffer(buf));
|
|
const g = m.game();
|
|
// Enqueue always lands the caller in a game — an open game awaiting an opponent has
|
|
// matched=false but still carries it — so decode the game whenever it is present.
|
|
return { matched: m.matched(), game: g ? decodeGameView(g) : undefined };
|
|
}
|
|
|
|
export function decodeChatMessage(buf: Uint8Array): ChatMessage {
|
|
return decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(new ByteBuffer(buf)));
|
|
}
|
|
|
|
export function decodeChatList(buf: Uint8Array): ChatMessage[] {
|
|
const cl = fb.ChatList.getRootAsChatList(new ByteBuffer(buf));
|
|
const out: ChatMessage[] = [];
|
|
for (let i = 0; i < cl.messagesLength(); i++) {
|
|
const m = cl.messages(i);
|
|
if (m) out.push(decodeChatMsg(m));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function encodeFeedbackSubmit(
|
|
body: string,
|
|
attachment: Uint8Array | null,
|
|
attachmentName: string,
|
|
channel: string,
|
|
version: string,
|
|
browserTz: string,
|
|
): Uint8Array {
|
|
const b = new Builder(256);
|
|
const bodyOff = b.createString(body);
|
|
const attOff = attachment && attachment.length > 0 ? fb.FeedbackSubmitRequest.createAttachmentVector(b, attachment) : 0;
|
|
const nameOff = b.createString(attachmentName);
|
|
const chOff = b.createString(channel);
|
|
const verOff = b.createString(version);
|
|
const tzOff = b.createString(browserTz);
|
|
fb.FeedbackSubmitRequest.startFeedbackSubmitRequest(b);
|
|
fb.FeedbackSubmitRequest.addBody(b, bodyOff);
|
|
if (attOff) fb.FeedbackSubmitRequest.addAttachment(b, attOff);
|
|
fb.FeedbackSubmitRequest.addAttachmentName(b, nameOff);
|
|
fb.FeedbackSubmitRequest.addChannel(b, chOff);
|
|
fb.FeedbackSubmitRequest.addVersion(b, verOff);
|
|
fb.FeedbackSubmitRequest.addBrowserTz(b, tzOff);
|
|
return finish(b, fb.FeedbackSubmitRequest.endFeedbackSubmitRequest(b));
|
|
}
|
|
|
|
export function decodeFeedbackState(buf: Uint8Array): FeedbackState {
|
|
const st = fb.FeedbackState.getRootAsFeedbackState(new ByteBuffer(buf));
|
|
const r = st.reply();
|
|
return {
|
|
canSend: st.canSend(),
|
|
blockedReason: s(st.blockedReason()),
|
|
reply: r ? { body: s(r.body()), repliedAtUnix: Number(r.repliedAtUnix()) } : null,
|
|
};
|
|
}
|
|
|
|
export function decodeFeedbackUnread(buf: Uint8Array): boolean {
|
|
return fb.FeedbackUnread.getRootAsFeedbackUnread(new ByteBuffer(buf)).replyUnread();
|
|
}
|
|
|
|
export function decodeEvent(kind: string, payload: Uint8Array): PushEvent | null {
|
|
const bb = new ByteBuffer(payload);
|
|
switch (kind) {
|
|
case 'your_turn': {
|
|
const e = fb.YourTurnEvent.getRootAsYourTurnEvent(bb);
|
|
return { kind: 'your_turn', gameId: s(e.gameId()), deadlineUnix: Number(e.deadlineUnix()), opponentName: s(e.opponentName()), moveCount: e.moveCount() };
|
|
}
|
|
case 'opponent_moved': {
|
|
const e = fb.OpponentMovedEvent.getRootAsOpponentMovedEvent(bb);
|
|
const m = e.move();
|
|
const g = e.game();
|
|
return {
|
|
kind: 'opponent_moved',
|
|
gameId: s(e.gameId()),
|
|
move: m ? decodeMove(m) : undefined,
|
|
game: g ? decodeGameView(g) : undefined,
|
|
bagLen: e.bagLen(),
|
|
};
|
|
}
|
|
case 'game_over': {
|
|
const e = fb.GameOverEvent.getRootAsGameOverEvent(bb);
|
|
const g = e.game();
|
|
return {
|
|
kind: 'game_over',
|
|
gameId: s(e.gameId()),
|
|
result: s(e.result()),
|
|
scoreLine: s(e.scoreLine()),
|
|
game: g ? decodeGameView(g) : undefined,
|
|
};
|
|
}
|
|
case 'chat_message':
|
|
return { kind: 'chat_message', message: decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(bb)) };
|
|
case 'nudge': {
|
|
const e = fb.NudgeEvent.getRootAsNudgeEvent(bb);
|
|
return { kind: 'nudge', gameId: s(e.gameId()), fromUserId: s(e.fromUserId()), senderName: s(e.senderName()) };
|
|
}
|
|
case 'match_found': {
|
|
const e = fb.MatchFoundEvent.getRootAsMatchFoundEvent(bb);
|
|
const st = e.state();
|
|
return { kind: 'match_found', gameId: s(e.gameId()), state: st ? decodeStateViewTable(st) : undefined };
|
|
}
|
|
case 'opponent_joined': {
|
|
// opponent_joined reuses the match_found payload layout (game id + the recipient's state).
|
|
const e = fb.MatchFoundEvent.getRootAsMatchFoundEvent(bb);
|
|
const st = e.state();
|
|
return { kind: 'opponent_joined', gameId: s(e.gameId()), state: st ? decodeStateViewTable(st) : undefined };
|
|
}
|
|
case 'notify': {
|
|
const e = fb.NotificationEvent.getRootAsNotificationEvent(bb);
|
|
const acc = e.account();
|
|
const inv = e.invitation();
|
|
const st = e.state();
|
|
return {
|
|
kind: 'notify',
|
|
sub: s(e.kind()),
|
|
account: acc ? decodeAccountRef(acc) : undefined,
|
|
invitation: inv ? decodeInvitationTable(inv) : undefined,
|
|
state: st ? decodeStateViewTable(st) : undefined,
|
|
};
|
|
}
|
|
case 'heartbeat':
|
|
return { kind: 'heartbeat' };
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --- social encoders ---
|
|
|
|
export function encodeTarget(accountId: string, gameId?: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const id = b.createString(accountId);
|
|
// game_id is set only by an in-game block, so a disguised-robot opponent is recorded
|
|
// per-game; every other target path omits it.
|
|
const gid = gameId ? b.createString(gameId) : 0;
|
|
fb.TargetRequest.startTargetRequest(b);
|
|
fb.TargetRequest.addAccountId(b, id);
|
|
if (gid) fb.TargetRequest.addGameId(b, gid);
|
|
return finish(b, fb.TargetRequest.endTargetRequest(b));
|
|
}
|
|
|
|
export function encodeFriendRespond(requesterId: string, accept: boolean): Uint8Array {
|
|
const b = new Builder(64);
|
|
const id = b.createString(requesterId);
|
|
fb.FriendRespondRequest.startFriendRespondRequest(b);
|
|
fb.FriendRespondRequest.addRequesterId(b, id);
|
|
fb.FriendRespondRequest.addAccept(b, accept);
|
|
return finish(b, fb.FriendRespondRequest.endFriendRespondRequest(b));
|
|
}
|
|
|
|
export function encodeRedeemCode(code: string): Uint8Array {
|
|
const b = new Builder(32);
|
|
const c = b.createString(code);
|
|
fb.RedeemCodeRequest.startRedeemCodeRequest(b);
|
|
fb.RedeemCodeRequest.addCode(b, c);
|
|
return finish(b, fb.RedeemCodeRequest.endRedeemCodeRequest(b));
|
|
}
|
|
|
|
export function encodeCreateInvitation(inviteeIds: string[], st: InvitationSettings): Uint8Array {
|
|
const b = new Builder(256);
|
|
const idOffs = inviteeIds.map((id) => b.createString(id));
|
|
const ids = fb.CreateInvitationRequest.createInviteeIdsVector(b, idOffs);
|
|
const variant = b.createString(st.variant);
|
|
const dropout = b.createString(st.dropoutTiles);
|
|
fb.CreateInvitationRequest.startCreateInvitationRequest(b);
|
|
fb.CreateInvitationRequest.addInviteeIds(b, ids);
|
|
fb.CreateInvitationRequest.addVariant(b, variant);
|
|
fb.CreateInvitationRequest.addTurnTimeoutSecs(b, st.turnTimeoutSecs);
|
|
fb.CreateInvitationRequest.addHintsAllowed(b, st.hintsAllowed);
|
|
fb.CreateInvitationRequest.addHintsPerPlayer(b, st.hintsPerPlayer);
|
|
fb.CreateInvitationRequest.addDropoutTiles(b, dropout);
|
|
fb.CreateInvitationRequest.addMultipleWordsPerTurn(b, st.multipleWordsPerTurn);
|
|
return finish(b, fb.CreateInvitationRequest.endCreateInvitationRequest(b));
|
|
}
|
|
|
|
export function encodeInvitationAction(invitationId: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const id = b.createString(invitationId);
|
|
fb.InvitationActionRequest.startInvitationActionRequest(b);
|
|
fb.InvitationActionRequest.addInvitationId(b, id);
|
|
return finish(b, fb.InvitationActionRequest.endInvitationActionRequest(b));
|
|
}
|
|
|
|
export function encodeUpdateProfile(p: ProfileUpdate): Uint8Array {
|
|
const b = new Builder(256);
|
|
const name = b.createString(p.displayName);
|
|
const lang = b.createString(p.preferredLanguage);
|
|
const tz = b.createString(p.timeZone);
|
|
const as = b.createString(p.awayStart);
|
|
const ae = b.createString(p.awayEnd);
|
|
const prefs = fb.UpdateProfileRequest.createVariantPreferencesVector(
|
|
b,
|
|
p.variantPreferences.map((v) => b.createString(v)),
|
|
);
|
|
fb.UpdateProfileRequest.startUpdateProfileRequest(b);
|
|
fb.UpdateProfileRequest.addDisplayName(b, name);
|
|
fb.UpdateProfileRequest.addPreferredLanguage(b, lang);
|
|
fb.UpdateProfileRequest.addTimeZone(b, tz);
|
|
fb.UpdateProfileRequest.addAwayStart(b, as);
|
|
fb.UpdateProfileRequest.addAwayEnd(b, ae);
|
|
fb.UpdateProfileRequest.addBlockChat(b, p.blockChat);
|
|
fb.UpdateProfileRequest.addBlockFriendRequests(b, p.blockFriendRequests);
|
|
fb.UpdateProfileRequest.addNotificationsInAppOnly(b, p.notificationsInAppOnly);
|
|
fb.UpdateProfileRequest.addVariantPreferences(b, prefs);
|
|
return finish(b, fb.UpdateProfileRequest.endUpdateProfileRequest(b));
|
|
}
|
|
|
|
// --- account linking & merge ---
|
|
|
|
export function encodeLinkEmailRequest(email: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const e = b.createString(email);
|
|
fb.LinkEmailRequest.startLinkEmailRequest(b);
|
|
fb.LinkEmailRequest.addEmail(b, e);
|
|
return finish(b, fb.LinkEmailRequest.endLinkEmailRequest(b));
|
|
}
|
|
|
|
export function encodeLinkEmailConfirm(email: string, code: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const e = b.createString(email);
|
|
const c = b.createString(code);
|
|
fb.LinkEmailConfirm.startLinkEmailConfirm(b);
|
|
fb.LinkEmailConfirm.addEmail(b, e);
|
|
fb.LinkEmailConfirm.addCode(b, c);
|
|
return finish(b, fb.LinkEmailConfirm.endLinkEmailConfirm(b));
|
|
}
|
|
|
|
export function encodeLinkTelegram(data: string): Uint8Array {
|
|
const b = new Builder(256);
|
|
const d = b.createString(data);
|
|
fb.LinkTelegramRequest.startLinkTelegramRequest(b);
|
|
fb.LinkTelegramRequest.addData(b, d);
|
|
return finish(b, fb.LinkTelegramRequest.endLinkTelegramRequest(b));
|
|
}
|
|
|
|
export function encodeLinkUnlink(kind: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const k = b.createString(kind);
|
|
fb.LinkUnlinkRequest.startLinkUnlinkRequest(b);
|
|
fb.LinkUnlinkRequest.addKind(b, k);
|
|
return finish(b, fb.LinkUnlinkRequest.endLinkUnlinkRequest(b));
|
|
}
|
|
|
|
export function encodeLinkVK(code: string, deviceId: string, codeVerifier: string): Uint8Array {
|
|
const b = new Builder(256);
|
|
const c = b.createString(code);
|
|
const d = b.createString(deviceId);
|
|
const v = b.createString(codeVerifier);
|
|
fb.LinkVKRequest.startLinkVKRequest(b);
|
|
fb.LinkVKRequest.addCode(b, c);
|
|
fb.LinkVKRequest.addDeviceId(b, d);
|
|
fb.LinkVKRequest.addCodeVerifier(b, v);
|
|
return finish(b, fb.LinkVKRequest.endLinkVKRequest(b));
|
|
}
|
|
|
|
export function encodeAccountDeleteConfirm(code: string, phrase: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const c = b.createString(code);
|
|
const p = b.createString(phrase);
|
|
fb.AccountDeleteConfirm.startAccountDeleteConfirm(b);
|
|
fb.AccountDeleteConfirm.addCode(b, c);
|
|
fb.AccountDeleteConfirm.addPhrase(b, p);
|
|
return finish(b, fb.AccountDeleteConfirm.endAccountDeleteConfirm(b));
|
|
}
|
|
|
|
export function decodeDeleteRequestResult(buf: Uint8Array): { method: 'email' | 'phrase' } {
|
|
const r = fb.AccountDeleteRequestResult.getRootAsAccountDeleteRequestResult(new ByteBuffer(buf));
|
|
return { method: (s(r.method()) || 'phrase') as 'email' | 'phrase' };
|
|
}
|
|
|
|
export function decodeLinkResult(buf: Uint8Array): LinkResult {
|
|
const r = fb.LinkResult.getRootAsLinkResult(new ByteBuffer(buf));
|
|
const sess = r.session();
|
|
return {
|
|
status: (s(r.status()) || 'linked') as LinkResult['status'],
|
|
secondaryUserId: s(r.secondaryUserId()),
|
|
secondaryDisplayName: s(r.secondaryDisplayName()),
|
|
secondaryGames: r.secondaryGames(),
|
|
secondaryFriends: r.secondaryFriends(),
|
|
session: sess ? sessionFromTable(sess) : null,
|
|
};
|
|
}
|
|
|
|
export function decodeConfirmLinkResult(buf: Uint8Array): ConfirmLinkResult {
|
|
const r = fb.EmailConfirmLinkResult.getRootAsEmailConfirmLinkResult(new ByteBuffer(buf));
|
|
const sess = r.session();
|
|
return {
|
|
purpose: (s(r.purpose()) || 'link') as ConfirmLinkResult['purpose'],
|
|
status: (s(r.status()) || 'confirmed') as ConfirmLinkResult['status'],
|
|
session: sess ? sessionFromTable(sess) : null,
|
|
};
|
|
}
|
|
|
|
// --- social decoders ---
|
|
|
|
function decodeAccountRef(r: fb.AccountRef): AccountRef {
|
|
return { accountId: s(r.accountId()), displayName: s(r.displayName()) };
|
|
}
|
|
|
|
export function decodeFriendList(buf: Uint8Array): AccountRef[] {
|
|
const l = fb.FriendList.getRootAsFriendList(new ByteBuffer(buf));
|
|
const out: AccountRef[] = [];
|
|
for (let i = 0; i < l.friendsLength(); i++) {
|
|
const r = l.friends(i);
|
|
if (r) out.push(decodeAccountRef(r));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function decodeIncomingList(buf: Uint8Array): AccountRef[] {
|
|
const l = fb.IncomingRequestList.getRootAsIncomingRequestList(new ByteBuffer(buf));
|
|
const out: AccountRef[] = [];
|
|
for (let i = 0; i < l.requestsLength(); i++) {
|
|
const r = l.requests(i);
|
|
if (r) out.push(decodeAccountRef(r));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function decodeOutgoingList(buf: Uint8Array): OutgoingList {
|
|
const l = fb.OutgoingRequestList.getRootAsOutgoingRequestList(new ByteBuffer(buf));
|
|
const requests: AccountRef[] = [];
|
|
for (let i = 0; i < l.requestsLength(); i++) {
|
|
const r = l.requests(i);
|
|
if (r) requests.push(decodeAccountRef(r));
|
|
}
|
|
const robots: RobotFriendRequestEntry[] = [];
|
|
for (let i = 0; i < l.robotsLength(); i++) {
|
|
const r = l.robots(i);
|
|
if (r)
|
|
robots.push({
|
|
id: r.id() ?? '',
|
|
displayName: r.displayName() ?? '',
|
|
gameId: r.gameId() ?? '',
|
|
seat: r.seat(),
|
|
});
|
|
}
|
|
return { requests, robots };
|
|
}
|
|
|
|
export function decodeBlockList(buf: Uint8Array): BlockList {
|
|
const l = fb.BlockList.getRootAsBlockList(new ByteBuffer(buf));
|
|
const blocked: AccountRef[] = [];
|
|
for (let i = 0; i < l.blockedLength(); i++) {
|
|
const r = l.blocked(i);
|
|
if (r) blocked.push(decodeAccountRef(r));
|
|
}
|
|
const robots: RobotBlockEntry[] = [];
|
|
for (let i = 0; i < l.robotsLength(); i++) {
|
|
const r = l.robots(i);
|
|
if (r)
|
|
robots.push({
|
|
id: r.id() ?? '',
|
|
displayName: r.displayName() ?? '',
|
|
gameId: r.gameId() ?? '',
|
|
seat: r.seat(),
|
|
});
|
|
}
|
|
return { blocked, robots };
|
|
}
|
|
|
|
export function decodeFriendCode(buf: Uint8Array): FriendCode {
|
|
const c = fb.FriendCode.getRootAsFriendCode(new ByteBuffer(buf));
|
|
return { code: s(c.code()), expiresAtUnix: Number(c.expiresAtUnix()) };
|
|
}
|
|
|
|
export function decodeRedeemResult(buf: Uint8Array): AccountRef {
|
|
const r = fb.RedeemResult.getRootAsRedeemResult(new ByteBuffer(buf));
|
|
const f = r.friend();
|
|
return f ? decodeAccountRef(f) : { accountId: '', displayName: '' };
|
|
}
|
|
|
|
export function decodeStats(buf: Uint8Array): Stats {
|
|
const v = fb.StatsView.getRootAsStatsView(new ByteBuffer(buf));
|
|
const bestMoves: BestMove[] = [];
|
|
for (let i = 0; i < v.bestMovesLength(); i++) {
|
|
const m = v.bestMoves(i);
|
|
if (!m) continue;
|
|
const word: BestMoveTile[] = [];
|
|
for (let j = 0; j < m.wordLength(); j++) {
|
|
const t = m.word(j);
|
|
if (t) word.push({ letter: s(t.letter()), value: t.value(), blank: t.blank() });
|
|
}
|
|
bestMoves.push({ variant: s(m.variant()) as Variant, score: m.score(), word });
|
|
}
|
|
return {
|
|
wins: v.wins(),
|
|
losses: v.losses(),
|
|
draws: v.draws(),
|
|
maxGamePoints: v.maxGamePoints(),
|
|
maxWordPoints: v.maxWordPoints(),
|
|
moves: v.moves(),
|
|
hintsUsed: v.hintsUsed(),
|
|
bestMoves,
|
|
};
|
|
}
|
|
|
|
function decodeInvitationTable(i: fb.Invitation): Invitation {
|
|
const inviter = i.inviter();
|
|
const invitees: InvitationInvitee[] = [];
|
|
for (let k = 0; k < i.inviteesLength(); k++) {
|
|
const iv = i.invitees(k);
|
|
if (iv) {
|
|
invitees.push({
|
|
accountId: s(iv.accountId()),
|
|
displayName: s(iv.displayName()),
|
|
seat: iv.seat(),
|
|
response: s(iv.response()),
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
id: s(i.id()),
|
|
inviter: inviter ? decodeAccountRef(inviter) : { accountId: '', displayName: '' },
|
|
invitees,
|
|
variant: s(i.variant()) as Variant,
|
|
turnTimeoutSecs: i.turnTimeoutSecs(),
|
|
hintsAllowed: i.hintsAllowed(),
|
|
hintsPerPlayer: i.hintsPerPlayer(),
|
|
multipleWordsPerTurn: i.multipleWordsPerTurn(),
|
|
dropoutTiles: s(i.dropoutTiles()),
|
|
status: s(i.status()),
|
|
gameId: s(i.gameId()),
|
|
expiresAtUnix: Number(i.expiresAtUnix()),
|
|
};
|
|
}
|
|
|
|
export function decodeInvitation(buf: Uint8Array): Invitation {
|
|
return decodeInvitationTable(fb.Invitation.getRootAsInvitation(new ByteBuffer(buf)));
|
|
}
|
|
|
|
export function decodeInvitationList(buf: Uint8Array): Invitation[] {
|
|
const l = fb.InvitationList.getRootAsInvitationList(new ByteBuffer(buf));
|
|
const out: Invitation[] = [];
|
|
for (let i = 0; i < l.invitationsLength(); i++) {
|
|
const inv = l.invitations(i);
|
|
if (inv) out.push(decodeInvitationTable(inv));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function decodeGcg(buf: Uint8Array): GcgExport {
|
|
const g = fb.GcgExport.getRootAsGcgExport(new ByteBuffer(buf));
|
|
return { gameId: s(g.gameId()), filename: s(g.filename()), content: s(g.content()) };
|
|
}
|
|
|
|
export function encodeExportUrlRequest(
|
|
gameId: string,
|
|
kind: 'png' | 'gcg',
|
|
dateLocale: string,
|
|
actionLabels: string[],
|
|
timeZone: string,
|
|
): Uint8Array {
|
|
const b = new Builder(256);
|
|
const gid = b.createString(gameId);
|
|
const k = b.createString(kind);
|
|
const dl = b.createString(dateLocale);
|
|
const tz = b.createString(timeZone);
|
|
const labels = fb.ExportUrlRequest.createActionLabelsVector(
|
|
b,
|
|
actionLabels.map((l) => b.createString(l)),
|
|
);
|
|
fb.ExportUrlRequest.startExportUrlRequest(b);
|
|
fb.ExportUrlRequest.addGameId(b, gid);
|
|
fb.ExportUrlRequest.addKind(b, k);
|
|
fb.ExportUrlRequest.addDateLocale(b, dl);
|
|
fb.ExportUrlRequest.addActionLabels(b, labels);
|
|
fb.ExportUrlRequest.addTimeZone(b, tz);
|
|
return finish(b, fb.ExportUrlRequest.endExportUrlRequest(b));
|
|
}
|
|
|
|
export function decodeExportUrl(buf: Uint8Array): ExportUrl {
|
|
const e = fb.ExportUrl.getRootAsExportUrl(new ByteBuffer(buf));
|
|
return { path: s(e.path()), filename: s(e.filename()) };
|
|
}
|
|
|
|
function emptyGame(): GameView {
|
|
return {
|
|
id: '',
|
|
variant: 'scrabble_en',
|
|
dictVersion: '',
|
|
status: '',
|
|
players: 0,
|
|
toMove: 0,
|
|
turnTimeoutSecs: 0,
|
|
multipleWordsPerTurn: true,
|
|
moveCount: 0,
|
|
endReason: '',
|
|
lastActivityUnix: 0,
|
|
vsAi: false,
|
|
unreadChat: false,
|
|
unreadMessages: false,
|
|
seats: [],
|
|
};
|
|
}
|
|
|
|
function emptyMove(): MoveRecord {
|
|
return { player: 0, action: '', dir: '', mainRow: 0, mainCol: 0, tiles: [], words: [], count: 0, score: 0, total: 0 };
|
|
}
|