@typetorch/framework 0.5.6 → 0.6.0

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.
Files changed (53) hide show
  1. package/README.md +36 -3
  2. package/out/analytics/SCHEMA.md +6 -2
  3. package/out/analytics/join.d.ts +69 -0
  4. package/out/analytics/join.luau +178 -0
  5. package/out/analytics/rows.d.ts +14 -0
  6. package/out/analytics/rows.luau +29 -0
  7. package/out/analytics/schema.d.ts +4 -1
  8. package/out/analytics/server.d.ts +10 -0
  9. package/out/analytics/server.luau +216 -29
  10. package/out/analytics/settings.luau +11 -0
  11. package/out/devtools/admin-ui.d.ts +22 -0
  12. package/out/devtools/admin-ui.luau +108 -1
  13. package/out/devtools/build-actions.d.ts +14 -0
  14. package/out/devtools/build-actions.luau +25 -0
  15. package/out/devtools/claude-ui.d.ts +5 -0
  16. package/out/devtools/claude-ui.luau +37 -7
  17. package/out/devtools/client.luau +144 -14
  18. package/out/devtools/commands.d.ts +115 -0
  19. package/out/devtools/commands.luau +504 -0
  20. package/out/devtools/layout.d.ts +11 -0
  21. package/out/devtools/layout.luau +22 -0
  22. package/out/devtools/palette-commands.d.ts +55 -0
  23. package/out/devtools/palette-commands.luau +667 -0
  24. package/out/devtools/palette-rules.d.ts +57 -0
  25. package/out/devtools/palette-rules.luau +103 -0
  26. package/out/devtools/palette.d.ts +26 -0
  27. package/out/devtools/palette.luau +469 -0
  28. package/out/devtools/panes.d.ts +21 -1
  29. package/out/devtools/panes.luau +111 -14
  30. package/out/devtools/sha256.d.ts +1 -8
  31. package/out/devtools/sha256.luau +15 -89
  32. package/out/index.d.ts +1 -0
  33. package/out/kernel.d.ts +6 -0
  34. package/out/notifications/api.d.ts +50 -0
  35. package/out/notifications/api.luau +17 -0
  36. package/out/notifications/client.d.ts +21 -0
  37. package/out/notifications/client.luau +201 -0
  38. package/out/notifications/index.d.ts +8 -0
  39. package/out/notifications/init.luau +34 -0
  40. package/out/notifications/protocol.d.ts +146 -0
  41. package/out/notifications/protocol.luau +519 -0
  42. package/out/notifications/server.d.ts +73 -0
  43. package/out/notifications/server.luau +856 -0
  44. package/out/notifications/token.d.ts +57 -0
  45. package/out/notifications/token.luau +349 -0
  46. package/out/runtime/start.luau +5 -2
  47. package/out/typetorch.d.ts +9 -0
  48. package/out/typetorch.luau +37 -3
  49. package/out/util/sha256.d.ts +8 -0
  50. package/out/util/sha256.luau +91 -0
  51. package/out/version.d.ts +1 -1
  52. package/out/version.luau +1 -1
  53. package/package.json +14 -3
package/README.md CHANGED
@@ -185,6 +185,16 @@ return the generation's stop function, which the kernel calls on a swap.
185
185
  build picker (Server > Branch) and a Claude prompt. The sidebar, top to bottom: Artifact, Server, Network, Modules,
186
186
  Manage, Logs, Dex, Claude. The window can be dragged by its header and resized from its corner (double-tap the
187
187
  header to reset).
