@homie-rocks/studio 0.45.2 → 0.45.3

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/worker/room.mjs CHANGED
@@ -484,15 +484,16 @@ export class NetRoom {
484
484
 
485
485
  hasViewSchedule() {
486
486
  const view = this.server?.viewSettings;
487
- return this.server?.viewRadiusM != null || Boolean(view?.precisionM > 0 || view?.nearM != null && view?.farHz != null);
487
+ return this.server?.privateDelivery || this.server?.viewRadiusM != null || Boolean(view?.precisionM > 0 || view?.nearM != null && view?.farHz != null);
488
488
  }
489
489
 
490
490
  /** The same per-player delivery role will run in Gates. Keyframes belong to sockets, not saved seats. */
491
491
  playerSnapshot(client, snap, welcome = false) {
492
492
  if (!snap || !this.hasViewSchedule()) return snap;
493
493
  client.viewSchedule ??= scheduledView(this.server.viewSettings ?? { radiusM: this.server.viewRadiusM }, this.tickHz);
494
- const selected = client.viewSchedule(snap, client.seat);
495
- client.snapEncoder ??= snapshotEncoder(this.tickHz, true);
494
+ let selected = client.viewSchedule(snap, client.seat);
495
+ if(this.server?.projectSnapshot) selected=this.server.projectSnapshot(selected,client.seat);
496
+ client.snapEncoder ??= snapshotEncoder(this.tickHz, !this.server?.privateDelivery);
496
497
  if (welcome) client.snapEncoder.reset();
497
498
  return client.snapEncoder.encode(selected, client.seat);
498
499
  }
@@ -918,11 +919,19 @@ export class NetRoom {
918
919
  this.onChat({ client: c }, m);
919
920
  return;
920
921
  }
922
+ case 'probeAck': {
923
+ if (c.probe && m.n === c.probe.n) {
924
+ const ms = now - c.probe.at; c.probe = null;
925
+ if(ms>=0 && ms<=10000){ c.measuredRtt=Math.min(c.measuredRtt??Infinity,ms); if(c.seat!==null)this.toServer({t:'latency',seat:c.seat,ms:c.measuredRtt}); }
926
+ }
927
+ return;
928
+ }
921
929
  case 'ping': {
922
930
  if (this.rules && c.rules && Number.isFinite(m.readySpeed)) { c.readySpeed = Math.max(0, Math.min(1, m.readySpeed)); c.readyAt = now; }
923
931
  const wasHidden = c.hidden;
924
932
  c.hidden = m.hid === true;
925
933
  this.send(c, { t: 'pong', c: m.c, st: now });
934
+ if(this.server && (!c.probe || now-c.probe.at>10000)){c.probe={n:crypto.randomUUID(),at:now};this.send(c,{t:'probe',n:c.probe.n});}
926
935
  // A host whose tab went to the background will stop ticking: hand the round on now.
927
936
  if (c.id === this.hostId && c.hidden && !wasHidden) this.yieldHost(c, 'host-hidden');
928
937
  return;
@@ -98,11 +98,15 @@ export const SAVES_SHELL_JS = `${PASSKEY_JS}
98
98
  if (late) loaded.then(function () { tell('player', { player: me(), cache: snapshot(), same: true }); });
99
99
  });
100
100
  }
101
- if (op === 'get') return loaded.then(function () { reply({ ok: true, value: valueOf(String(m.key)), version: versionOf(String(m.key)) }); });
101
+ if (op === 'get') return loaded.then(function () {
102
+ var key=String(m.key);if(key.indexOf('server:')===0&&KEY.test(key))return api('GET','/api/player/saves/'+GAME+'/'+encodeURIComponent(key)).then(function(r){if(r.ok){S.cache[key]={v:r.value,ver:r.version,at:r.updatedAt};persist();}reply(r);},function(){reply({ok:false,error:'offline',value:valueOf(key)});});
103
+ reply({ ok: true, value: valueOf(key), version: versionOf(key) });
104
+ });
102
105
  if (op === 'list') return loaded.then(function () { var keys = {}; Object.keys(S.cache).forEach(function (k) { keys[k] = 1; }); Object.keys(S.pending).forEach(function (k) { keys[k] = 1; });
103
106
  reply({ ok: true, keys: Object.keys(keys).filter(function (k) { return valueOf(k) !== null; }).sort().map(function (k) { var c = S.cache[k]; return { key: k, version: c ? c.ver : 0, updatedAt: c ? c.at : null, pending: !!S.pending[k] }; }) }); });
104
107
  if (op === 'set' || op === 'del') {
105
108
  var key = String(m.key || '');
109
+ if(key.indexOf('server:')===0)return reply({ok:false,error:'authority',message:'This record is written by the game server'});
106
110
  if (!KEY.test(key)) return reply({ ok: false, error: 'key', message: 'a save key is 1 to 64 letters, digits, _ . : or -' });
107
111
  if (op === 'set') {
108
112
  var text; try { text = JSON.stringify(m.value); } catch (e) { return reply({ ok: false, error: 'value', message: 'a save is JSON' }); }
package/worker/saves.mjs CHANGED
@@ -85,6 +85,7 @@ export async function writeSaves(env, player, game, { set = [], del = [] } = {})
85
85
  const at = Date.now();
86
86
  for (const item of items) {
87
87
  const key = String(item?.key ?? '');
88
+ if(key.startsWith('server:')){results.push({key,ok:false,error:'authority',message:'This record is written by the game server'});continue;}
88
89
  if (!SAVE_KEY.test(key)) { results.push({ key, ok: false, error: 'key', message: 'a save key is 1 to 64 letters, digits, _ . : or -' }); continue; }
89
90
  const base = item.base === undefined || item.base === null ? null : Math.floor(Number(item.base));
90
91
  if (base !== null && !(base >= 0)) { results.push({ key, ok: false, error: 'base', message: 'base is the version the change was made from (0 when the key is new)' }); continue; }
@@ -148,7 +149,7 @@ export async function writeSaves(env, player, game, { set = [], del = [] } = {})
148
149
  export function wipeStatements(env, player, game, keep = []) {
149
150
  const kept = (Array.isArray(keep) ? keep : []).filter((k) => SAVE_KEY.test(String(k))).slice(0, SAVE_LIMITS.keys);
150
151
  const marks = kept.map((_, i) => `?${i + 3}`).join(', ');
151
- return env.DB.prepare(`DELETE FROM saves WHERE player = ?1 AND game = ?2${kept.length ? ` AND key NOT IN (${marks})` : ''} RETURNING key, value, blob`).bind(player, game, ...kept);
152
+ return env.DB.prepare(`DELETE FROM saves WHERE player = ?1 AND game = ?2 AND key NOT LIKE 'server:%'${kept.length ? ` AND key NOT IN (${marks})` : ''} RETURNING key, value, blob`).bind(player, game, ...kept);
152
153
  }
153
154
 
154
155
  export async function wipeSaves(env, player, game, { keep = [] } = {}) {
@@ -289,3 +290,20 @@ export async function adoptStatements(env, from, to) {
289
290
  after: () => dropBlobs(env, clash.map(blobKey)),
290
291
  };
291
292
  }
293
+
294
+ /** Server-only, idempotent bounded round receipts. Optimistic versions also
295
+ * serialize simultaneous rooms without dropping either room's result. */
296
+ export async function appendServerRecord(env,player,game,key,record){
297
+ if(!key.startsWith('server:')||!SAVE_KEY.test(key)||typeof record.id!=='string')throw new Error('invalid server record');
298
+ for(let attempt=0;attempt<16;attempt++){
299
+ const old=await getSave(env,player,game,key),matches=Array.isArray(old.value?.matches)?old.value.matches:[];
300
+ if(matches.some(r=>r.id===record.id))return;
301
+ const kept=[record,...matches].sort((a,b)=>b.endedAt-a.endedAt||a.id.localeCompare(b.id)).slice(0,100);
302
+ let value=JSON.stringify({v:1,matches:kept}),bytes=bytesOf(value);
303
+ while(bytes>SAVE_LIMITS.valueBytes&&kept.length>1){kept.pop();value=JSON.stringify({v:1,matches:kept});bytes=bytesOf(value);}
304
+ if(bytes>SAVE_LIMITS.valueBytes)throw new Error('server record size');
305
+ const result=old.version?await env.DB.prepare('UPDATE saves SET value=?4,bytes=?5,version=version+1,updated_at=?6 WHERE player=?1 AND game=?2 AND key=?3 AND version=?7 RETURNING version').bind(player,game,key,value,bytes,Date.now(),old.version).first():await env.DB.prepare('INSERT INTO saves(player,game,key,value,bytes,version,updated_at,blob) VALUES(?1,?2,?3,?4,?5,1,?6,0) ON CONFLICT(player,game,key) DO NOTHING RETURNING version').bind(player,game,key,value,bytes,Date.now()).first();
306
+ if(result)return;
307
+ }
308
+ throw new Error('server record conflict; retry the durable outbox');
309
+ }
@@ -1,2 +1,2 @@
1
1
  /** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
2
- export const STUDIO_VERSION_TAG = '0.45.2';
2
+ export const STUDIO_VERSION_TAG = '0.45.3';