tarinai/js/item_update_scheduler.js

214 lines
7.5 KiB
JavaScript
Raw Normal View History

2026-06-25 14:51:58 +09:00
"use strict";
(function (global) {
2026-06-29 16:21:58 +09:00
const policy = global.TarinaiItemUpdatePolicy;
2026-06-25 14:51:58 +09:00
function itemUpdateInterval(item) {
2026-06-29 16:21:58 +09:00
const interval = policy.itemUpdateInterval(item);
2026-06-26 12:46:32 +09:00
return Number.isFinite(interval) ? interval : Infinity;
}
function candidateItems(worldRef) {
2026-06-29 16:21:58 +09:00
return policy.candidateItems(worldRef);
2026-06-26 12:46:32 +09:00
}
function ensureBuckets(worldRef) {
2026-06-29 16:21:58 +09:00
return policy.ensureBuckets(worldRef);
2026-06-25 14:51:58 +09:00
}
function heapPush(heap, entry) {
heap.push(entry);
let i = heap.length - 1;
while (i > 0) {
const p = (i - 1) >> 1;
if (heap[p].time <= entry.time) break;
heap[i] = heap[p];
i = p;
}
heap[i] = entry;
}
function heapPop(heap) {
if (!heap.length) return null;
const root = heap[0];
const last = heap.pop();
if (heap.length && last) {
let i = 0;
while (true) {
let c = i * 2 + 1;
if (c >= heap.length) break;
if (c + 1 < heap.length && heap[c + 1].time < heap[c].time) c++;
if (heap[c].time >= last.time) break;
heap[i] = heap[c];
i = c;
}
heap[i] = last;
}
return root;
}
function schedulerSignature(worldRef) {
return [
(worldRef.items || []).length,
worldRef.itemBucketRebuildsTotal || 0,
worldRef.itemBucketsDirty ? 1 : 0,
].join(":");
}
2026-07-01 22:59:49 +09:00
function scheduleDelay(item, interval, initial = false) {
const key = item?.id || item?._schedulerToken || item?.type || "item";
const salt = initial ? "item-scheduler-initial" : `item-scheduler:${Math.floor((item?._schedulerLastAt || 0) * 3)}`;
const unit = typeof stableUnit === "function" ? stableUnit(key, salt) : Math.random();
const spread = initial ? (0.42 + unit * 1.18) : (0.88 + unit * 0.24);
return interval * spread;
}
2026-06-25 14:51:58 +09:00
function scheduleItem(state, item, now, interval = itemUpdateInterval(item)) {
if (!item || item.dead || !Number.isFinite(interval)) return false;
if (interval <= 0) {
state.realtime.push(item);
return true;
}
const token = (item._schedulerToken || 0) + 1;
item._schedulerToken = token;
2026-07-01 22:59:49 +09:00
const initial = !Number.isFinite(item._schedulerLastAt);
if (initial) item._schedulerLastAt = now;
heapPush(state.heap, { time: now + scheduleDelay(item, interval, initial), item, token });
2026-06-25 14:51:58 +09:00
return true;
}
function rebuild(worldRef, reason = "manual") {
ensureBuckets(worldRef);
const now = worldRef.time || 0;
const state = {
heap: [],
realtime: [],
signature: schedulerSignature(worldRef),
rebuilds: ((worldRef._itemUpdateScheduler?.rebuilds || 0) + 1),
reason,
};
for (const item of candidateItems(worldRef)) {
item._schedulerLastAt = Number.isFinite(item._schedulerLastAt) ? item._schedulerLastAt : now;
scheduleItem(state, item, now, itemUpdateInterval(item));
}
worldRef._itemUpdateScheduler = state;
worldRef._itemUpdateRealtime = state.realtime;
return state;
}
function ensure(worldRef) {
const sig = schedulerSignature(worldRef);
if (!worldRef._itemUpdateScheduler || worldRef._itemUpdateScheduler.signature !== sig) return rebuild(worldRef, "signature");
return worldRef._itemUpdateScheduler;
}
function runRealtime(worldRef, state, dt) {
let ran = 0;
const now = worldRef.time || 0;
const nextRealtime = [];
for (const item of state.realtime || []) {
if (!item || item.dead) continue;
const interval = itemUpdateInterval(item);
if (!Number.isFinite(interval)) {
2026-07-01 14:41:05 +09:00
continue;
2026-06-25 14:51:58 +09:00
}
if (interval <= 0) {
2026-06-29 16:21:58 +09:00
global.TarinaiItemRuntime.updateOne(item, dt, worldRef);
2026-06-25 14:51:58 +09:00
item._schedulerLastAt = now;
ran += 1;
2026-07-05 18:01:36 +09:00
const nextInterval = itemUpdateInterval(item);
if (Number.isFinite(nextInterval) && nextInterval <= 0) nextRealtime.push(item);
else scheduleItem(state, item, now, nextInterval);
2026-06-25 14:51:58 +09:00
} else {
scheduleItem(state, item, now, interval);
}
}
state.realtime = nextRealtime;
worldRef._itemUpdateRealtime = state.realtime;
return ran;
}
function runDue(worldRef, state) {
let ran = 0;
const now = worldRef.time || 0;
const maxDue = Math.max(24, Math.ceil((state.heap.length || 0) * 0.08));
let dueRuns = 0;
while (state.heap.length && state.heap[0].time <= now + 0.0001 && dueRuns < maxDue) {
const entry = heapPop(state.heap);
const item = entry?.item;
if (!item || item.dead || item._schedulerToken !== entry.token) continue;
const interval = itemUpdateInterval(item);
if (!Number.isFinite(interval)) continue;
if (interval <= 0) {
state.realtime.push(item);
2026-07-01 14:41:05 +09:00
continue;
2026-06-25 14:51:58 +09:00
}
const elapsed = Math.min(10, Math.max(0.001, now - (item._schedulerLastAt ?? entry.time - interval)));
2026-06-29 16:21:58 +09:00
global.TarinaiItemRuntime.updateOne(item, elapsed, worldRef);
2026-06-25 14:51:58 +09:00
item._schedulerLastAt = now;
ran += 1;
dueRuns += 1;
scheduleItem(state, item, now, itemUpdateInterval(item));
}
return ran;
}
2026-06-27 19:22:38 +09:00
function physicsBudgetFor(worldRef) {
const itemCount = (worldRef?.items || []).length || 0;
2026-06-29 16:21:58 +09:00
const mobile = Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
2026-06-28 13:40:41 +09:00
if (mobile) return { maxPairs: 150, maxLinks: 96, postMaxPairs: 40, maxSubsteps: 2 };
2026-06-27 19:22:38 +09:00
return itemCount > 80
? { maxPairs: 240, maxLinks: 170, postMaxPairs: 78, maxSubsteps: 4 }
: { maxPairs: 300, maxLinks: 210, postMaxPairs: 100, maxSubsteps: 5 };
}
2026-06-25 14:51:58 +09:00
function run(worldRef, dt) {
2026-06-29 16:21:58 +09:00
const end = global.TarinaiPerf.begin("update.items");
2026-06-25 14:51:58 +09:00
try {
const state = ensure(worldRef);
2026-06-29 16:21:58 +09:00
const realtimeEnd = global.TarinaiPerf.begin("update.items.realtime");
2026-06-27 19:22:38 +09:00
let realtimeRan = runRealtime(worldRef, state, dt);
let ran = realtimeRan;
if (realtimeEnd) realtimeEnd();
2026-06-29 16:21:58 +09:00
const dueEnd = global.TarinaiPerf.begin("update.items.scheduled");
2026-06-27 19:22:38 +09:00
const dueRan = runDue(worldRef, state);
ran += dueRan;
if (dueEnd) dueEnd();
2026-06-29 16:21:58 +09:00
const physicsEnd = global.TarinaiPerf.begin("update.physicsWorld");
2026-06-27 19:22:38 +09:00
const physicsBudget = physicsBudgetFor(worldRef);
2026-06-29 16:21:58 +09:00
const physicsWorld = global.TarinaiPhysicsWorldSystem.updateWorld(worldRef, dt, physicsBudget);
const mechanicalWorld = physicsWorld.mechanical || null;
const constraintWorld = physicsWorld.constraintRan || 0;
const postConstraintPairs = physicsWorld.postConstraintPairs || 0;
2026-06-28 23:07:40 +09:00
const poisonContactRemoved = worldRef.resolvePoisonBlockItemContacts?.({ maxPoison: 24, maxTargets: 80 }) || 0;
2026-06-27 19:22:38 +09:00
if (physicsEnd) physicsEnd();
2026-06-25 14:51:58 +09:00
state.signature = schedulerSignature(worldRef);
worldRef._itemUpdateSchedulerStats = {
heap: state.heap.length,
realtime: state.realtime.length,
rebuilds: state.rebuilds,
lastReason: state.reason,
lastRan: ran,
2026-06-27 19:22:38 +09:00
realtimeRan,
dueRan,
sleeping: state.heap.length,
activeRealtime: state.realtime.length,
physicsBudget,
physics: physicsWorld || mechanicalWorld,
constraints: constraintWorld,
postConstraintPairs,
mechanicalStats: worldRef._mechanicalWorldStats || null,
constraintStats: worldRef._constraintWorldStats || null,
2026-06-25 14:51:58 +09:00
};
2026-06-29 16:21:58 +09:00
return ran + (physicsWorld.ran || mechanicalWorld?.ran || 0) + constraintWorld + poisonContactRemoved;
2026-06-25 14:51:58 +09:00
} finally {
if (end) end();
}
}
function trackedDynamicItems(worldRef) {
return worldRef?._itemUpdateRealtime || [];
}
2026-06-29 16:21:58 +09:00
global.TarinaiItemUpdateScheduler = Object.freeze({ run, trackedDynamicItems });
2026-06-25 14:51:58 +09:00
})(typeof window !== "undefined" ? window : globalThis);