188
+ - **Command palette (`Ctrl+K`, `Cmd+K` on a Mac, or the search button in the menu header):** opens the dev menu if it
189
+ is closed and searches every command the dev may run now (fuzzy: `swb` finds *Switch branch*). Up / Down pick,
190
+ Enter runs, Shift+Enter opens a page in a split, Escape closes; recent commands come first on an empty query. Never
191
+ fires while typing in chat or a text box. Commands: open any page; split right / down, open the pane in a new window,
192
+ close it, dock all windows; Reload, Roll back, switch branch, load a build here, join a server on a branch or build
193
+ (the Branch tab's rules and words, `devtools/build-actions.ts`), New server and Join a listed server (owners); find a
194
+ player by name, display name or UserId and open them in Manage > Players or their Kick / Ban card (owners); copy the
195
+ job id, artifact id, commit, branch or place version (the copy popup); "Ask Claude: ..." puts the query in the Claude
196
+ draft. Anything that swaps what runs on the server takes a second Enter or tap to confirm. The kernel and the server
197
+ re-check every request. Core: `devtools/commands.ts` (pure, Lune-tested by `scripts/test-commands.luau`).
188
198
  - **Server TPS (framework 0.4.2, kernel 0.4.2):** Server > Status has a *TPS* line next to Memory and Lua heap: the
189
199
  last minute's average, its slowest second and the physics FPS (`59.9 avg, 52.0 min, physics 60`; the warning colour
190
200
  under 50 on average). Older kernels show `needs kernel 0.4.2`. The same numbers ride on fleet heartbeats (the
@@ -350,6 +360,27 @@ TypeTorch.runDetached(() => store.StartSessionAsync(`Player_${userId}`)).then((p
350
360
  run at once per server (then it throws); one past 60 s is logged and flagged in Server > Status. Older kernels
351
361
  throw "needs kernel 0.3.8; use the DataHost job queue (Player data guide)": the other option, which works on every
352
362
  kernel. `features.runDetached` tells which one this server has.
363
+ - **`notifications()`** (plans/29): native Roblox experience notifications through the TypeTorch backend (`backend.url`
364
+ / `backend.key`, the launch-token keys in `backend.notify` from `typetorch notify key rotate`; no kernel change).
365
+ The game says why a player should hear from it, the backend decides when and sends, the framework verifies the tap:
366
+
367
+ ```ts
368
+ const notify = TypeTorch.notifications();
369
+ // Server: intents (upsert by user + kind + key; offline players by UserId), batched and swap-safe.
370
+ notify.want(userIdOrPlayer, "gift_new", { key: letterId, until: expiresAt, params: { gift: "5 coins" }, data: `mail:${letterId}` });
371
+ notify.drop(userIdOrPlayer, "streak", "20034", "played"); // no key: every key of the kind
372
+ trove.add(notify.onOpen((player, open) => openMail(player, open.data))); // open: { nid, kind, data, ageSeconds, first }
373
+ // Client: Roblox's opt-in prompt, once per session (show your own explainer first). Yields until it closes.
374
+ const result = TypeTorch.notifications().promptOptIn("streak_2"); // "shown" | "unavailable" | "error" | "busy" | "already"
375
+ ```
376
+
377
+ Kinds are declared in typetorch.json `notifications` and pushed with the CLI. Intents go to
378
+ `<backend.url>/v1/notify/intents` every `flushSeconds` (and at 200); presence (`online` on join and every 3 h, `offline` on leave)
379
+ rides along while `backend.notify` is set. A join with `tt1n.` launch data is verified on the server (HMAC, the
380
+ player's UserId, 14 days; the previous key for 14 days after a rotation): `onOpen` gets only verified data, also for
381
+ players who joined before the callback connected. Rows (`kind = notify`): `want`, `drop`, `optin_check`,
382
+ `optin_shown`, `optin_closed`, `open`, `open_bad`, `played` (src/analytics/SCHEMA.md); the join row gets
383
+ `from = "notification"`. Without backend settings (Studio) nothing is posted; rows are still written.
353
384
  - **Edit mode** (UI Labs stories, no kernel): `running` is false, identity has defaults, `persist` keeps a local table,
354
385
  events never fire, and the server-only reads throw.
355
386
 
@@ -510,7 +541,7 @@ const variant = analytics.experiment("onboarding", ["short", "long"]); // same a
510
541
  when the player is alone) intersected with the players there, not a web call per player.
511
542
 
512
543
  **Settings, framework 0.4.0 / kernel 0.4.0 (plans/21):** the record's `backend` = `{ url, key, analytics?: { flushSeconds,
513
- recordShare } }` (`typetorch backend setup`) is the whole sink: DuckDB at `<url>/v1/ingest` with the one key (Basin
544
+ recordShare, joinLaunchPrefix } }` (`typetorch backend setup`) is the whole sink: DuckDB at `<url>/v1/ingest` with the one key (Basin
514
545
  identities to `<url>/v1/identity`). The old `analytics` value below is read while `backend` is missing, and stays the
515
546
  way to a Basin sink. The engine also tells the kernel each player's pid (`setAnalyticsId`: the kernel's error reports
516
547
  count affected players by pid, never by name) and counts its own DataStore and HTTP requests for the budget view (dev
@@ -579,8 +610,10 @@ bun run build # rbxtsc --type package -> out/
579
610
  vouch, live-copy rules), `test-messaging` (messaging and the server list), `test-settings` (`liveConfig` and the
580
611
  settings lines), `test-detached` (`runDetached`), `test-dev-access` (the dev-only tools rule and every gate through
581
612
  it), `test-remote-debug` (the remote debug ops and their limits), `test-net-inspect` (packet capture),
