272 lines
10 KiB
JavaScript
272 lines
10 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const policy = global.TarinaiItemUpdatePolicy;
|
|
|
|
function itemUpdateInterval(item) {
|
|
const interval = policy.itemUpdateInterval(item);
|
|
return Number.isFinite(interval) ? interval : Infinity;
|
|
}
|
|
|
|
function candidateItems(worldRef) {
|
|
return policy.candidateItems(worldRef);
|
|
}
|
|
|
|
function ensureBuckets(worldRef) {
|
|
return policy.ensureBuckets(worldRef);
|
|
}
|
|
|
|
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,
|
|
global.TarinaiPerf?.simulationOptimizationTier?.() || "balanced",
|
|
].join(":");
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
const initial = !Number.isFinite(item._schedulerLastAt);
|
|
if (initial) item._schedulerLastAt = now;
|
|
heapPush(state.heap, { time: now + scheduleDelay(item, interval, initial), item, token });
|
|
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)) {
|
|
const interval = itemUpdateInterval(item);
|
|
if (interval > 0 && !Number.isFinite(item._schedulerLastAt)) item._schedulerLastAt = now;
|
|
scheduleItem(state, item, now, interval);
|
|
}
|
|
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 scheduledBudgetFor() {
|
|
return Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().scheduledItemBudget || 30));
|
|
}
|
|
|
|
function beginTouchedFrame(worldRef, state) {
|
|
if (!state.touchedFrame) state.touchedFrame = [];
|
|
else state.touchedFrame.length = 0;
|
|
if (!state.touchedSet) state.touchedSet = new Set();
|
|
else state.touchedSet.clear();
|
|
worldRef._itemUpdateTouched = state.touchedFrame;
|
|
}
|
|
|
|
function recordTouched(state, item) {
|
|
if (!item || item.dead || state.touchedSet.has(item)) return;
|
|
state.touchedSet.add(item);
|
|
state.touchedFrame.push(item);
|
|
}
|
|
|
|
function runRealtime(worldRef, state, dt) {
|
|
let ran = 0;
|
|
const now = worldRef.time || 0;
|
|
const realtime = state.realtime || [];
|
|
let write = 0;
|
|
const len = realtime.length;
|
|
for (let i = 0; i < len; i += 1) {
|
|
const item = realtime[i];
|
|
if (!item || item.dead) continue;
|
|
const interval = itemUpdateInterval(item);
|
|
if (!Number.isFinite(interval)) {
|
|
continue;
|
|
}
|
|
if (interval <= 0) {
|
|
global.TarinaiItemRuntime.updateOne(item, dt, worldRef);
|
|
recordTouched(state, item);
|
|
item._schedulerLastAt = now;
|
|
ran += 1;
|
|
const nextInterval = itemUpdateInterval(item);
|
|
if (Number.isFinite(nextInterval) && nextInterval <= 0) realtime[write++] = item;
|
|
else scheduleItem(state, item, now, nextInterval);
|
|
} else {
|
|
scheduleItem(state, item, now, interval);
|
|
}
|
|
}
|
|
realtime.length = write;
|
|
state.realtime = realtime;
|
|
worldRef._itemUpdateRealtime = state.realtime;
|
|
return ran;
|
|
}
|
|
|
|
function runDue(worldRef, state, budget = scheduledBudgetFor(worldRef, state)) {
|
|
let ran = 0;
|
|
const now = worldRef.time || 0;
|
|
const maxDue = Math.max(1, Number(budget || 0) || 1);
|
|
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);
|
|
continue;
|
|
}
|
|
const elapsed = Math.min(10, Math.max(0.001, now - (item._schedulerLastAt ?? entry.time - interval)));
|
|
global.TarinaiItemRuntime.updateOne(item, elapsed, worldRef);
|
|
recordTouched(state, item);
|
|
item._schedulerLastAt = now;
|
|
ran += 1;
|
|
dueRuns += 1;
|
|
scheduleItem(state, item, now, itemUpdateInterval(item));
|
|
}
|
|
state.lastBudgetDue = maxDue;
|
|
state.lastDueSkipped = (state.heap.length && state.heap[0].time <= now + 0.0001) ? 1 : 0;
|
|
return ran;
|
|
}
|
|
|
|
function physicsBudgetFor() {
|
|
return { ...(global.TarinaiPerf?.performanceProfile?.().physicsBudget || {}) };
|
|
}
|
|
|
|
function poisonContactBudgetFor(worldRef) {
|
|
const possibleTargets =
|
|
(worldRef?.itemCounts?.grass_bed || 0) +
|
|
(worldRef?.itemCounts?.zunchi || 0) +
|
|
(worldRef?.itemCounts?.water || 0) +
|
|
(worldRef?.itemCounts?.grass || 0);
|
|
if ((worldRef?.itemCounts?.poison_block || 0) <= 0 || possibleTargets <= 0) return { maxPoison: 0, maxTargets: 0, skip: true };
|
|
return { ...(global.TarinaiPerf?.performanceProfile?.().poisonContactBudget || { maxPoison: 12, maxTargets: 42, skip: false }) };
|
|
}
|
|
|
|
function wakeItem(worldRef, item, holdSeconds = 0.22) {
|
|
if (!worldRef || !item || item.dead) return false;
|
|
const state = ensure(worldRef);
|
|
const now = worldRef.time || 0;
|
|
item._activeUntil = Math.max(Number(item._activeUntil || -Infinity) || -Infinity, now + Math.max(0.05, Number(holdSeconds || 0.22) || 0.22));
|
|
// Invalidate a pending heap entry and promote the item immediately. This is
|
|
// used by continuous external forces such as fan wind.
|
|
item._schedulerToken = (item._schedulerToken || 0) + 1;
|
|
item._schedulerLastAt = now;
|
|
if (!state.realtime.includes(item)) state.realtime.push(item);
|
|
worldRef._itemUpdateRealtime = state.realtime;
|
|
return true;
|
|
}
|
|
|
|
function run(worldRef, dt) {
|
|
const end = global.TarinaiPerf.begin("update.items");
|
|
try {
|
|
const state = ensure(worldRef);
|
|
beginTouchedFrame(worldRef, state);
|
|
const realtimeEnd = global.TarinaiPerf.begin("update.items.realtime");
|
|
let realtimeRan = runRealtime(worldRef, state, dt);
|
|
let ran = realtimeRan;
|
|
if (realtimeEnd) realtimeEnd();
|
|
const dueEnd = global.TarinaiPerf.begin("update.items.scheduled");
|
|
const budgetDue = scheduledBudgetFor(worldRef, state);
|
|
const dueRan = runDue(worldRef, state, budgetDue);
|
|
ran += dueRan;
|
|
if (dueEnd) dueEnd();
|
|
global.TarinaiSignalSystem?.updateWorld?.(worldRef, dt);
|
|
global.TarinaiItemEnvironmentHazardSystem?.updatePoweredHazards?.(worldRef, dt);
|
|
const physicsEnd = global.TarinaiPerf.begin("update.physicsWorld");
|
|
const physicsBudget = physicsBudgetFor(worldRef);
|
|
const physicsWorld = global.TarinaiPhysicsWorldSystem.updateWorld(worldRef, dt, physicsBudget);
|
|
const trapHeld = global.TarinaiItemEnvironmentHazardSystem?.enforceTrapHolds?.(worldRef) || 0;
|
|
const mechanicalWorld = physicsWorld.mechanical || null;
|
|
const constraintWorld = physicsWorld.constraintRan || 0;
|
|
const postConstraintPairs = physicsWorld.postConstraintPairs || 0;
|
|
const poisonContactBudget = poisonContactBudgetFor(worldRef);
|
|
const poisonContactRemoved = poisonContactBudget.skip ? 0 : (worldRef.resolvePoisonBlockItemContacts?.(poisonContactBudget) || 0);
|
|
if (physicsEnd) physicsEnd();
|
|
state.signature = schedulerSignature(worldRef);
|
|
worldRef._itemUpdateSchedulerStats = {
|
|
heap: state.heap.length,
|
|
realtime: state.realtime.length,
|
|
rebuilds: state.rebuilds,
|
|
lastReason: state.reason,
|
|
lastRan: ran,
|
|
realtimeRan,
|
|
dueRan,
|
|
budgetDue,
|
|
dueSkipped: state.lastDueSkipped || 0,
|
|
sleeping: state.heap.length,
|
|
activeRealtime: state.realtime.length,
|
|
physicsBudget,
|
|
poisonContactBudget,
|
|
poisonContactRemoved,
|
|
trapHeld,
|
|
physics: physicsWorld || mechanicalWorld,
|
|
constraints: constraintWorld,
|
|
postConstraintPairs,
|
|
mechanicalStats: worldRef._mechanicalWorldStats || null,
|
|
constraintStats: worldRef._constraintWorldStats || null,
|
|
};
|
|
return ran + (physicsWorld.ran || mechanicalWorld?.ran || 0) + poisonContactRemoved;
|
|
} finally {
|
|
if (end) end();
|
|
}
|
|
}
|
|
|
|
function trackedDynamicItems(worldRef) {
|
|
return worldRef?._itemUpdateTouched || [];
|
|
}
|
|
|
|
global.TarinaiItemUpdateScheduler = Object.freeze({ run, trackedDynamicItems, wakeItem });
|
|
})(typeof window !== "undefined" ? window : globalThis);
|