@homie-rocks/studio 0.46.0 → 0.46.2

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/rules/rules.ts CHANGED
@@ -205,6 +205,8 @@ export interface RoomSettings {
205
205
  durability: { movementSeconds: number };
206
206
  budget: { tick: number };
207
207
  predict: { idleHold?: boolean; catchM: number | null; catchUp: number; snapM: number | null; blendMs: number; interpMs: number | null };
208
+ /** The studio's own pace on what one player sends, messages a second by frame kind (`in`, `ev`, ...). Absent: none. */
209
+ rates?: Record<string, number>;
208
210
  }
209
211
  /**
210
212
  * The default `budget.tick`: 500,000 units at 20 ticks a second or fewer, and less at a faster rate, so a second of
@@ -238,8 +240,16 @@ export function roomSettings(raw: unknown): { settings: RoomSettings; problems:
238
240
  if (!Number.isFinite(n) || n < 0) { problems.push(`"room.${name}" is a number, zero or more; using its default`); return d; }
239
241
  return n;
240
242
  };
241
- const known = new Set(['host', 'offline', 'tickHz', 'inputHz', 'durability', 'budget', 'predict', 'view', 'lifetime', 'characters']);
242
- for (const key of Object.keys(r)) if (!known.has(key)) problems.push(`"room.${key}" is not a setting (host, offline, tickHz, inputHz, durability, budget, predict, view)`);
243
+ const known = new Set(['host', 'offline', 'tickHz', 'inputHz', 'durability', 'budget', 'predict', 'view', 'lifetime', 'characters', 'rates']);
244
+ for (const key of Object.keys(r)) if (!known.has(key)) problems.push(`"room.${key}" is not a setting (host, offline, tickHz, inputHz, durability, budget, predict, view, rates)`);
245
+ let rates: Record<string, number> | undefined;
246
+ if (r.rates !== undefined) {
247
+ if (!r.rates || typeof r.rates !== 'object' || Array.isArray(r.rates)) problems.push('"room.rates" is an object of messages a second per player by kind ({ "in": 60 }); using none');
248
+ else for (const [kind, v] of Object.entries(r.rates)) {
249
+ if (!/^[a-z][a-z-]{0,15}$/.test(kind) || typeof v !== 'number' || !Number.isFinite(v) || v < 0) problems.push(`"room.rates.${kind}" is a number of messages a second, zero or more; leaving it out`);
250
+ else (rates ??= {})[kind] = v;
251
+ }
252
+ }
243
253
  let host: 'server' | 'browser' = 'server';
244
254
  if (r.host !== undefined) { if (r.host === 'server' || r.host === 'browser') host = r.host; else problems.push('"room.host" is "server" or "browser"; using "server"'); }
245
255
  if (r.lifetime !== undefined && !['match', 'persistent'].includes(r.lifetime)) problems.push('"room.lifetime" is "match" or "persistent"');
@@ -258,7 +268,7 @@ export function roomSettings(raw: unknown): { settings: RoomSettings; problems:
258
268
  const p = r.predict && typeof r.predict === 'object' ? r.predict : {};
259
269
  return {
260
270
  settings: {
261
- host, offline, tickHz, inputHz, ...(r.lifetime === 'persistent' ? { lifetime: 'persistent' as const } : {}), ...(r.characters === true ? { characters: true } : {}),
271
+ host, offline, tickHz, inputHz, ...(rates ? { rates } : {}), ...(r.lifetime === 'persistent' ? { lifetime: 'persistent' as const } : {}), ...(r.characters === true ? { characters: true } : {}),
262
272
  view: { radiusM: r.view?.radiusM === null ? null : num('view.radiusM', r.view?.radiusM, null), ...(r.view?.precisionM !== undefined ? { precisionM: num('view.precisionM', r.view.precisionM, 0) as number } : {}), ...(r.view?.nearM !== undefined ? { nearM: num('view.nearM', r.view.nearM, null) } : {}), ...(r.view?.farHz !== undefined ? { farHz: whole('view.farHz', r.view.farHz, 1, tickHz, tickHz) } : {}) },
263
273
  durability: { movementSeconds: whole('durability.movementSeconds', r.durability?.movementSeconds, 1, 60, r.lifetime === 'persistent' ? 30 : 1) },
264
274
  budget: { tick: Math.max(1, Math.floor(num('budget.tick', r.budget?.tick, budgetFor(tickHz)) as number)) },
@@ -0,0 +1,20 @@
1
+ /** Bounded stage samples. Frozen runtime clocks are unavailable, never zero CPU. */
2
+ export function stageTimings(now = () => globalThis.performance?.now?.() ?? 0, limit = 256) {
3
+ const stages = new Map();
4
+ return {
5
+ now,
6
+ record(stage, start) {
7
+ const ms = Math.max(0, now() - start);
8
+ let sample = stages.get(stage);
9
+ if (!sample) { sample = { values: [], next: 0 }; stages.set(stage, sample); }
10
+ sample.values[sample.next] = ms; sample.next = (sample.next + 1) % limit;
11
+ },
12
+ facts() {
13
+ const advancing = [...stages.values()].some(({ values }) => values.some(ms => ms > 0));
14
+ return { clock: advancing ? 'performance' : 'unavailable-or-below-resolution', ms: Object.fromEntries([...stages].map(([name, { values }]) => {
15
+ const sorted = [...values].sort((a,b)=>a-b);
16
+ return [name, advancing ? { samples: sorted.length, p50: sorted[Math.floor(sorted.length*.5)], p95: sorted[Math.floor(sorted.length*.95)], max: sorted.at(-1) } : null];
17
+ })) };
18
+ }
19
+ };
20
+ }
@@ -0,0 +1,2 @@
1
+ // Generated by lib/build-identity.mjs; identifies the bundled runtime sources.
2
+ export const STUDIO_BUILD = Object.freeze({"version":"0.46.2","sha256":"335192af92b18b0857b7a32abc28017670a609737ef888a592f78e75561d16f0"});