"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 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); 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); 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); 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); const physicsEnd = global.TarinaiPerf.begin("update.physicsWorld"); const physicsBudget = physicsBudgetFor(worldRef); const physicsWorld = global.TarinaiPhysicsWorldSystem.updateWorld(worldRef, dt, physicsBudget); 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, 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?._itemUpdateRealtime || []; } global.TarinaiItemUpdateScheduler = Object.freeze({ run, trackedDynamicItems, wakeItem }); })(typeof window !== "undefined" ? window : globalThis);