// world.js export class Block { constructor(id, pos, state) { this.id = id; this.pos = pos; this.state = state; } } export class World { constructor(script = '') { // Pure Data State this.events = []; this.labels = new Map(); this.initialState = new Map(); this.initialIndex = 0; if (script) { this.#compile(script); } } #compile(script) { let currentTime = -1; let capturedInitialState = false; const shadowWorld = new Map(); const captureInitial = () => { if (capturedInitialState) return; for (const [k, v] of shadowWorld.entries()) { this.initialState.set(k, {id: v.id, pos: [...v.pos], state: {...v.state}}); } this.initialIndex = this.events.length; capturedInitialState = true; }; const lines = script.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); let currentBatch = []; for (let line of lines) { let mergeNext = false; if (line.endsWith('&')) { mergeNext = true; line = line.slice(0, -1).trim(); } const tokens = line.split(/\s+/); const cmd = tokens[0]; if (cmd === 't') { const newTime = parseFloat(tokens[1]); if (newTime >= 0 && !capturedInitialState) captureInitial(); if (currentBatch.length > 0) { this.events.push({time: currentTime, actions: currentBatch}); currentBatch = []; } currentTime = newTime; continue; } if (cmd === 'l') { this.labels.set(tokens[1], this.events.length); continue; } if (cmd === 'p') { const x = parseFloat(tokens[1]); const y = parseFloat(tokens[2]); const z = parseFloat(tokens[3]); const key = `${x},${y},${z}`; let id = null; const newState = {}; for (let i = 4; i < tokens.length; i++) { const token = tokens[i]; if (token.includes('=')) { const [k, v] = token.split('='); newState[k] = v; } else { id = token; } } const prevBlock = shadowWorld.get(key); const prev = prevBlock ? {id: prevBlock.id, state: {...prevBlock.state}} : null; const isAir = id === 'air' || id === 'minecraft:air'; if (isAir) { shadowWorld.delete(key); } else if (id) { shadowWorld.set(key, {id, pos: [x, y, z], state: newState}); } else if (prevBlock) { Object.assign(prevBlock.state, newState); } else { console.warn(`Timeline warning: Attempted to update state of non-existent block at ${key}`); continue; } const nextBlock = shadowWorld.get(key); const next = nextBlock ? {id: nextBlock.id, state: {...nextBlock.state}} : null; currentBatch.push({x, y, z, prev, next}); if (!mergeNext) { this.events.push({time: currentTime, actions: currentBatch}); currentBatch = []; } } } if (currentBatch.length > 0) { this.events.push({time: currentTime, actions: currentBatch}); } if (!capturedInitialState) captureInitial(); } getUniqueBlockStates() { const unique = new Map(); const add = (block) => { if (!block || !block.id || block.id === 'air' || block.id === 'minecraft:air') return; // Create a stable hash for the state object so we don't duplicate requests const stateHash = Object.entries(block.state) .sort((a, b) => a[0].localeCompare(b[0])) .map(e => `${e[0]}=${e[1]}`) .join(','); const hash = `${block.id}[${stateHash}]`; if (!unique.has(hash)) { unique.set(hash, {id: block.id, state: {...block.state}}); } }; for (const block of this.initialState.values()) add(block); for (const ev of this.events) { for (const action of ev.actions) { add(action.prev); add(action.next); } } return Array.from(unique.values()); } } export class WorldFrame { constructor(world) { this.world = world; // Playback State this.blocks = new Map(); this.listeners = new Set(); this.currentIndex = 0; this.currentTime = -Infinity; // Immediately sync frame to the bedrock state this.reset(); } subscribe(callback) { this.listeners.add(callback); return () => this.listeners.delete(callback); } notify() { for (const listener of this.listeners) listener(); } get(x, y, z) { return this.blocks.get(`${x},${y},${z}`); } #apply(data, x, y, z) { const key = `${x},${y},${z}`; if (data) { this.blocks.set(key, new Block(data.id, [x, y, z], {...data.state})); } else { this.blocks.delete(key); } } seek(target) { let targetTime = target; if (typeof target === 'string') { const absoluteIdx = this.world.labels.get(target); if (absoluteIdx === undefined) throw new Error(`Label not found: ${target}`); this.seekIndex(absoluteIdx); return; } let changed = false; while (this.currentIndex < this.world.events.length && this.world.events[this.currentIndex].time < targetTime) { const batch = this.world.events[this.currentIndex]; for (const action of batch.actions) { this.#apply(action.next, action.x, action.y, action.z); } this.currentIndex++; changed = true; } while (this.currentIndex > this.world.initialIndex && this.world.events[this.currentIndex - 1].time >= targetTime) { this.currentIndex--; const batch = this.world.events[this.currentIndex]; for (let i = batch.actions.length - 1; i >= 0; i--) { const action = batch.actions[i]; this.#apply(action.prev, action.x, action.y, action.z); } changed = true; } this.currentTime = targetTime; if (changed) this.notify(); } seekIndex(absoluteIndex) { let targetIndex = Math.max(this.world.initialIndex, Math.min(this.world.events.length, absoluteIndex)); let changed = false; while (this.currentIndex < this.world.events.length && this.currentIndex < targetIndex) { const batch = this.world.events[this.currentIndex]; for (const action of batch.actions) { this.#apply(action.next, action.x, action.y, action.z); } this.currentIndex++; changed = true; } while (this.currentIndex > 0 && this.currentIndex > targetIndex) { this.currentIndex--; const batch = this.world.events[this.currentIndex]; for (let i = batch.actions.length - 1; i >= 0; i--) { const action = batch.actions[i]; this.#apply(action.prev, action.x, action.y, action.z); } changed = true; } if (this.currentIndex < this.world.events.length) { this.currentTime = this.world.events[this.currentIndex].time; } else if (this.world.events.length > 0) { this.currentTime = this.world.events[this.world.events.length - 1].time; } else { this.currentTime = -Infinity; } if (changed) this.notify(); } reset() { this.blocks.clear(); for (const [key, block] of this.world.initialState.entries()) { this.blocks.set(key, new Block(block.id, [...block.pos], {...block.state})); } this.currentIndex = this.world.initialIndex; if (this.world.initialIndex > 0 && this.world.events.length > 0) { this.currentTime = this.world.events[this.world.initialIndex - 1].time; } else { this.currentTime = -Infinity; } this.notify(); } }