@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/CHANGELOG.md +29 -0
- package/guides/game/RULES.md +222 -2
- package/lib/build.mjs +1 -1
- package/lib/rules-check-worker.mjs +6 -5
- package/lib/rules-check.mjs +2 -2
- package/lib/rules-guard.mjs +4 -3
- package/lib/rules-types.mjs +1 -1
- package/lib/template-history.json +42 -0
- package/netplay/netplay.ts +1 -0
- package/package.json +1 -1
- package/rules/collision.ts +51 -11
- package/rules/core.ts +105 -21
- package/rules/guard.ts +1 -1
- package/rules/host.ts +40 -9
- package/rules/interest.mjs +3 -3
- package/rules/live.ts +19 -7
- package/rules/map-index.ts +7 -1
- package/rules/math.ts +20 -8
- package/rules/navigation.ts +69 -0
- package/rules/pack.ts +2 -2
- package/rules/query.ts +103 -0
- package/rules/rules.ts +89 -23
- package/rules/terrain-legacy.ts +93 -0
- package/rules/terrain.ts +96 -11
- package/rules/types.ts +17 -7
- package/rules/view.ts +38 -19
- package/worker/index.mjs +29 -1
- package/worker/players.mjs +3 -1
- package/worker/room.mjs +12 -3
- package/worker/saves-shell.mjs +5 -1
- package/worker/saves.mjs +19 -1
- package/worker/version.mjs +1 -1
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
|
-
|
|
495
|
-
|
|
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;
|
package/worker/saves-shell.mjs
CHANGED
|
@@ -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 () {
|
|
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
|
+
}
|
package/worker/version.mjs
CHANGED
|
@@ -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
|
+
export const STUDIO_VERSION_TAG = '0.45.3';
|