582
- `test-explorer-core` (Dex), `test-toolbox-sanitize` (the Creator Store sanitizer) and `test-sha256` (the pairing-code
583
- fingerprint). `node scripts/check-self-calls.mjs out` flags compiled functions that call themselves on their first
613
+ `test-explorer-core` (Dex), `test-toolbox-sanitize` (the Creator Store sanitizer), `test-sha256` (SHA-256 / HMAC in util and the
614
+ pairing-code fingerprint), `test-notifications` (the launch token with the contract's shared vector, intent checks,
615
+ the queue: batches of 200, rev order, a swap's handover), `test-notify-server` (the notifications server against
616
+ mocked services) and `test-analytics-join` (the join row's metadata; no other player's UserId). `node scripts/check-self-calls.mjs out` flags compiled functions that call themselves on their first
584
617
  lines (after `bun run build`).
585
618
  - **Package contents:** only `out/` (no `.tsbuildinfo`), `README.md` and `LICENSE`; check with `bun pm pack --dry-run`.
586
619
  - **Explorer class icons:** `assets/class-icons.png` (kept locally, not in the repo: it is Roblox's own texture) is
@@ -15,7 +15,7 @@ Integers are written with every digit (never exponent notation).
15
15
  |---|---|---|
16
16
  | `v` | int32 | Row format version: `1` |
17
17
  | `t` | int64 | Unix ms on the game server's clock. Client events are corrected by the server (`server now - age - ping/2`) |
18
- | `kind` | string | `session`, `tech`, `zone`, `funnel`, `purchase`, `currency`, `state`, `experiment`, `custom`, `recording_meta`, `fleet` |
18
+ | `kind` | string | `session`, `tech`, `zone`, `funnel`, `purchase`, `currency`, `state`, `experiment`, `custom`, `recording_meta`, `fleet`, `notify` |
19
19
  | `name` | string | Event name (1-64 chars), see below |
20
20
  | `pid` | string | Random player id (32 hex chars; `t` + 31 hex = temporary, the player-id store couldn't be read). `""` for server-only events. Never the UserId |
21
21
  | `sid` | string | Session id (32 hex), new per join, kept across hot swaps. `""` for server-only events |
@@ -57,8 +57,11 @@ First-ever-session detail, one row per packed chunk.
57
57
 
58
58
  | kind | name | props |
59
59
  |---|---|---|
60
- | session | `join` | `from`: `direct` (Home, search, sorts: Roblox doesn't say), `teleport` (same game), `teleport_game` (another game), `referral` (invite/referral link), `share` (launch data), `follow` (followed someone); `ctx?` (GameJoinContext.JoinSource name); `tp?` (had teleport data); `party?` (players teleported along); `age` (account age: `<1d`, `1-7d`, `7-30d`, `30-90d`, `90-365d`, `1-3y`, `3y+`); `prem` (Premium); `country?` (ISO code); `friends` (how many of the player's friends are in the server: one friend-list lookup per join, at most 10 pages, `0` when it fails); `ret` (days since the last visit, `-1` first visit or unknown); `late?` (the engine started over 60 s after the join). `t` is the join time |
60
+ | session | `join` | `from`: `direct` (Home, search, sorts: Roblox doesn't say), `teleport` (same game), `teleport_game` (another game), `referral` (invite/referral link), `notification` (a TypeTorch notification's launch data verified), `share` (other launch data), `follow` (followed someone); join metadata (plans/29): `src_place?`, `src_game?` (SourcePlaceId / SourceGameId), `ref?` / `follow?` (the referrer / followed user as their pid when this game knows them, else `"other"`: never a UserId; players in the server are free, others cost one DataStore read each, at most 2 per join and only with read budget to spare), `party?` (players teleported along) + `party_pids?` (the pids of those this game knows, at most 10), `ctx?`, `ctx_item?`, `ctx_asset?`, `ctx_outfit?`, `ctx_asset_type?` (GameJoinContext JoinSource / ItemType / AssetId / OutfitId / AssetType), `launch?` (`notification` + `nid`, `nk` (kind); `tt_bad` + `launch_bad` (`format`, `sig`, `user`, `old`); or the game's own launch data: `launch_len`, plus its first 32 chars in `launch` only with the settings' `joinLaunchPrefix`), `tp?` + `tp_bytes?` (TeleportData present, its JSON size; never the content), `age_days` (exact account age), `locale?`, `verified?` (HasVerifiedBadge), `membership` (MembershipType name); `age` (account age: `<1d`, `1-7d`, `7-30d`, `30-90d`, `90-365d`, `1-3y`, `3y+`); `prem` (Premium); `country?` (ISO code); `friends` (how many of the player's friends are in the server: one friend-list lookup per join, at most 10 pages, `0` when it fails); `ret` (days since the last visit, `-1` first visit or unknown); `late?` (the engine started over 60 s after the join). `t` is the join time |
61
61
  | session | `device` (src client) | `dev?` (the device class, as the `dev` column; frameworks after 0.4.2, older ones leave it out), `input` (`kbm`, `touch`, `gamepad`, `vr`, `unknown`), `w`, `h` (viewport points), `touch`, `kb`, `mouse`, `pad`, `vr`. Once per session. The class (Roblox gives game scripts no OS): `vr` when VRService.VREnabled; `console` when GuiService:IsTenFootInterface(); touch without a keyboard: `tablet` when the viewport's short side is 600 points or more, else `phone` (the first hello waits up to 2 s for a real viewport); a keyboard or mouse: `desktop` (touch laptops, tablets with a keyboard); else `unknown` |
62
+ | notify | `want`, `drop` | Notifications (plans/29), written by the game server: `k` (kind), `key?` (first 12 hex of SHA-256 of the intent's key; never the key), `at?`, `until?`, `offline` (want: the user isn't in this server; the row then has no pid), `why?` (drop) |
63
+ | notify | `optin_check`, `optin_shown`, `optin_closed` | The client's opt-in prompt, checked and written by the server (at most 3 of each per session): `where`, `can` (check), `err?` (`timeout`, `swap` or the error's first 64 chars), `secs` (closed: how long it was up) |
64
+ | notify | `open`, `open_bad`, `played` | A join from a notification: `open` (`nid`, `k`, `age` (secs since sent), `first`), `open_bad` (`why`: `format`, `sig`, `user`, `old`), `played` (`nid`, `k`, `secs`: the opened session's length, at its end). Backend rows of the same kind (`planned`, `sent`, ...) come from `src = "backend"` |
62
65
  | session | `leave` | `secs` (session length), `why`: `left`, `shutdown`, `gone` (left while no generation ran the engine), `swap60?` (left within 60 s of a hot swap) |
63
66
  | tech | `server` | Every `techEvery` s: `fps` (physics), `hb` (Heartbeat rate), `mem` (MB, 10s), `players`, `ping50?`, `ping90?` (ms), `q`, `qr` (queued events, recordings), `dropped`, `sent`, `fails`, `health?`, `errors?` (kernel 0.3.2 health) |
64
67
  | tech | `client` (src client) | Every `techEvery` s: `fps`, `mem` (MB, 10s), `ping?` (ms) |
@@ -159,6 +162,7 @@ repo's `bun run local` does it). Every new signed copy applies at once, so dials
159
162
  | `experiments` | none | Per experiment: `active: false` (everyone gets the first variant, not stamped), `weights` (per variant, in the game's order), `variant` (force one), `players` (`{ [UserId or pid]: variant }`, at most 200: forces one player's variant over everything else, stamped `forced`; each client's hello gets only its own entry). The CLI writes them: `typetorch experiment` |
160
163
  | `identity` | the settings' `fleet` url + `/v1/identity` | basin: where identity rows go (below); none and no `fleet` in the settings: not sent. duckdb: unused |
161
164
  | `identityToken` | the settings' `fleet` token | basin: the token for `identity`. Never logged |
165
+ | `joinLaunchPrefix` | false | true: the join row's `launch` keeps the first 32 characters of the game's own launch data (off: only `launch_len`). Set it in `backend.analytics` (or the old `analytics` value) |
162
166
 
163
167
  Unknown fields are ignored. Settings errors are warned once (never with the token).
164
168
 
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The `session` / `join` row's join metadata (plans/29 "Native join metadata"): every field of `Player:GetJoinData()`
3
+ * and the player facts, as props. The engine reads the data (enum values as their names, the TeleportData's JSON size,
4
+ * other players' pids looked up beforehand) and this builds the props.
5
+ *
6
+ * Privacy: no other player's UserId ever lands in a prop. Referrer, followed user and party members become pids (the
7
+ * game's pseudonymous ids) when this game knows them, else "other" (ref / follow) or nothing (party_pids).
8
+ *
9
+ * Pure: no imports and no services, so it runs under Lune (scripts/test-analytics-join.luau).
10
+ */
11
+ /** What the engine reads from GetJoinData (enums already turned into names). */
12
+ export interface JoinDataLike {
13
+ SourcePlaceId?: number;
14
+ SourceGameId?: number;
15
+ ReferredByPlayerId?: number;
16
+ Members?: number[];
17
+ /** Whether TeleportData was present (its content never leaves the server). */
18
+ HasTeleportData?: boolean;
19
+ LaunchData?: string;
20
+ GameJoinContext?: {
21
+ JoinSource?: string;
22
+ ItemType?: string;
23
+ AssetId?: string;
24
+ OutfitId?: string;
25
+ AssetType?: string;
26
+ };
27
+ }
28
+ /** The joining player and this server. */
29
+ export interface JoinFacts {
30
+ userId: number;
31
+ /** game.GameId (a teleport from another game is `teleport_game`). */
32
+ gameId: number;
33
+ followUserId: number;
34
+ /** Player.AccountAge (days). */
35
+ accountAge: number;
36
+ locale?: string;
37
+ verified?: boolean;
38
+ /** Enum.MembershipType name. */
39
+ membership?: string;
40
+ /** The TeleportData's JSON size in bytes (undefined: none or not encodable). */
41
+ tpBytes?: number;
42
+ }
43
+ /** The launch data's verdict (notifications/token.ts verifyToken): undefined when it isn't a TypeTorch token. */
44
+ export type LaunchVerdict = {
45
+ ok: true;
46
+ nid: string;
47
+ kind: string;
48
+ } | {
49
+ ok: false;
50
+ why: string;
51
+ } | undefined;
52
+ export interface JoinOptions {
53
+ /**
54
+ * Keep the first LAUNCH_KEEP chars of the game's own launch data in `launch` (the analytics settings'
55
+ * `joinLaunchPrefix`, default off: launch data can carry anything a link maker put there). `launch_len` is always kept.
56
+ */
57
+ launchPrefix?: boolean;
58
+ }
59
+ /** Party pids kept at most. */
60
+ export declare const PARTY_PIDS_MAX = 10;
61
+ /** The game's own launch data: this many characters are kept. */
62
+ export declare const LAUNCH_KEEP = 32;
63
+ /** UserIds whose pids the row wants (referrer, followed user, party members), without the player and duplicates. */
64
+ export declare function wantedUserIds(data: JoinDataLike | undefined, facts: JoinFacts): number[];
65
+ /**
66
+ * The join props. `pidOf(userId)` answers a known pid (a player in this server, or one this game's records know) or
67
+ * undefined.
68
+ */
69
+ export declare function joinProps(data: JoinDataLike | undefined, facts: JoinFacts, pidOf: (userId: number) => string | undefined, launch: LaunchVerdict, options?: JoinOptions): Record<string, unknown>;
@@ -0,0 +1,178 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ --[[
3
+ *
4
+ * The `session` / `join` row's join metadata (plans/29 "Native join metadata"): every field of `Player:GetJoinData()`
5
+ * and the player facts, as props. The engine reads the data (enum values as their names, the TeleportData's JSON size,
6
+ * other players' pids looked up beforehand) and this builds the props.
7
+ *
8
+ * Privacy: no other player's UserId ever lands in a prop. Referrer, followed user and party members become pids (the
9
+ * game's pseudonymous ids) when this game knows them, else "other" (ref / follow) or nothing (party_pids).
10
+ *
11
+ * Pure: no imports and no services, so it runs under Lune (scripts/test-analytics-join.luau).
12
+
13
+ ]]
14
+ --* What the engine reads from GetJoinData (enums already turned into names).
15
+ --* The joining player and this server.
16
+ --* The launch data's verdict (notifications/token.ts verifyToken): undefined when it isn't a TypeTorch token.
17
+ --* Party pids kept at most.
18
+ local PARTY_PIDS_MAX = 10
19
+ --* The game's own launch data: this many characters are kept.
20
+ local LAUNCH_KEEP = 32
21
+ --* UserIds whose pids the row wants (referrer, followed user, party members), without the player and duplicates.
22
+ local function wantedUserIds(data, facts)
23
+ local seen = {}
24
+ local list = {}
25
+ local add = function(userId)
26
+ local _userId = userId
27
+ local _condition = not (type(_userId) == "number")
28
+ if not _condition then
29
+ _condition = userId <= 0 or userId == facts.userId
30
+ if not _condition then
31
+ local _userId_1 = userId
32
+ _condition = seen[_userId_1] ~= nil
33
+ end
34
+ end
35
+ if _condition then
36
+ return nil
37
+ end
38
+ local _userId_1 = userId
39
+ seen[_userId_1] = true
40
+ local _userId_2 = userId
41
+ table.insert(list, _userId_2)
42
+ end
43
+ local _result = data
44
+ if _result ~= nil then
45
+ _result = _result.ReferredByPlayerId
46
+ end
47
+ add(_result)
48
+ add(facts.followUserId)
49
+ local _result_1 = data
50
+ if _result_1 ~= nil then
51
+ _result_1 = _result_1.Members
52
+ end
53
+ if _result_1 ~= nil then
54
+ for _, member in data.Members do
55
+ add(member)
56
+ end
57
+ end
58
+ return list
59
+ end
60
+ --[[
61
+ *
62
+ * The join props. `pidOf(userId)` answers a known pid (a player in this server, or one this game's records know) or
63
+ * undefined.
64
+
65
+ ]]
66
+ local function joinProps(data, facts, pidOf, launch, options)
67
+ if options == nil then
68
+ options = {}
69
+ end
70
+ local info = {}
71
+ local from = "direct"
72
+ local mapped = function(userId)
73
+ local _condition = pidOf(userId)
74
+ if _condition == nil then
75
+ _condition = "other"
76
+ end
77
+ return _condition
78
+ end
79
+ if data ~= nil then
80
+ if data.SourcePlaceId ~= nil and data.SourcePlaceId ~= 0 then
81
+ from = if data.SourceGameId ~= nil and data.SourceGameId ~= facts.gameId then "teleport_game" else "teleport"
82
+ info.src_place = data.SourcePlaceId
83
+ if data.SourceGameId ~= nil and data.SourceGameId ~= 0 then
84
+ info.src_game = data.SourceGameId
85
+ end
86
+ elseif data.ReferredByPlayerId ~= nil and data.ReferredByPlayerId ~= 0 then
87
+ from = "referral"
88
+ elseif launch ~= nil and launch.ok then
89
+ from = "notification"
90
+ elseif data.LaunchData ~= nil and data.LaunchData ~= "" then
91
+ from = "share"
92
+ end
93
+ if data.ReferredByPlayerId ~= nil and data.ReferredByPlayerId > 0 then
94
+ info.ref = mapped(data.ReferredByPlayerId)
95
+ end
96
+ local context = data.GameJoinContext
97
+ if context ~= nil then
98
+ if context.JoinSource ~= nil then
99
+ info.ctx = context.JoinSource
100
+ end
101
+ if context.ItemType ~= nil then
102
+ info.ctx_item = context.ItemType
103
+ end
104
+ if context.AssetId ~= nil and context.AssetId ~= "" then
105
+ info.ctx_asset = context.AssetId
106
+ end
107
+ if context.OutfitId ~= nil and context.OutfitId ~= "" then
108
+ info.ctx_outfit = context.OutfitId
109
+ end
110
+ if context.AssetType ~= nil then
111
+ info.ctx_asset_type = context.AssetType
112
+ end
113
+ end
114
+ if data.HasTeleportData == true then
115
+ info.tp = true
116
+ if facts.tpBytes ~= nil then
117
+ info.tp_bytes = facts.tpBytes
118
+ end
119
+ end
120
+ local members = data.Members
121
+ if members ~= nil and #members > 0 then
122
+ info.party = #members
123
+ local pids = {}
124
+ for _, member in members do
125
+ if member == facts.userId or #pids >= PARTY_PIDS_MAX then
126
+ continue
127
+ end
128
+ local pid = pidOf(member)
129
+ if pid ~= nil and not (table.find(pids, pid) ~= nil) then
130
+ table.insert(pids, pid)
131
+ end
132
+ end
133
+ if #pids > 0 then
134
+ info.party_pids = pids
135
+ end
136
+ end
137
+ local launchData = data.LaunchData
138
+ if launch ~= nil then
139
+ if launch.ok then
140
+ info.launch = "notification"
141
+ info.nid = launch.nid
142
+ info.nk = launch.kind
143
+ else
144
+ info.launch = "tt_bad"
145
+ info.launch_bad = launch.why
146
+ end
147
+ elseif launchData ~= nil and launchData ~= "" then
148
+ if options.launchPrefix == true then
149
+ info.launch = string.sub(launchData, 1, LAUNCH_KEEP)
150
+ end
151
+ info.launch_len = #launchData
152
+ end
153
+ end
154
+ if from == "direct" and facts.followUserId ~= 0 then
155
+ from = "follow"
156
+ end
157
+ if facts.followUserId > 0 then
158
+ info.follow = mapped(facts.followUserId)
159
+ end
160
+ info.from = from
161
+ info.age_days = math.max(0, math.floor(facts.accountAge))
162
+ if facts.locale ~= nil and facts.locale ~= "" then
163
+ info.locale = facts.locale
164
+ end
165
+ if facts.verified ~= nil then
166
+ info.verified = facts.verified
167
+ end
168
+ if facts.membership ~= nil then
169
+ info.membership = facts.membership
170
+ end
171
+ return info
172
+ end
173
+ return {
174
+ wantedUserIds = wantedUserIds,
175
+ joinProps = joinProps,
176
+ PARTY_PIDS_MAX = PARTY_PIDS_MAX,
177
+ LAUNCH_KEEP = LAUNCH_KEEP,
178
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * How built-ins outside the engine (notifications) write rows into the running server engine's events: the engine
3
+ * registers a writer when it starts and removes it when its generation stops (the `status.ts` pattern). Module state is
4
+ * fresh in every generation. Types only, so importing this loads nothing else.
5
+ *
6
+ * `target`: a Player (their session's pid / sid), a UserId (the session of that player when they are in this server;
7
+ * otherwise a server row without a pid: never a raw UserId in `events`), or undefined (a server row).
8
+ */
9
+ export type BuiltinRowKind = "notify";
10
+ export type BuiltinRowWriter = (target: Player | number | undefined, kind: BuiltinRowKind, name: string, props: object) => void;
11
+ /** Registers the engine's writer; the returned function removes it again (only if it is still the registered one). */
12
+ export declare function registerBuiltinRows(fn: BuiltinRowWriter): () => void;
13
+ /** The running server engine's writer, or undefined when no engine runs in this generation (yet). */
14
+ export declare function builtinRowWriter(): BuiltinRowWriter | undefined;
@@ -0,0 +1,29 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ --[[
3
+ *
4
+ * How built-ins outside the engine (notifications) write rows into the running server engine's events: the engine
5
+ * registers a writer when it starts and removes it when its generation stops (the `status.ts` pattern). Module state is
6
+ * fresh in every generation. Types only, so importing this loads nothing else.
7
+ *
8
+ * `target`: a Player (their session's pid / sid), a UserId (the session of that player when they are in this server;
9
+ * otherwise a server row without a pid: never a raw UserId in `events`), or undefined (a server row).
10
+
11
+ ]]
12
+ local writer
13
+ --* Registers the engine's writer; the returned function removes it again (only if it is still the registered one).
14
+ local function registerBuiltinRows(fn)
15
+ writer = fn
16
+ return function()
17
+ if writer == fn then
18
+ writer = nil
19
+ end
20
+ end
21
+ end
22
+ --* The running server engine's writer, or undefined when no engine runs in this generation (yet).
23
+ local function builtinRowWriter()
24
+ return writer
25
+ end
26
+ return {
27
+ registerBuiltinRows = registerBuiltinRows,
28
+ builtinRowWriter = builtinRowWriter,
29
+ }
@@ -9,7 +9,7 @@ export declare const SCHEMA_VERSION = 1;
9
9
  export declare const RECORDING_CODEC = "tt-rec-1";
10
10
  /** Most bytes of the `props` JSON text. Larger props become `{"_trunc":<bytes>}`. */
11
11
  export declare const PROPS_MAX_BYTES = 4096;
12
- export type EventKind = "session" | "tech" | "zone" | "funnel" | "purchase" | "currency" | "state" | "experiment" | "custom" | "recording_meta" | "fleet";
12
+ export type EventKind = "session" | "tech" | "zone" | "funnel" | "purchase" | "currency" | "state" | "experiment" | "custom" | "recording_meta" | "fleet" | "notify";
13
13
  export type DeviceKind = "desktop" | "phone" | "tablet" | "console" | "vr" | "unknown";
14
14
  /** One row of the events table. Flat; every column is always present. */
15
15
  export interface EventRow {
@@ -119,6 +119,8 @@ export interface AnalyticsSettings {
119
119
  identity?: string;
120
120
  /** basin: the token for `identity` (default: the settings' `fleet` token). Never logged. */
121
121
  identityToken?: string;
122
+ /** Keep the first 32 chars of the game's own launch data on the join row (`launch`); default off (`launch_len` always). */
123
+ joinLaunchPrefix?: boolean;
122
124
  }
123
125
  /** AnalyticsSettings with every default filled in. */
124
126
  export interface ResolvedSettings {
@@ -132,6 +134,7 @@ export interface ResolvedSettings {
132
134
  experiments: Map<string, ExperimentOverride>;
133
135
  identity?: string;
134
136
  identityToken?: string;
137
+ joinLaunchPrefix: boolean;
135
138
  }
136
139
  export type AnalyticsValue = string | number | boolean;
137
140
  /** Event properties: a flat JSON object (nested tables work but are harder to query). */
@@ -94,6 +94,8 @@ export declare class ServerAnalytics {
94
94
  private requeueEvents;
95
95
  /** A "fleet" row (server-only, src server) on the fleet queue. */
96
96
  private emitFleet;
97
+ /** A built-in's row (analytics/rows.ts): on the player's session when they are here, else a server row (no pid). */
98
+ private builtinRow;
97
99
  /** The player's session; created now if game code is faster than the engine's own join handler. */
98
100
  sessionOf(player: Player): PlayerSession | undefined;
99
101
  /** Game code's events. `player` undefined: a server-only event (no pid). */
@@ -126,6 +128,14 @@ export declare class ServerAnalytics {
126
128
  private tellKernelPid;
127
129
  /** Kernel 0.4.0: counts this engine's own requests for the budget view (older kernels: nothing). */
128
130
  private countBudget;
131
+ /** A player in this server's pid (free: their session), for the join row's ref / follow / party. */
132
+ private pidHere;
133
+ /**
134
+ * A player not in this server: this game's record of them (one GetAsync, no retries, only with read budget to
135
+ * spare). undefined when unknown (never seen by this game, no budget, or the store can't be read).
136
+ */
137
+ private pidFromRecord;
138
+ /** The join row's join metadata (analytics/join.ts): GetJoinData, the player facts, the launch data's verdict. */
129
139
  private joinSource;
130
140
  /**
131
141
  * How many of the player's friends are in this server: one `GetFriendsAsync` per join (at most FRIEND_PAGES_MAX