@thegeem/protocol 0.1.36 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +12 -5
- package/dist/index.d.cts +69 -11
- package/dist/index.d.ts +69 -11
- package/dist/index.js +10 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -33,7 +33,8 @@ __export(index_exports, {
|
|
|
33
33
|
ROOM_ALPHABET: () => ROOM_ALPHABET,
|
|
34
34
|
gameIdForPv: () => gameIdForPv,
|
|
35
35
|
gamesForPv: () => gamesForPv,
|
|
36
|
-
modesForGame: () => modesForGame
|
|
36
|
+
modesForGame: () => modesForGame,
|
|
37
|
+
normalizeWesternDigits: () => normalizeWesternDigits
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(index_exports);
|
|
39
40
|
var import_zod = require("zod");
|
|
@@ -75,7 +76,8 @@ var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
|
|
|
75
76
|
var CHARACTER_IDS = ["m", "f"];
|
|
76
77
|
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
77
78
|
var UNSAFE_CHARS = /[\p{Cc}\u202A-\u202E\u2066-\u2069<>]/gu;
|
|
78
|
-
var
|
|
79
|
+
var normalizeWesternDigits = (s) => s.replace(/[٠-٩۰-۹]/g, (d) => String("\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) >= 0 ? "\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) : "\u06F0\u06F1\u06F2\u06F3\u06F4\u06F5\u06F6\u06F7\u06F8\u06F9".indexOf(d)));
|
|
80
|
+
var DisplayName = import_zod.z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform((s) => normalizeWesternDigits(s.replace(UNSAFE_CHARS, "")).trim()).refine((s) => s.length >= 1, "name is empty after sanitizing");
|
|
79
81
|
var RoomCode = import_zod.z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
|
|
80
82
|
var PlayerId = import_zod.z.string().max(40);
|
|
81
83
|
var HelpType = import_zod.z.enum([
|
|
@@ -89,7 +91,7 @@ var HelpType = import_zod.z.enum([
|
|
|
89
91
|
]);
|
|
90
92
|
var Pv = import_zod.z.number().int().min(1).max(1e6).optional();
|
|
91
93
|
var IdToken = import_zod.z.string().max(4096).optional();
|
|
92
|
-
var Platform = import_zod.z.enum(["web", "ios", "android"]).optional();
|
|
94
|
+
var Platform = import_zod.z.enum(["web", "ios", "android", "tv"]).optional();
|
|
93
95
|
var AppVersion = import_zod.z.string().trim().max(24).regex(/^[0-9]+(\.[0-9]+){0,3}([-+][0-9A-Za-z.-]+)?$/, "bad app version").optional();
|
|
94
96
|
var Avatar = import_zod.z.object({
|
|
95
97
|
emoji: import_zod.z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
|
|
@@ -216,7 +218,11 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
216
218
|
// trivia is 4-option (0–3)
|
|
217
219
|
import_zod.z.object({ t: import_zod.z.literal("useHelp"), helpType: HelpType }),
|
|
218
220
|
// sabotage a rival (active team only): spend a sabotage help to debuff targetTeam's next question
|
|
219
|
-
|
|
221
|
+
// `player` (gs#80): WHICH member of the target team sits out — required by استريح/restPlayer and
|
|
222
|
+
// ignored by عرجوب/tripLevel, which targets a whole team. Optional on the wire so pre-gs#80 clients
|
|
223
|
+
// are unchanged; the engine refuses to spend a restPlayer card without a valid one, so an old client
|
|
224
|
+
// cannot burn a lifeline by omitting it.
|
|
225
|
+
import_zod.z.object({ t: import_zod.z.literal("sabotage"), helpType: HelpType, targetTeam: import_zod.z.number().int().min(0).max(LIMITS.MAX_PLAYERS - 1), player: import_zod.z.string().max(64).optional() }),
|
|
220
226
|
// 0-based team index
|
|
221
227
|
// moderation
|
|
222
228
|
// Report another player in the room (App Store 1.2 UGC — display names). Available in lobby or
|
|
@@ -275,5 +281,6 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
275
281
|
ROOM_ALPHABET,
|
|
276
282
|
gameIdForPv,
|
|
277
283
|
gamesForPv,
|
|
278
|
-
modesForGame
|
|
284
|
+
modesForGame,
|
|
285
|
+
normalizeWesternDigits
|
|
279
286
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -103,6 +103,15 @@ declare const CHARACTER_IDS: readonly ["m", "f"];
|
|
|
103
103
|
type CharacterId = (typeof CHARACTER_IDS)[number];
|
|
104
104
|
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
105
105
|
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
106
|
+
/**
|
|
107
|
+
* Arabic-Indic (٠-٩) and Extended/Persian (۰-۹) digits → Western, D-043 applied to NAMES at the
|
|
108
|
+
* gate (ruling 2026-08-13, the «سالم ١٧» leaderboard sighting). A digit is a GLYPH of a number,
|
|
109
|
+
* not identity — «سالم ١٧» and «سالم 17» are the same name — so normalising at write-time is the
|
|
110
|
+
* same class of edit as the bidi strip above, and every surface on every client agrees on what to
|
|
111
|
+
* call the person. Normalising at RENDER instead would have web and iOS disagreeing about a name.
|
|
112
|
+
* Letters are never touched; this maps exactly twenty codepoints.
|
|
113
|
+
*/
|
|
114
|
+
declare const normalizeWesternDigits: (s: string) => string;
|
|
106
115
|
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
107
116
|
declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
108
117
|
t: z.ZodLiteral<"createRoom">;
|
|
@@ -121,7 +130,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
121
130
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
122
131
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
123
132
|
idToken: z.ZodOptional<z.ZodString>;
|
|
124
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
133
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
125
134
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
126
135
|
}, "strip", z.ZodTypeAny, {
|
|
127
136
|
t: "createRoom";
|
|
@@ -134,7 +143,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
143
|
character?: "m" | "f" | undefined;
|
|
135
144
|
pv?: number | undefined;
|
|
136
145
|
idToken?: string | undefined;
|
|
137
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
146
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
138
147
|
appVersion?: string | undefined;
|
|
139
148
|
}, {
|
|
140
149
|
t: "createRoom";
|
|
@@ -147,7 +156,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
147
156
|
character?: "m" | "f" | undefined;
|
|
148
157
|
pv?: number | undefined;
|
|
149
158
|
idToken?: string | undefined;
|
|
150
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
159
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
151
160
|
appVersion?: string | undefined;
|
|
152
161
|
}>, z.ZodObject<{
|
|
153
162
|
t: z.ZodLiteral<"renameHost">;
|
|
@@ -205,7 +214,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
205
214
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
206
215
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
207
216
|
idToken: z.ZodOptional<z.ZodString>;
|
|
208
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
217
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
209
218
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
210
219
|
}, "strip", z.ZodTypeAny, {
|
|
211
220
|
code: string;
|
|
@@ -218,7 +227,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
218
227
|
character?: "m" | "f" | undefined;
|
|
219
228
|
pv?: number | undefined;
|
|
220
229
|
idToken?: string | undefined;
|
|
221
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
230
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
222
231
|
appVersion?: string | undefined;
|
|
223
232
|
}, {
|
|
224
233
|
code: string;
|
|
@@ -231,7 +240,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
231
240
|
character?: "m" | "f" | undefined;
|
|
232
241
|
pv?: number | undefined;
|
|
233
242
|
idToken?: string | undefined;
|
|
234
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
243
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
235
244
|
appVersion?: string | undefined;
|
|
236
245
|
}>, z.ZodObject<{
|
|
237
246
|
t: z.ZodLiteral<"resume">;
|
|
@@ -250,7 +259,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
250
259
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
251
260
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
252
261
|
idToken: z.ZodOptional<z.ZodString>;
|
|
253
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
262
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
254
263
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
255
264
|
}, "strip", z.ZodTypeAny, {
|
|
256
265
|
code: string;
|
|
@@ -263,7 +272,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
263
272
|
character?: "m" | "f" | undefined;
|
|
264
273
|
pv?: number | undefined;
|
|
265
274
|
idToken?: string | undefined;
|
|
266
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
275
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
267
276
|
appVersion?: string | undefined;
|
|
268
277
|
}, {
|
|
269
278
|
code: string;
|
|
@@ -276,7 +285,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
276
285
|
character?: "m" | "f" | undefined;
|
|
277
286
|
pv?: number | undefined;
|
|
278
287
|
idToken?: string | undefined;
|
|
279
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
288
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
280
289
|
appVersion?: string | undefined;
|
|
281
290
|
}>, z.ZodObject<{
|
|
282
291
|
t: z.ZodLiteral<"setPlaying">;
|
|
@@ -505,14 +514,17 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
505
514
|
t: z.ZodLiteral<"sabotage">;
|
|
506
515
|
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
507
516
|
targetTeam: z.ZodNumber;
|
|
517
|
+
player: z.ZodOptional<z.ZodString>;
|
|
508
518
|
}, "strip", z.ZodTypeAny, {
|
|
509
519
|
t: "sabotage";
|
|
510
520
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
511
521
|
targetTeam: number;
|
|
522
|
+
player?: string | undefined;
|
|
512
523
|
}, {
|
|
513
524
|
t: "sabotage";
|
|
514
525
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
515
526
|
targetTeam: number;
|
|
527
|
+
player?: string | undefined;
|
|
516
528
|
}>, z.ZodObject<{
|
|
517
529
|
t: z.ZodLiteral<"report">;
|
|
518
530
|
playerId: z.ZodString;
|
|
@@ -691,6 +703,32 @@ interface LobbyPlayer {
|
|
|
691
703
|
avatar?: AvatarView;
|
|
692
704
|
/** Team index once the game has started; null/absent in the lobby. */
|
|
693
705
|
teamIndex?: number | null;
|
|
706
|
+
/**
|
|
707
|
+
* Equipped avatar-frame slug (e.g. "frame-sadu") — resolves to art via /assets/store/catalog.json
|
|
708
|
+
* (the catalog is the ONLY resolver; nothing about the art is derived from the slug). Read from the
|
|
709
|
+
* account once at room entry, so an equip change shows on the player's next join/resume. Absent =
|
|
710
|
+
* frameless. Earnable-class cosmetics only ever ride the room wire (D-059) — a paid exclusive can
|
|
711
|
+
* never appear here, so there is no receipt check in the broadcast path and no have/have-not row
|
|
712
|
+
* between children on one sofa. (gs#84 contract, D-060.)
|
|
713
|
+
*/
|
|
714
|
+
frame?: string;
|
|
715
|
+
/**
|
|
716
|
+
* This room kicked this exact person before — a HINT for the host, never a gate (the ruling is
|
|
717
|
+
* "label the re-knock, no ban list"). **Present ONLY in the HOST's copy of the frame**: telling a
|
|
718
|
+
* whole room who was once removed is a social leak, not a moderation aid.
|
|
719
|
+
*
|
|
720
|
+
* The same bit also rides `joinRequest` for the immediate ping. This one is the DURABLE carrier —
|
|
721
|
+
* it is RE-READ from the answer stored on the seat (decided once, at join; never recomputed by
|
|
722
|
+
* re-matching, which goes stale) and re-sent on every snapshot, so it survives a host reconnect,
|
|
723
|
+
* where a cached one-shot event would not. A client should prefer this and treat the event as a
|
|
724
|
+
* nudge. MID-GAME there are no lobby frames: there a host who resumes is re-sent the pending
|
|
725
|
+
* `joinRequest`s instead (labels included), so the durable promise holds in both halves.
|
|
726
|
+
*
|
|
727
|
+
* Absent means "no reason to think so", which includes the deliberate miss: an anonymous player who
|
|
728
|
+
* force-quits and rejoins reads as a stranger. Matched only on exact socket/sign-in identity — never
|
|
729
|
+
* a display name or IP, because a wrong label is worse than none.
|
|
730
|
+
*/
|
|
731
|
+
wasKicked?: boolean;
|
|
694
732
|
}
|
|
695
733
|
/**
|
|
696
734
|
* Who is in the room, carried ON the game-state push (gs#71).
|
|
@@ -719,6 +757,9 @@ interface RosterEntry {
|
|
|
719
757
|
isHost?: boolean;
|
|
720
758
|
/** Is this person currently connected? Mirrors `LobbyPlayer.connected`. */
|
|
721
759
|
connected?: boolean;
|
|
760
|
+
/** Equipped avatar-frame slug — mirrors `LobbyPlayer.frame`, because the roster is the MID-GAME
|
|
761
|
+
* carrier (gs#71): a cosmetic with no mid-game carrier silently vanishes for watchers and the TV. */
|
|
762
|
+
frame?: string;
|
|
722
763
|
}
|
|
723
764
|
/** Per-recipient context attached to lobby/state pushes. */
|
|
724
765
|
interface YouContext {
|
|
@@ -786,6 +827,8 @@ interface TeamView {
|
|
|
786
827
|
/** The player on THIS team whose turn it is to answer (rotates each turn); null if the team has
|
|
787
828
|
* no seated players. In controller mode only this player may `answer` on the team's turn. */
|
|
788
829
|
currentPlayerId: string | null;
|
|
830
|
+
/** استريح (gs#80): this team's player who sits out their next question, if a rival named one. */
|
|
831
|
+
restedPlayerId?: string | null;
|
|
789
832
|
/** The seated player's privacy-safe avatar (relay-enriched; engine view omits it). */
|
|
790
833
|
avatar?: AvatarView;
|
|
791
834
|
}
|
|
@@ -797,6 +840,10 @@ interface QuestionView {
|
|
|
797
840
|
selectedAnswerIndex: number | null;
|
|
798
841
|
/** Only present once revealed (anti-cheat). Null while the question is live. */
|
|
799
842
|
correctAnswerIndex: number | null;
|
|
843
|
+
/** «معلومة» — optional explanation shown AFTER the reveal. Null while the question is live (it
|
|
844
|
+
* usually names the answer — same anti-cheat gate as correctAnswerIndex) and null when the
|
|
845
|
+
* question simply has none, which is most of the bank: render the card only when non-null. */
|
|
846
|
+
explain?: string | null;
|
|
800
847
|
imageName?: string;
|
|
801
848
|
videoName?: string;
|
|
802
849
|
/** For video questions: seconds into the clip to stop playback (so the answer isn't shown). Absent = play through. */
|
|
@@ -958,6 +1005,7 @@ type ServerMsg = {
|
|
|
958
1005
|
t: 'joinRequest';
|
|
959
1006
|
playerId: string;
|
|
960
1007
|
name: string;
|
|
1008
|
+
wasKicked?: boolean;
|
|
961
1009
|
} | {
|
|
962
1010
|
t: 'joined';
|
|
963
1011
|
youId: string;
|
|
@@ -983,6 +1031,7 @@ type ServerMsg = {
|
|
|
983
1031
|
hostId: string;
|
|
984
1032
|
hostName: string;
|
|
985
1033
|
hostAvatar?: AvatarView;
|
|
1034
|
+
hostFrame?: string;
|
|
986
1035
|
hostPlaying?: boolean;
|
|
987
1036
|
hostPinned?: boolean;
|
|
988
1037
|
players: LobbyPlayer[];
|
|
@@ -994,6 +1043,9 @@ type ServerMsg = {
|
|
|
994
1043
|
tally?: {
|
|
995
1044
|
[playerId: string]: number;
|
|
996
1045
|
};
|
|
1046
|
+
tallyNames?: {
|
|
1047
|
+
[playerId: string]: string;
|
|
1048
|
+
};
|
|
997
1049
|
modeId: ModeId | null;
|
|
998
1050
|
you: YouContext;
|
|
999
1051
|
} | {
|
|
@@ -1040,10 +1092,16 @@ type ServerMsg = {
|
|
|
1040
1092
|
message: string;
|
|
1041
1093
|
level?: 'info' | 'warn' | 'maintenance';
|
|
1042
1094
|
restartInSec?: number;
|
|
1043
|
-
}
|
|
1095
|
+
}
|
|
1096
|
+
/** `reason` (gs#83, additive): today only `'ended'`, and only on `no_room` — the code WAS a room
|
|
1097
|
+
* until recently (stale WhatsApp link, knock after the host closed the night). Absent = "no such
|
|
1098
|
+
* room as far as we know". Lets a client say «انتهى اللعب» instead of «ما في غرفة بهذا الكود»
|
|
1099
|
+
* without guessing. Old clients ignore it. */
|
|
1100
|
+
| {
|
|
1044
1101
|
t: 'error';
|
|
1045
1102
|
code: string;
|
|
1046
1103
|
message: string;
|
|
1104
|
+
reason?: string;
|
|
1047
1105
|
};
|
|
1048
1106
|
|
|
1049
|
-
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type PackView, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
|
|
1107
|
+
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type PackView, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame, normalizeWesternDigits };
|
package/dist/index.d.ts
CHANGED
|
@@ -103,6 +103,15 @@ declare const CHARACTER_IDS: readonly ["m", "f"];
|
|
|
103
103
|
type CharacterId = (typeof CHARACTER_IDS)[number];
|
|
104
104
|
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
105
105
|
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
106
|
+
/**
|
|
107
|
+
* Arabic-Indic (٠-٩) and Extended/Persian (۰-۹) digits → Western, D-043 applied to NAMES at the
|
|
108
|
+
* gate (ruling 2026-08-13, the «سالم ١٧» leaderboard sighting). A digit is a GLYPH of a number,
|
|
109
|
+
* not identity — «سالم ١٧» and «سالم 17» are the same name — so normalising at write-time is the
|
|
110
|
+
* same class of edit as the bidi strip above, and every surface on every client agrees on what to
|
|
111
|
+
* call the person. Normalising at RENDER instead would have web and iOS disagreeing about a name.
|
|
112
|
+
* Letters are never touched; this maps exactly twenty codepoints.
|
|
113
|
+
*/
|
|
114
|
+
declare const normalizeWesternDigits: (s: string) => string;
|
|
106
115
|
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
107
116
|
declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
108
117
|
t: z.ZodLiteral<"createRoom">;
|
|
@@ -121,7 +130,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
121
130
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
122
131
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
123
132
|
idToken: z.ZodOptional<z.ZodString>;
|
|
124
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
133
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
125
134
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
126
135
|
}, "strip", z.ZodTypeAny, {
|
|
127
136
|
t: "createRoom";
|
|
@@ -134,7 +143,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
143
|
character?: "m" | "f" | undefined;
|
|
135
144
|
pv?: number | undefined;
|
|
136
145
|
idToken?: string | undefined;
|
|
137
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
146
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
138
147
|
appVersion?: string | undefined;
|
|
139
148
|
}, {
|
|
140
149
|
t: "createRoom";
|
|
@@ -147,7 +156,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
147
156
|
character?: "m" | "f" | undefined;
|
|
148
157
|
pv?: number | undefined;
|
|
149
158
|
idToken?: string | undefined;
|
|
150
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
159
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
151
160
|
appVersion?: string | undefined;
|
|
152
161
|
}>, z.ZodObject<{
|
|
153
162
|
t: z.ZodLiteral<"renameHost">;
|
|
@@ -205,7 +214,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
205
214
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
206
215
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
207
216
|
idToken: z.ZodOptional<z.ZodString>;
|
|
208
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
217
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
209
218
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
210
219
|
}, "strip", z.ZodTypeAny, {
|
|
211
220
|
code: string;
|
|
@@ -218,7 +227,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
218
227
|
character?: "m" | "f" | undefined;
|
|
219
228
|
pv?: number | undefined;
|
|
220
229
|
idToken?: string | undefined;
|
|
221
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
230
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
222
231
|
appVersion?: string | undefined;
|
|
223
232
|
}, {
|
|
224
233
|
code: string;
|
|
@@ -231,7 +240,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
231
240
|
character?: "m" | "f" | undefined;
|
|
232
241
|
pv?: number | undefined;
|
|
233
242
|
idToken?: string | undefined;
|
|
234
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
243
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
235
244
|
appVersion?: string | undefined;
|
|
236
245
|
}>, z.ZodObject<{
|
|
237
246
|
t: z.ZodLiteral<"resume">;
|
|
@@ -250,7 +259,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
250
259
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
251
260
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
252
261
|
idToken: z.ZodOptional<z.ZodString>;
|
|
253
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
262
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
254
263
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
255
264
|
}, "strip", z.ZodTypeAny, {
|
|
256
265
|
code: string;
|
|
@@ -263,7 +272,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
263
272
|
character?: "m" | "f" | undefined;
|
|
264
273
|
pv?: number | undefined;
|
|
265
274
|
idToken?: string | undefined;
|
|
266
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
275
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
267
276
|
appVersion?: string | undefined;
|
|
268
277
|
}, {
|
|
269
278
|
code: string;
|
|
@@ -276,7 +285,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
276
285
|
character?: "m" | "f" | undefined;
|
|
277
286
|
pv?: number | undefined;
|
|
278
287
|
idToken?: string | undefined;
|
|
279
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
288
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
280
289
|
appVersion?: string | undefined;
|
|
281
290
|
}>, z.ZodObject<{
|
|
282
291
|
t: z.ZodLiteral<"setPlaying">;
|
|
@@ -505,14 +514,17 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
505
514
|
t: z.ZodLiteral<"sabotage">;
|
|
506
515
|
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
507
516
|
targetTeam: z.ZodNumber;
|
|
517
|
+
player: z.ZodOptional<z.ZodString>;
|
|
508
518
|
}, "strip", z.ZodTypeAny, {
|
|
509
519
|
t: "sabotage";
|
|
510
520
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
511
521
|
targetTeam: number;
|
|
522
|
+
player?: string | undefined;
|
|
512
523
|
}, {
|
|
513
524
|
t: "sabotage";
|
|
514
525
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
515
526
|
targetTeam: number;
|
|
527
|
+
player?: string | undefined;
|
|
516
528
|
}>, z.ZodObject<{
|
|
517
529
|
t: z.ZodLiteral<"report">;
|
|
518
530
|
playerId: z.ZodString;
|
|
@@ -691,6 +703,32 @@ interface LobbyPlayer {
|
|
|
691
703
|
avatar?: AvatarView;
|
|
692
704
|
/** Team index once the game has started; null/absent in the lobby. */
|
|
693
705
|
teamIndex?: number | null;
|
|
706
|
+
/**
|
|
707
|
+
* Equipped avatar-frame slug (e.g. "frame-sadu") — resolves to art via /assets/store/catalog.json
|
|
708
|
+
* (the catalog is the ONLY resolver; nothing about the art is derived from the slug). Read from the
|
|
709
|
+
* account once at room entry, so an equip change shows on the player's next join/resume. Absent =
|
|
710
|
+
* frameless. Earnable-class cosmetics only ever ride the room wire (D-059) — a paid exclusive can
|
|
711
|
+
* never appear here, so there is no receipt check in the broadcast path and no have/have-not row
|
|
712
|
+
* between children on one sofa. (gs#84 contract, D-060.)
|
|
713
|
+
*/
|
|
714
|
+
frame?: string;
|
|
715
|
+
/**
|
|
716
|
+
* This room kicked this exact person before — a HINT for the host, never a gate (the ruling is
|
|
717
|
+
* "label the re-knock, no ban list"). **Present ONLY in the HOST's copy of the frame**: telling a
|
|
718
|
+
* whole room who was once removed is a social leak, not a moderation aid.
|
|
719
|
+
*
|
|
720
|
+
* The same bit also rides `joinRequest` for the immediate ping. This one is the DURABLE carrier —
|
|
721
|
+
* it is RE-READ from the answer stored on the seat (decided once, at join; never recomputed by
|
|
722
|
+
* re-matching, which goes stale) and re-sent on every snapshot, so it survives a host reconnect,
|
|
723
|
+
* where a cached one-shot event would not. A client should prefer this and treat the event as a
|
|
724
|
+
* nudge. MID-GAME there are no lobby frames: there a host who resumes is re-sent the pending
|
|
725
|
+
* `joinRequest`s instead (labels included), so the durable promise holds in both halves.
|
|
726
|
+
*
|
|
727
|
+
* Absent means "no reason to think so", which includes the deliberate miss: an anonymous player who
|
|
728
|
+
* force-quits and rejoins reads as a stranger. Matched only on exact socket/sign-in identity — never
|
|
729
|
+
* a display name or IP, because a wrong label is worse than none.
|
|
730
|
+
*/
|
|
731
|
+
wasKicked?: boolean;
|
|
694
732
|
}
|
|
695
733
|
/**
|
|
696
734
|
* Who is in the room, carried ON the game-state push (gs#71).
|
|
@@ -719,6 +757,9 @@ interface RosterEntry {
|
|
|
719
757
|
isHost?: boolean;
|
|
720
758
|
/** Is this person currently connected? Mirrors `LobbyPlayer.connected`. */
|
|
721
759
|
connected?: boolean;
|
|
760
|
+
/** Equipped avatar-frame slug — mirrors `LobbyPlayer.frame`, because the roster is the MID-GAME
|
|
761
|
+
* carrier (gs#71): a cosmetic with no mid-game carrier silently vanishes for watchers and the TV. */
|
|
762
|
+
frame?: string;
|
|
722
763
|
}
|
|
723
764
|
/** Per-recipient context attached to lobby/state pushes. */
|
|
724
765
|
interface YouContext {
|
|
@@ -786,6 +827,8 @@ interface TeamView {
|
|
|
786
827
|
/** The player on THIS team whose turn it is to answer (rotates each turn); null if the team has
|
|
787
828
|
* no seated players. In controller mode only this player may `answer` on the team's turn. */
|
|
788
829
|
currentPlayerId: string | null;
|
|
830
|
+
/** استريح (gs#80): this team's player who sits out their next question, if a rival named one. */
|
|
831
|
+
restedPlayerId?: string | null;
|
|
789
832
|
/** The seated player's privacy-safe avatar (relay-enriched; engine view omits it). */
|
|
790
833
|
avatar?: AvatarView;
|
|
791
834
|
}
|
|
@@ -797,6 +840,10 @@ interface QuestionView {
|
|
|
797
840
|
selectedAnswerIndex: number | null;
|
|
798
841
|
/** Only present once revealed (anti-cheat). Null while the question is live. */
|
|
799
842
|
correctAnswerIndex: number | null;
|
|
843
|
+
/** «معلومة» — optional explanation shown AFTER the reveal. Null while the question is live (it
|
|
844
|
+
* usually names the answer — same anti-cheat gate as correctAnswerIndex) and null when the
|
|
845
|
+
* question simply has none, which is most of the bank: render the card only when non-null. */
|
|
846
|
+
explain?: string | null;
|
|
800
847
|
imageName?: string;
|
|
801
848
|
videoName?: string;
|
|
802
849
|
/** For video questions: seconds into the clip to stop playback (so the answer isn't shown). Absent = play through. */
|
|
@@ -958,6 +1005,7 @@ type ServerMsg = {
|
|
|
958
1005
|
t: 'joinRequest';
|
|
959
1006
|
playerId: string;
|
|
960
1007
|
name: string;
|
|
1008
|
+
wasKicked?: boolean;
|
|
961
1009
|
} | {
|
|
962
1010
|
t: 'joined';
|
|
963
1011
|
youId: string;
|
|
@@ -983,6 +1031,7 @@ type ServerMsg = {
|
|
|
983
1031
|
hostId: string;
|
|
984
1032
|
hostName: string;
|
|
985
1033
|
hostAvatar?: AvatarView;
|
|
1034
|
+
hostFrame?: string;
|
|
986
1035
|
hostPlaying?: boolean;
|
|
987
1036
|
hostPinned?: boolean;
|
|
988
1037
|
players: LobbyPlayer[];
|
|
@@ -994,6 +1043,9 @@ type ServerMsg = {
|
|
|
994
1043
|
tally?: {
|
|
995
1044
|
[playerId: string]: number;
|
|
996
1045
|
};
|
|
1046
|
+
tallyNames?: {
|
|
1047
|
+
[playerId: string]: string;
|
|
1048
|
+
};
|
|
997
1049
|
modeId: ModeId | null;
|
|
998
1050
|
you: YouContext;
|
|
999
1051
|
} | {
|
|
@@ -1040,10 +1092,16 @@ type ServerMsg = {
|
|
|
1040
1092
|
message: string;
|
|
1041
1093
|
level?: 'info' | 'warn' | 'maintenance';
|
|
1042
1094
|
restartInSec?: number;
|
|
1043
|
-
}
|
|
1095
|
+
}
|
|
1096
|
+
/** `reason` (gs#83, additive): today only `'ended'`, and only on `no_room` — the code WAS a room
|
|
1097
|
+
* until recently (stale WhatsApp link, knock after the host closed the night). Absent = "no such
|
|
1098
|
+
* room as far as we know". Lets a client say «انتهى اللعب» instead of «ما في غرفة بهذا الكود»
|
|
1099
|
+
* without guessing. Old clients ignore it. */
|
|
1100
|
+
| {
|
|
1044
1101
|
t: 'error';
|
|
1045
1102
|
code: string;
|
|
1046
1103
|
message: string;
|
|
1104
|
+
reason?: string;
|
|
1047
1105
|
};
|
|
1048
1106
|
|
|
1049
|
-
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type PackView, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
|
|
1107
|
+
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type PackView, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame, normalizeWesternDigits };
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,8 @@ var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
|
|
|
38
38
|
var CHARACTER_IDS = ["m", "f"];
|
|
39
39
|
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
40
40
|
var UNSAFE_CHARS = /[\p{Cc}\u202A-\u202E\u2066-\u2069<>]/gu;
|
|
41
|
-
var
|
|
41
|
+
var normalizeWesternDigits = (s) => s.replace(/[٠-٩۰-۹]/g, (d) => String("\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) >= 0 ? "\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) : "\u06F0\u06F1\u06F2\u06F3\u06F4\u06F5\u06F6\u06F7\u06F8\u06F9".indexOf(d)));
|
|
42
|
+
var DisplayName = z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform((s) => normalizeWesternDigits(s.replace(UNSAFE_CHARS, "")).trim()).refine((s) => s.length >= 1, "name is empty after sanitizing");
|
|
42
43
|
var RoomCode = z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
|
|
43
44
|
var PlayerId = z.string().max(40);
|
|
44
45
|
var HelpType = z.enum([
|
|
@@ -52,7 +53,7 @@ var HelpType = z.enum([
|
|
|
52
53
|
]);
|
|
53
54
|
var Pv = z.number().int().min(1).max(1e6).optional();
|
|
54
55
|
var IdToken = z.string().max(4096).optional();
|
|
55
|
-
var Platform = z.enum(["web", "ios", "android"]).optional();
|
|
56
|
+
var Platform = z.enum(["web", "ios", "android", "tv"]).optional();
|
|
56
57
|
var AppVersion = z.string().trim().max(24).regex(/^[0-9]+(\.[0-9]+){0,3}([-+][0-9A-Za-z.-]+)?$/, "bad app version").optional();
|
|
57
58
|
var Avatar = z.object({
|
|
58
59
|
emoji: z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
|
|
@@ -179,7 +180,11 @@ var ClientMsg = z.discriminatedUnion("t", [
|
|
|
179
180
|
// trivia is 4-option (0–3)
|
|
180
181
|
z.object({ t: z.literal("useHelp"), helpType: HelpType }),
|
|
181
182
|
// sabotage a rival (active team only): spend a sabotage help to debuff targetTeam's next question
|
|
182
|
-
|
|
183
|
+
// `player` (gs#80): WHICH member of the target team sits out — required by استريح/restPlayer and
|
|
184
|
+
// ignored by عرجوب/tripLevel, which targets a whole team. Optional on the wire so pre-gs#80 clients
|
|
185
|
+
// are unchanged; the engine refuses to spend a restPlayer card without a valid one, so an old client
|
|
186
|
+
// cannot burn a lifeline by omitting it.
|
|
187
|
+
z.object({ t: z.literal("sabotage"), helpType: HelpType, targetTeam: z.number().int().min(0).max(LIMITS.MAX_PLAYERS - 1), player: z.string().max(64).optional() }),
|
|
183
188
|
// 0-based team index
|
|
184
189
|
// moderation
|
|
185
190
|
// Report another player in the room (App Store 1.2 UGC — display names). Available in lobby or
|
|
@@ -237,5 +242,6 @@ export {
|
|
|
237
242
|
ROOM_ALPHABET,
|
|
238
243
|
gameIdForPv,
|
|
239
244
|
gamesForPv,
|
|
240
|
-
modesForGame
|
|
245
|
+
modesForGame,
|
|
246
|
+
normalizeWesternDigits
|
|
241
247
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegeem/protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "Geem's wire protocol — shared zod schemas + TypeScript types for talking to the Geem game server over Socket.IO. The source of truth all clients implement against.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://geem.tv",
|