"use strict"; (function (global) { const Snapshot = global.TarinaiSnapshot; const SaveSchema = global.TarinaiSaveSchema; if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_codec.js"); if (!SaveSchema) throw new Error("TarinaiSaveSchema is not available for save_codec.js"); const EXPORT_PREFIX = "\u305f"; const { BINARY_SCHEMA_VERSION, FIELD_IDS, } = SaveSchema; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); const SAVE_STATS_SECTION_ORDER = Object.freeze([ "format", "stringTable", "world", "tarinaiBasic", "tarinaiSeed", "tarinaiRelationships", "itemIndex", "normalItems", "physicsItems", "achievementMetadata", ]); let lastEncodeStats = null; let lastDecodeStats = null; function nowMs() { return Number(global.performance?.now?.() ?? Date.now()); } function debugSaveStatsEnabled() { if (global.__tarinaiDebugOverlayEnabled === true) return true; try { const params = new URLSearchParams(global.location?.search || ""); return params.get("debug") === "1" || params.get("debug") === "true"; } catch (_) { return false; } } function createSaveStats(snapshot) { return { createdAt: Date.now(), sections: Object.fromEntries(SAVE_STATS_SECTION_ORDER.map(key => [key, 0])), details: { tarinaiCount: Array.isArray(snapshot?.t) ? snapshot.t.length : 0, itemCount: Array.isArray(snapshot?.i) ? snapshot.i.length : 0, normalItemCount: 0, physicsItemCount: 0, stringTableEntries: 0, relationshipRows: 0, parentLinks: 0, birthSeedRawUtf8Bytes: 0, normalItemExtraEncodedBytes: 0, physicsItemExtraEncodedBytes: 0, normalItemExtraRawUtf8Bytes: 0, physicsItemExtraRawUtf8Bytes: 0, }, binaryBytes: 0, rawBinaryBytes: 0, compressedBytes: 0, compressionSavedBytes: 0, compressed: false, textBits: 15, hashChars: 0, payloadChars: 0, encodeMs: 0, accountedBytes: 0, unaccountedBytes: 0, }; } const SAVE_TEXT_BITS = 15; const SAVE_TEXT_BASE = 1 << SAVE_TEXT_BITS; const SAVE_TEXT_RANGE_A_START = 0x3400; const SAVE_TEXT_RANGE_A_COUNT = 0x4DC0 - 0x3400; // 6592 const SAVE_TEXT_RANGE_B_START = 0x4E00; const SAVE_TEXT_RANGE_B_COUNT = 0xA000 - 0x4E00; // 20992 const SAVE_TEXT_RANGE_C_START = 0xAC00; const SAVE_TEXT_RANGE_C_COUNT = SAVE_TEXT_BASE - SAVE_TEXT_RANGE_A_COUNT - SAVE_TEXT_RANGE_B_COUNT; // 5184 function saveTextCharFromValue(value = 0) { const n = Number(value) & (SAVE_TEXT_BASE - 1); if (n < SAVE_TEXT_RANGE_A_COUNT) return String.fromCharCode(SAVE_TEXT_RANGE_A_START + n); const afterA = n - SAVE_TEXT_RANGE_A_COUNT; if (afterA < SAVE_TEXT_RANGE_B_COUNT) return String.fromCharCode(SAVE_TEXT_RANGE_B_START + afterA); return String.fromCharCode(SAVE_TEXT_RANGE_C_START + afterA - SAVE_TEXT_RANGE_B_COUNT); } function saveTextValueFromCode(code = 0) { const n = Number(code) || 0; if (n >= SAVE_TEXT_RANGE_A_START && n < SAVE_TEXT_RANGE_A_START + SAVE_TEXT_RANGE_A_COUNT) return n - SAVE_TEXT_RANGE_A_START; if (n >= SAVE_TEXT_RANGE_B_START && n < SAVE_TEXT_RANGE_B_START + SAVE_TEXT_RANGE_B_COUNT) return SAVE_TEXT_RANGE_A_COUNT + n - SAVE_TEXT_RANGE_B_START; if (n >= SAVE_TEXT_RANGE_C_START && n < SAVE_TEXT_RANGE_C_START + SAVE_TEXT_RANGE_C_COUNT) return SAVE_TEXT_RANGE_A_COUNT + SAVE_TEXT_RANGE_B_COUNT + n - SAVE_TEXT_RANGE_C_START; return -1; } function base32768Encode(bytes) { const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []); let buffer = 0; let bits = 0; let out = ""; for (const byte of source) { buffer |= (byte & 255) << bits; bits += 8; while (bits >= SAVE_TEXT_BITS) { out += saveTextCharFromValue(buffer); buffer = Math.floor(buffer / SAVE_TEXT_BASE); bits -= SAVE_TEXT_BITS; } } if (bits > 0) out += saveTextCharFromValue(buffer); return out; } function base32768Decode(text) { let buffer = 0; let bits = 0; const out = []; const source = String(text || "").trim(); for (let i = 0; i < source.length; i++) { const value = saveTextValueFromCode(source.charCodeAt(i)); if (value < 0) continue; buffer += value * (2 ** bits); bits += SAVE_TEXT_BITS; while (bits >= 8) { out.push(buffer & 255); buffer = Math.floor(buffer / 256); bits -= 8; } } return new Uint8Array(out); } function encodeVarUint(value = 0) { let n = toSafeUInt(value); const out = []; while (n >= 128) { out.push((n % 128) | 128); n = Math.floor(n / 128); } out.push(n & 127); return new Uint8Array(out); } function decodeVarUintPrefix(bytes) { let n = 0; let mul = 1; for (let i = 0; i < Math.min(10, bytes.length); i++) { const byte = bytes[i]; n += (byte & 127) * mul; if (!(byte & 128)) return { value: n, length: i + 1 }; mul *= 128; } throw new Error("invalid save frame length"); } function concatBytes(...parts) { const arrays = parts.map(part => part instanceof Uint8Array ? part : new Uint8Array(part || [])); const total = arrays.reduce((sum, part) => sum + part.length, 0); const out = new Uint8Array(total); let offset = 0; for (const part of arrays) { out.set(part, offset); offset += part.length; } return out; } async function transformBytes(bytes, format, decompress = false) { const Ctor = decompress ? global.DecompressionStream : global.CompressionStream; if (typeof Ctor !== "function") return null; try { const stream = new Ctor(format); const reader = stream.readable.getReader(); const sink = stream.writable.getWriter(); const chunks = []; let total = 0; // Read the output concurrently with writes. Waiting for write/close before // draining the readable side can deadlock in browsers that apply stream // backpressure to CompressionStream/DecompressionStream. const readTask = (async () => { while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = value instanceof Uint8Array ? value : new Uint8Array(value || []); chunks.push(chunk); total += chunk.length; } })(); try { await sink.write(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || [])); await sink.close(); await readTask; } catch (error) { try { await sink.abort(error); } catch (_) {} try { await reader.cancel(error); } catch (_) {} try { await readTask; } catch (_) {} throw error; } finally { try { sink.releaseLock(); } catch (_) {} try { reader.releaseLock(); } catch (_) {} } const out = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { out.set(chunk, offset); offset += chunk.length; } return out; } catch (_) { return null; } } async function makeSaveFrame(rawBytes) { const raw = rawBytes instanceof Uint8Array ? rawBytes : new Uint8Array(rawBytes || []); const compressed = await transformBytes(raw, "deflate", false); const useCompressed = Boolean(compressed && compressed.length < raw.length); const payload = useCompressed ? compressed : raw; const taggedLength = payload.length * 2 + (useCompressed ? 1 : 0); return { frame: concatBytes(encodeVarUint(taggedLength), payload), compressed: useCompressed, compressedBytes: payload.length, }; } async function openSaveFrame(encodedBytes) { const all = encodedBytes instanceof Uint8Array ? encodedBytes : new Uint8Array(encodedBytes || []); const prefix = decodeVarUintPrefix(all); const compressed = Boolean(prefix.value & 1); const payloadLength = Math.floor(prefix.value / 2); const end = prefix.length + payloadLength; if (payloadLength < 1 || end > all.length) throw new Error("truncated save frame"); const payload = all.subarray(prefix.length, end); if (!compressed) return { raw: payload, frameBytes: end, compressed: false }; const raw = await transformBytes(payload, "deflate", true); if (!raw) throw new Error("deflate save data is not supported in this browser"); return { raw, frameBytes: end, compressed: true }; } function toSafeUInt(value = 0) { const n = Math.floor(Number(value) || 0); return Number.isFinite(n) && n > 0 ? Math.min(n, Number.MAX_SAFE_INTEGER) : 0; } function toSafeInt(value = 0) { const n = Math.round(Number(value) || 0); return Number.isFinite(n) ? Math.max(Number.MIN_SAFE_INTEGER, Math.min(Number.MAX_SAFE_INTEGER, n)) : 0; } function zigzagEncode(value = 0) { const n = toSafeInt(value); return n < 0 ? (-n * 2 - 1) : (n * 2); } function zigzagDecode(value = 0) { const n = toSafeUInt(value); return (n % 2) ? -((n + 1) / 2) : (n / 2); } class BinaryWriter { constructor(stats = null) { this.bytes = []; this.stats = stats; this.activeCategory = "format"; } get length() { return this.bytes.length; } pushByte(value = 0) { this.bytes.push((Number(value) || 0) & 255); if (this.stats) { const key = this.activeCategory || "format"; this.stats.sections[key] = (this.stats.sections[key] || 0) + 1; } } category(name, fn) { const previous = this.activeCategory; this.activeCategory = String(name || "format"); try { return fn(); } finally { this.activeCategory = previous; } } setDetail(name, value) { if (this.stats) this.stats.details[String(name)] = value; } addDetail(name, value = 1) { if (!this.stats) return; const key = String(name); this.stats.details[key] = (Number(this.stats.details[key]) || 0) + (Number(value) || 0); } u(value = 0) { let n = toSafeUInt(value); while (n >= 128) { this.pushByte((n % 128) | 128); n = Math.floor(n / 128); } this.pushByte(n & 127); } s(value = 0) { this.u(zigzagEncode(value)); } b(value = 0) { this.pushByte(value); } u32(value = 0) { const n = Number(value) >>> 0; this.pushByte(n & 255); this.pushByte((n >>> 8) & 255); this.pushByte((n >>> 16) & 255); this.pushByte((n >>> 24) & 255); } str(value = "") { const bytes = textEncoder.encode(String(value || "")); this.u(bytes.length); for (const byte of bytes) this.pushByte(byte); } raw(value = []) { const bytes = value instanceof Uint8Array ? value : new Uint8Array(value || []); for (const byte of bytes) this.pushByte(byte); } out() { return new Uint8Array(this.bytes); } } class BinaryReader { constructor(bytes = []) { this.bytes = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []); this.pos = 0; } ensure(count = 1) { if (this.pos + count > this.bytes.length) throw new Error("truncated binary save"); } u() { let n = 0; let mul = 1; for (let i = 0; i < 10; i++) { this.ensure(1); const byte = this.bytes[this.pos++]; n += (byte & 127) * mul; if (!(byte & 128)) return n; mul *= 128; } throw new Error("invalid varuint in binary save"); } s() { return zigzagDecode(this.u()); } b() { this.ensure(1); return this.bytes[this.pos++]; } u32() { this.ensure(4); const n = (this.bytes[this.pos]) | (this.bytes[this.pos + 1] << 8) | (this.bytes[this.pos + 2] << 16) | (this.bytes[this.pos + 3] << 24); this.pos += 4; return n >>> 0; } str() { const len = this.u(); this.ensure(len); const slice = this.bytes.subarray(this.pos, this.pos + len); this.pos += len; return textDecoder.decode(slice); } raw(length = 0) { const len = Math.max(0, Number(length) || 0); this.ensure(len); const slice = this.bytes.subarray(this.pos, this.pos + len); this.pos += len; return slice; } } let activeStringWriteMap = null; let activeStringReadList = null; function utf8Length(value = "") { return textEncoder.encode(String(value || "")).length; } function collectStringCounts(snapshot) { const counts = new Map(); const add = value => { const str = String(value || ""); if (str.length < 2 || utf8Length(str) < 4) return; counts.set(str, (counts.get(str) || 0) + 1); }; for (const row of snapshot?.i || []) { if (isPhysicsTypeId(row?.[0] || 0)) continue; const extra = Array.isArray(row?.[4]) ? row[4] : []; for (const value of extra) if (typeof value === "string") add(value); } return [...counts.entries()] .filter(([, count]) => count > 1) .sort((a, b) => (b[0].length * b[1]) - (a[0].length * a[1])) .map(([value]) => value) .slice(0, 63); } function writeStringTable(writer, list = []) { const values = Array.isArray(list) ? list : []; writer.setDetail("stringTableEntries", values.length); if (!values.length) { activeStringWriteMap = null; return; } writer.u(values.length); activeStringWriteMap = new Map(values.map((value, i) => [value, i])); for (const value of values) writer.str(value); } function readStringTable(reader, present = false) { if (!present) { activeStringReadList = null; return; } const count = reader.u(); const list = []; for (let i = 0; i < count; i++) list.push(reader.str()); activeStringReadList = list.length ? list : null; } function writeSaveString(writer, value = "") { const s = String(value || ""); if (!activeStringWriteMap) return writer.str(s); const index = activeStringWriteMap.get(s); if (index == null) { writer.u(0); writer.str(s); } else { writer.u(index + 1); } } function readSaveString(reader) { if (!activeStringReadList) return reader.str(); const index = reader.u(); if (!index) return reader.str(); return activeStringReadList[index - 1] || ""; } function clampToBits(value = 0, bits = 8) { const max = (2 ** Math.max(0, bits)) - 1; return Math.max(0, Math.min(max, toSafeUInt(value))); } function writeBitFields(writer, values = [], bits = []) { let acc = 0; let accBits = 0; for (let i = 0; i < bits.length; i++) { const width = Math.max(0, Number(bits[i]) || 0); if (!width) continue; let value = clampToBits(values?.[i] || 0, width); let left = width; while (left > 0) { const room = 8 - accBits; const take = Math.min(room, left); const mask = (1 << take) - 1; acc |= (value & mask) << accBits; accBits += take; value = Math.floor(value / (2 ** take)); left -= take; if (accBits === 8) { writer.b(acc); acc = 0; accBits = 0; } } } if (accBits > 0) writer.b(acc); } function readBitFields(reader, bits = []) { const out = []; let acc = 0; let accBits = 0; for (const rawWidth of bits) { const width = Math.max(0, Number(rawWidth) || 0); let value = 0; let shift = 0; let left = width; while (left > 0) { if (accBits === 0) { acc = reader.b(); accBits = 8; } const take = Math.min(accBits, left); const mask = (1 << take) - 1; value += (acc & mask) * (2 ** shift); acc = Math.floor(acc / (2 ** take)); accBits -= take; shift += take; left -= take; } out.push(value); } return out; } function writeQuantizedBitFields(writer, values = [], count = 0, bits = 6, step = 1) { const list = []; for (let i = 0; i < count; i++) list.push(Math.round((Number(values?.[i]) || 0) / Math.max(1, step))); writeBitFields(writer, list, Array.from({ length: count }, () => bits)); } function readQuantizedBitFields(reader, count = 0, bits = 6, step = 1) { return readBitFields(reader, Array.from({ length: count }, () => bits)).map(v => v * Math.max(1, step)); } function writeFixedPacked(writer, arr = [], count = 0, bits = 8, step = 1) { writeQuantizedBitFields(writer, arr, count, bits, step); } function readFixedPacked(reader, count = 0, bits = 8, step = 1) { return readQuantizedBitFields(reader, count, bits, step); } function writeFixedDiffPacked(writer, base = [], current = [], count = 0, bits = 8) { let mask = 0; const values = []; const widths = []; for (let i = 0; i < count; i++) { if (valuesDiffer(current?.[i], base?.[i])) { mask |= (1 << i); values.push(current?.[i] || 0); widths.push(bits); } } writeBitFields(writer, [mask], [count]); if (widths.length) writeBitFields(writer, values, widths); } function readFixedDiffPacked(reader, base = [], count = 0, bits = 8) { const [mask] = readBitFields(reader, [count]); const out = Array.from({ length: count }, (_, i) => base?.[i] || 0); const changed = []; for (let i = 0; i < count; i++) if (mask & (1 << i)) changed.push(i); const values = changed.length ? readBitFields(reader, changed.map(() => bits)) : []; changed.forEach((index, i) => { out[index] = values[i]; }); return out; } function writeIndexList(writer, arr = []) { const list = Array.isArray(arr) ? arr : []; writer.u(list.length); for (const idx of list) writer.u(idx || 0); } function readIndexList(reader) { const count = reader.u(); const out = []; for (let i = 0; i < count; i++) out.push(reader.u()); return out; } function valuesDiffer(a, b) { return (Number(a) || 0) !== (Number(b) || 0); } function arrayHasValues(arr = [], defaults = []) { for (let i = 0; i < Math.max(arr?.length || 0, defaults.length); i++) { if (valuesDiffer(arr?.[i], defaults?.[i] || 0)) return true; } return false; } function writeTimers(writer, timers = []) { const defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100]; let mask = 0; for (let i = 0; i < defaults.length; i++) if (valuesDiffer(timers?.[i], defaults[i])) mask |= (1 << i); writeBitFields(writer, [mask], [defaults.length]); for (let i = 0; i < defaults.length; i++) if (mask & (1 << i)) writer.s(timers[i] || 0); } function readTimers(reader) { const defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100]; const out = defaults.slice(); const [mask] = readBitFields(reader, [defaults.length]); for (let i = 0; i < defaults.length; i++) if (mask & (1 << i)) out[i] = reader.s(); return out; } function writeModes(writer, modes = []) { writeBitFields(writer, [modes?.[0] || 0, modes?.[1] || 0, modes?.[2] || 0], [2, 2, 1]); } function readModes(reader) { return readBitFields(reader, [2, 2, 1]); } function defaultNeedsFromVitals(hunger = 0, energy = 0, sleepPressure = 0) { const food = Math.max(0, Math.min(100, Math.round(Number(hunger) || 0))); const sleep = Math.max(0, Math.min(100, Math.round(Math.max(Number(sleepPressure) || 0, 100 - (Number(energy) || 0))))); return [food, sleep, 0, 0, 0, 0]; } function writeRelationships(writer, rows = []) { const list = (Array.isArray(rows) ? rows : []) .map(row => [toSafeUInt(row?.[0] || 0), toSafeInt(row?.[1] || 0), toSafeInt(row?.[2] || 0), toSafeUInt(row?.[3] || 0)]) .sort((a, b) => a[0] - b[0]); writer.u(list.length); let previousTarget = 0; for (const row of list) { writer.u(row[0] - previousTarget); previousTarget = row[0]; writer.s(row[1]); writer.s(row[2]); writer.u(row[3]); } } function readRelationships(reader) { const count = reader.u(); const out = []; let previousTarget = 0; for (let i = 0; i < count; i++) { const target = previousTarget + reader.u(); previousTarget = target; out.push([target, reader.s(), reader.s(), reader.u()]); } return out; } function worldSeedWords(value = "") { const seed = String(value || ""); const match = /^w([0-9a-f]{8})([0-9a-f]{8})$/i.exec(seed); if (match) return [parseInt(match[1], 16) >>> 0, parseInt(match[2], 16) >>> 0]; let high = 2166136261; let low = 2246822519; for (let i = 0; i < seed.length; i++) { const code = seed.charCodeAt(i); high ^= code; high = Math.imul(high, 16777619); low ^= code + i * 131; low = Math.imul(low, 3266489917); } return [high >>> 0, low >>> 0]; } function worldSeedFromWords(high = 0, low = 0) { if (typeof global.TarinaiSeedFactory?.worldSeedFromWords === "function") return global.TarinaiSeedFactory.worldSeedFromWords(high, low); return `w${(Number(high) >>> 0).toString(16).padStart(8, "0")}${(Number(low) >>> 0).toString(16).padStart(8, "0")}`; } function canonicalWorldSeed(value = "") { const [high, low] = worldSeedWords(value); return worldSeedFromWords(high, low); } function writeWorld(writer, w = []) { writeBitFields(writer, [w[0] || 0, w[1] || 0, w[2] || 0, w[4] || 0], [2, 3, 4, 3]); writer.u(w[3] || 0); // worldTick10 let mask = 0; if (valuesDiffer(w[5], -9990)) mask |= 1; if (valuesDiffer(w[6], 1)) mask |= 2; if (valuesDiffer(w[7], 0)) mask |= 4; if (valuesDiffer(w[9], 0)) mask |= 8; if (valuesDiffer(w[10], 0)) mask |= 16; if (valuesDiffer(w[11], 0)) mask |= 32; writeBitFields(writer, [mask], [6]); if (mask & 1) writer.s(w[5] || 0); // lastBirthAt10 can be negative if (mask & 2) writer.u(w[6] || 1); // generation if (mask & 4) writer.u(w[7] || 0); // dead count const [seedHigh, seedLow] = worldSeedWords(w[8] || ""); writer.u32(seedHigh); writer.u32(seedLow); if (mask & 8) writer.u(w[9] || 0); // birthSerial if (mask & 16) writer.u(w[10] || 0); // tarinai population limit if (mask & 32) writer.u(w[11] || 0); // object limit } function readWorld(reader) { const [field, ground, mood, weather] = readBitFields(reader, [2, 3, 4, 3]); const tick = reader.u(); const [mask] = readBitFields(reader, [6]); const lastBirthAt = (mask & 1) ? reader.s() : -9990; const generation = (mask & 2) ? reader.u() : 1; const deadCount = (mask & 4) ? reader.u() : 0; const seed = worldSeedFromWords(reader.u32(), reader.u32()); const birthSerial = (mask & 8) ? reader.u() : 0; const tarinaiPopulationLimit = (mask & 16) ? reader.u() : 0; const objectLimit = (mask & 32) ? reader.u() : 0; return [field, ground, mood, tick, weather, lastBirthAt, generation, deadCount, seed, birthSerial, tarinaiPopulationLimit, objectLimit]; } const birthHash32 = global.TarinaiCoreHelpers?.birthHash32 || (seed => { const match = /^b[0-9a-z]+_([0-9a-z]+)$/i.exec(String(seed || "")); if (match) { const parsed = parseInt(match[1], 36); if (Number.isFinite(parsed)) return parsed >>> 0; } let h = 2166136261; const value = String(seed || ""); for (let i = 0; i < value.length; i++) { h ^= value.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; }); function birthSeedFromDescriptor(descriptor = [], worldSeed = "", fallbackSerial = 1) { const serial = Math.max(1, Math.floor(Number(descriptor?.[0]) || fallbackSerial || 1)); const expected = global.TarinaiSeedFactory?.childSeed?.(worldSeed, serial, []) || `b${serial.toString(36)}_0`; const expectedHash = birthHash32(expected); const hash = Number.isFinite(Number(descriptor?.[1])) ? (Number(descriptor[1]) >>> 0) : expectedHash; return { serial, hash, expectedHash, seed: hash === expectedHash ? expected : `b${serial.toString(36)}_${hash.toString(36)}` }; } function writeBirthDescriptor(writer, descriptor = [], worldSeed = "", fallbackSerial = 1) { const identity = birthSeedFromDescriptor(descriptor, worldSeed, fallbackSerial); const customHash = identity.hash !== identity.expectedHash; writer.u(identity.serial * 2 + (customHash ? 1 : 0)); if (customHash) writer.u32(identity.hash); return identity.seed; } function readBirthDescriptor(reader, worldSeed = "", fallbackSerial = 1) { const token = reader.u(); const serial = Math.max(1, Math.floor(token / 2) || fallbackSerial || 1); const expected = global.TarinaiSeedFactory?.childSeed?.(worldSeed, serial, []) || `b${serial.toString(36)}_0`; const hash = (token & 1) ? reader.u32() : birthHash32(expected); return { descriptor: [serial, hash], seed: hash === birthHash32(expected) ? expected : `b${serial.toString(36)}_${hash.toString(36)}` }; } function seedPersonalityArray(seed = "", flags = 0) { // \u305a\u3093\u3061\u3069\u308c\u3044\u306f\u8a95\u751f\u6642\u6027\u683c\u3082\u5168\u8ef8 -1 \u306a\u306e\u3067\u3001\u5dee\u5206\u57fa\u6e96\u30820\u56fa\u5b9a\u306b\u3059\u308b\u3002 if ((Number(flags) || 0) & (1 << 5)) return [0, 0, 0, 0]; return global.TarinaiSeedFactory.personalityArray(seed) || [100, 100, 100, 100]; } function seedGeneticsArray(seed = "", flags = 0) { const zunchiSlave = Boolean((Number(flags) || 0) & ((1 << 5) | (1 << 6))); const genetics = global.TarinaiSeedFactory?.birthProfile?.(seed, { isZunchiSlave: zunchiSlave, zunchiSlaveLocked: zunchiSlave })?.genetics || {}; return [ Math.round((Number(genetics.lifeSpanMul) || 1) * 100), Math.round((Number(genetics.attackMul) || 1) * 100), Math.round((Number(genetics.speedMul) || 1) * 100), Math.round((Number(genetics.sizeMul) || 1) * 100), Math.round((Number(genetics.temperatureOffset) || 0) * 10), ]; } function writeSignedDiffList(writer, base = [], current = [], count = 0) { let mask = 0; for (let i = 0; i < count; i++) if (valuesDiffer(current?.[i], base?.[i])) mask |= (1 << i); writeBitFields(writer, [mask], [count]); for (let i = 0; i < count; i++) if (mask & (1 << i)) writer.s((current?.[i] || 0) - (base?.[i] || 0)); } function readSignedDiffList(reader, base = [], count = 0) { const [mask] = readBitFields(reader, [count]); const out = Array.from({ length: count }, (_, i) => base?.[i] || 0); for (let i = 0; i < count; i++) if (mask & (1 << i)) out[i] = (base?.[i] || 0) + reader.s(); return out; } class CompactBitWriter { constructor() { this.bytes = []; this.buffer = 0; this.bits = 0; } write(value = 0, width = 0) { let n = clampToBits(value, width); let left = Math.max(0, Number(width) || 0); while (left > 0) { const room = 8 - this.bits; const take = Math.min(room, left); const mask = (1 << take) - 1; this.buffer |= (n & mask) << this.bits; this.bits += take; n = Math.floor(n / (2 ** take)); left -= take; if (this.bits === 8) { this.bytes.push(this.buffer); this.buffer = 0; this.bits = 0; } } } finish() { if (this.bits > 0) { this.bytes.push(this.buffer); this.buffer = 0; this.bits = 0; } return new Uint8Array(this.bytes); } } class CompactBitReader { constructor(bytes = []) { this.bytes = bytes; this.pos = 0; this.buffer = 0; this.bits = 0; } read(width = 0) { let value = 0; let shift = 0; let left = Math.max(0, Number(width) || 0); while (left > 0) { if (this.bits === 0) { if (this.pos >= this.bytes.length) throw new Error("truncated tarinai bit plane"); this.buffer = this.bytes[this.pos++]; this.bits = 8; } const take = Math.min(this.bits, left); const mask = (1 << take) - 1; value += (this.buffer & mask) * (2 ** shift); this.buffer = Math.floor(this.buffer / (2 ** take)); this.bits -= take; shift += take; left -= take; } return value; } } function diffMask(base = [], current = [], count = 0) { let mask = 0; for (let i = 0; i < count; i++) if (valuesDiffer(current?.[i], base?.[i])) mask |= (1 << i); return mask; } function analyzeTarinaiRow(row = [], worldSeed = "", rowIndex = 0) { const flags = row[0] || 0; const birth = birthSeedFromDescriptor(row[1], worldSeed, rowIndex + 1); const defaultPersonality = seedPersonalityArray(birth.seed, flags); const defaultTimers = [0, 0, 0, 0, 0, 0, 0, 0, 100]; const genetics = Array.isArray(row[18]) ? row[18] : []; const fightStats = Array.isArray(row[19]) ? row[19] : []; let bodyMask = 0; if (arrayHasValues(row[10], defaultPersonality)) bodyMask |= 1; if ((row[11] || []).length) bodyMask |= 2; if ((row[12] || []).length) bodyMask |= 4; if (arrayHasValues(row[13], defaultTimers)) bodyMask |= 8; if (arrayHasValues(row[14], [0, 0, 0])) bodyMask |= 16; const pinIdx = row[15] ?? -1; const nestIdx = row[16] ?? -1; const stateId = row[17]?.[0] ? 1 : 0; const stateTarget = row[17]?.[1] ?? -1; const geneticsBase = seedGeneticsArray(birth.seed, flags); let linkMask = 0; if (pinIdx >= 0) linkMask |= 1; if (nestIdx >= 0) linkMask |= 2; return { flags, birth, bodyMask, linkMask, pinIdx, nestIdx, stateId, stateTarget, genetics, fightStats, geneticsBase, geneticsMask: diffMask(geneticsBase, genetics, 5), fightMask: diffMask([0, 0, 0], fightStats, 3), hunger: Math.max(0, Math.min(100, Math.round(Number(row[6]) || 0))), energy: Math.max(0, Math.min(100, Math.round(Number(row[7]) || 0))), sleepPressure: Math.max(0, Math.min(100, Math.round(Number(row[8]) || 0))), }; } const TARINAI_COLUMN_LAYOUT = Object.freeze([ ["flags", 10], ["hunger", 7], ["energy", 7], ["sleepPressure", 7], ["bodyMask", 5], ["linkMask", 2], ["stateId", 1], ["geneticsMask", 5], ["fightMask", 3], ]); function writeTarinaiBitPlane(writer, metas = []) { let columnMask = 0; for (let column = 0; column < TARINAI_COLUMN_LAYOUT.length; column++) { const key = TARINAI_COLUMN_LAYOUT[column][0]; if (metas.some(meta => (Number(meta?.[key]) || 0) !== 0)) columnMask |= (1 << column); } writer.u(columnMask); const bits = new CompactBitWriter(); for (let column = 0; column < TARINAI_COLUMN_LAYOUT.length; column++) { if (!(columnMask & (1 << column))) continue; const [key, width] = TARINAI_COLUMN_LAYOUT[column]; for (const meta of metas) bits.write(meta?.[key] || 0, width); } writer.raw(bits.finish()); writer.setDetail("tarinaiColumnMask", columnMask); } function readTarinaiBitPlane(reader, count = 0) { const columnMask = reader.u(); let totalBits = 0; for (let column = 0; column < TARINAI_COLUMN_LAYOUT.length; column++) { if (columnMask & (1 << column)) totalBits += Math.max(0, count) * TARINAI_COLUMN_LAYOUT[column][1]; } const bits = new CompactBitReader(reader.raw(Math.ceil(totalBits / 8))); const metas = Array.from({ length: Math.max(0, count) }, () => ({})); for (let column = 0; column < TARINAI_COLUMN_LAYOUT.length; column++) { const [key, width] = TARINAI_COLUMN_LAYOUT[column]; if (!(columnMask & (1 << column))) { for (const meta of metas) meta[key] = 0; continue; } for (const meta of metas) meta[key] = bits.read(width); } return metas; } function writeDiffValues(writer, base = [], current = [], count = 0, mask = 0) { for (let i = 0; i < count; i++) if (mask & (1 << i)) writer.s((current?.[i] || 0) - (base?.[i] || 0)); } function readDiffValues(reader, base = [], count = 0, mask = 0) { const out = Array.from({ length: count }, (_, i) => base?.[i] || 0); for (let i = 0; i < count; i++) if (mask & (1 << i)) out[i] = (base?.[i] || 0) + reader.s(); return out; } function writeTarinaiPayload(writer, row = [], meta = null, coordState = null, worldSeed = "", rowIndex = 0) { const info = meta || analyzeTarinaiRow(row, worldSeed, rowIndex); let birthSeed = ""; writer.category("tarinaiSeed", () => { birthSeed = writeBirthDescriptor(writer, row[1], worldSeed, rowIndex + 1); }); writer.addDetail("birthSeedRawUtf8Bytes", utf8Length(birthSeed)); const x = toSafeInt(row[2] || 0); const y = toSafeInt(row[3] || 0); const age = toSafeInt(row[4] || 0); const generation = toSafeInt(row[5] || 1); writer.s(x - (coordState?.x || 0)); writer.s(y - (coordState?.y || 0)); writer.s(age - (coordState?.age || 0)); writer.s(generation - (coordState?.generation || 1)); if (coordState) { coordState.x = x; coordState.y = y; coordState.age = age; coordState.generation = generation; } const defaultPersonality = seedPersonalityArray(birthSeed, info.flags); if (info.bodyMask & 1) writeFixedDiffPacked(writer, defaultPersonality, row[10], 4, 8); if (info.bodyMask & 2) { writer.addDetail("parentLinks", (row[11] || []).length); writer.category("tarinaiRelationships", () => writeIndexList(writer, row[11])); } if (info.bodyMask & 4) { writer.addDetail("relationshipRows", (row[12] || []).length); writer.category("tarinaiRelationships", () => writeRelationships(writer, row[12])); } if (info.bodyMask & 8) writeTimers(writer, row[13]); if (info.bodyMask & 16) writeModes(writer, row[14]); if (info.linkMask & 1) writer.u(info.pinIdx); if (info.linkMask & 2) writer.u(info.nestIdx); if (info.stateId) writer.s(info.stateTarget); if (info.geneticsMask) writeDiffValues(writer, info.geneticsBase, info.genetics, 5, info.geneticsMask); if (info.fightMask) writeDiffValues(writer, [0, 0, 0], info.fightStats, 3, info.fightMask); } function readTarinaiPayload(reader, header = {}, coordState = null, worldSeed = "", rowIndex = 0) { const flags = header.flags || 0; const birth = readBirthDescriptor(reader, worldSeed, rowIndex + 1); const x = (coordState?.x || 0) + reader.s(); const y = (coordState?.y || 0) + reader.s(); const age = (coordState?.age || 0) + reader.s(); const generation = (coordState?.generation || 1) + reader.s(); if (coordState) { coordState.x = x; coordState.y = y; coordState.age = age; coordState.generation = generation; } const hunger = header.hunger || 0; const energy = header.energy || 0; const sleepPressure = header.sleepPressure || 0; const bodyMask = header.bodyMask || 0; const linkMask = header.linkMask || 0; const needs = defaultNeedsFromVitals(hunger, energy, sleepPressure); const defaultPersonality = seedPersonalityArray(birth.seed, flags); const current = (bodyMask & 1) ? readFixedDiffPacked(reader, defaultPersonality, 4, 8) : defaultPersonality; const parents = (bodyMask & 2) ? readIndexList(reader) : []; const relationships = (bodyMask & 4) ? readRelationships(reader) : []; const timers = (bodyMask & 8) ? readTimers(reader) : [0, 0, 0, 0, 0, 0, 0, 0, 100]; const modes = (bodyMask & 16) ? readModes(reader) : [0, 0, 0]; const pinIdx = (linkMask & 1) ? reader.u() : -1; const nestIdx = (linkMask & 2) ? reader.u() : -1; const stateInfo = header.stateId ? [header.stateId, reader.s()] : [0, -1]; const geneticsBase = seedGeneticsArray(birth.seed, flags); const genetics = header.geneticsMask ? readDiffValues(reader, geneticsBase, 5, header.geneticsMask) : []; const fightStats = header.fightMask ? readDiffValues(reader, [0, 0, 0], 3, header.fightMask) : []; return [flags, birth.descriptor, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo, genetics, fightStats]; } const PHYSICS_BODY_TYPES = new Set(["rotator", "poison_block", "reciprocator"]); const PHYSICS_CONSTRAINT_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]); function physicsTypeFromId(typeId = 0) { return SaveSchema.itemTypeValue(Number(typeId) || 0, ""); } function isPhysicsTypeId(typeId = 0) { const type = physicsTypeFromId(typeId); return PHYSICS_BODY_TYPES.has(type) || PHYSICS_CONSTRAINT_TYPES.has(type); } function defaultPhysicsAngle(type = "") { const fallback = type === "reciprocator" ? Math.PI / 2 : 0; try { return Math.round(Number(global.defaultItemAngle?.(type) ?? fallback) * 1000); } catch (_) { return Math.round(fallback * 1000); } } function defaultPhysicsSegments(type = "") { if (type === "poison_block") return [[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]]; if (type === "rotator") return [[-78, 0, 78, 0], [0, -52, 0, 52]]; return [[-78, 0, 78, 0]]; } function defaultPhysicsKind(type = "") { if (type === "rotator") return "rotational"; if (type === "reciprocator") return "linear"; if (type === "poison_block") return "passive"; if (type === "rope") return "flexible-distance"; if (type === "rod") return "rigid-distance"; if (type === "spring") return "spring-distance"; if (type === "wire" || type === "insulated_wire") return "signal-wire"; return "none"; } function defaultPhysicsBodyCompact(type = "", x = 0, y = 0) { const angle = defaultPhysicsAngle(type); return { kind: defaultPhysicsKind(type), pose: [x, y, angle], vel: [0, 0, 0, 0], motor: type === "rotator" ? [1, 20, 1] : (type === "reciprocator" ? [1, 920, 1] : [0, 0, 1]), rail: type === "reciprocator" ? [angle, 1500, 0, x, y] : null, shape: [type === "poison_block" ? 110 : 120, defaultPhysicsSegments(type)], collision: type === "poison_block" ? [1, 1] : [1, 0], hazard: type === "poison_block" ? ["poison", 70] : null, mass: type === "poison_block" ? 5 : 10, inertia: type === "poison_block" ? 3200 : 22000, }; } function arraysEqualNumbers(a = [], b = []) { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if ((Number(a[i]) || 0) !== (Number(b[i]) || 0)) return false; return true; } function segmentsEqual(a = [], b = []) { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (!arraysEqualNumbers(a[i], b[i])) return false; return true; } function writeChangedSignedArray(writer, current = [], defaults = [], count = 0) { let mask = 0; for (let i = 0; i < count; i++) if ((Number(current?.[i]) || 0) !== (Number(defaults?.[i]) || 0)) mask |= (1 << i); writeBitFields(writer, [mask], [count]); for (let i = 0; i < count; i++) if (mask & (1 << i)) writer.s(current?.[i] || 0); } function readChangedSignedArray(reader, defaults = [], count = 0) { const [mask] = readBitFields(reader, [count]); const out = Array.from({ length: count }, (_, i) => Number(defaults?.[i]) || 0); for (let i = 0; i < count; i++) if (mask & (1 << i)) out[i] = reader.s(); return out; } function writePhysicsSegments(writer, segments = []) { const list = Array.isArray(segments) ? segments.slice(0, 128) : []; writer.u(list.length); const previous = [0, 0, 0, 0]; for (const segment of list) { for (let i = 0; i < 4; i++) { const value = toSafeInt(segment?.[i] || 0); writer.s(value - previous[i]); previous[i] = value; } } } function readPhysicsSegments(reader) { const count = Math.min(128, reader.u()); const out = []; const previous = [0, 0, 0, 0]; for (let n = 0; n < count; n++) { const segment = []; for (let i = 0; i < 4; i++) { previous[i] += reader.s(); segment.push(previous[i]); } out.push(segment); } return out; } function writePhysicsBodyExtra(writer, extra, type, row = []) { const body = extra?.body && typeof extra.body === "object" ? extra.body : {}; const x = toSafeInt(row?.[1] || 0); const y = toSafeInt(row?.[2] || 0); const defaults = defaultPhysicsBodyCompact(type, x, y); const pose = Array.isArray(body.pose) ? body.pose : defaults.pose; const vel = Array.isArray(body.vel) ? body.vel : defaults.vel; const motor = Array.isArray(body.motor) ? body.motor : defaults.motor; const rail = type === "reciprocator" ? (Array.isArray(body.rail) ? body.rail : defaults.rail) : null; const shape = Array.isArray(body.shape) ? body.shape : defaults.shape; const collision = Array.isArray(body.collision) ? body.collision : defaults.collision; const hazard = type === "poison_block" ? (Array.isArray(body.hazard) ? body.hazard : defaults.hazard) : null; const kind = String(body.kind || defaults.kind); let mask = 0; if ((Number(pose[0]) || 0) !== x || (Number(pose[1]) || 0) !== y) mask |= 1; if ((Number(pose[2]) || 0) !== defaults.pose[2]) mask |= 2; if (!arraysEqualNumbers(vel, defaults.vel)) mask |= 4; if (!arraysEqualNumbers(motor, defaults.motor)) mask |= 8; if (type === "reciprocator" && !arraysEqualNumbers(rail, defaults.rail)) mask |= 16; if ((Number(shape?.[0]) || 0) !== defaults.shape[0]) mask |= 32; if (!segmentsEqual(shape?.[1], defaults.shape[1])) mask |= 64; if (!arraysEqualNumbers(collision, defaults.collision)) mask |= 128; if (type === "poison_block" && (!hazard || String(hazard[0] || "") !== defaults.hazard[0] || (Number(hazard[1]) || 0) !== defaults.hazard[1])) mask |= 256; if ((Number(body.mass) || 0) !== defaults.mass) mask |= 512; if ((Number(body.inertia) || 0) !== defaults.inertia) mask |= 1024; if (kind !== defaults.kind) mask |= 2048; writer.u(mask); if (mask & 1) { writer.s((Number(pose[0]) || 0) - x); writer.s((Number(pose[1]) || 0) - y); } if (mask & 2) writer.s(pose[2] || 0); if (mask & 4) writeChangedSignedArray(writer, vel, defaults.vel, 4); if (mask & 8) writeChangedSignedArray(writer, motor, defaults.motor, 3); if (mask & 16) writeChangedSignedArray(writer, rail, defaults.rail, 5); if (mask & 32) writer.u(shape?.[0] || 0); if (mask & 64) writePhysicsSegments(writer, shape?.[1] || []); if (mask & 128) writeBitFields(writer, [collision?.[0] ? 1 : 0, collision?.[1] ? 1 : 0], [1, 1]); if (mask & 256) { const hazardKind = String(hazard?.[0] || "poison"); const kindChanged = hazardKind !== defaults.hazard[0]; writeBitFields(writer, [kindChanged ? 1 : 0], [1]); if (kindChanged) writer.str(hazardKind); writer.u(hazard?.[1] || 0); } if (mask & 512) writer.u(body.mass || 0); if (mask & 1024) writer.u(body.inertia || 0); if (mask & 2048) writer.str(kind); } function readPhysicsBodyExtra(reader, type, row = []) { const x = toSafeInt(row?.[1] || 0); const y = toSafeInt(row?.[2] || 0); const defaults = defaultPhysicsBodyCompact(type, x, y); const mask = reader.u(); const pose = defaults.pose.slice(); if (mask & 1) { pose[0] = x + reader.s(); pose[1] = y + reader.s(); } if (mask & 2) pose[2] = reader.s(); const vel = (mask & 4) ? readChangedSignedArray(reader, defaults.vel, 4) : defaults.vel.slice(); const motor = (mask & 8) ? readChangedSignedArray(reader, defaults.motor, 3) : defaults.motor.slice(); const rail = type === "reciprocator" ? ((mask & 16) ? readChangedSignedArray(reader, defaults.rail, 5) : defaults.rail.slice()) : null; const thickness = (mask & 32) ? reader.u() : defaults.shape[0]; const segments = (mask & 64) ? readPhysicsSegments(reader) : defaults.shape[1].map(segment => segment.slice()); const collision = (mask & 128) ? readBitFields(reader, [1, 1]) : defaults.collision.slice(); let hazard = defaults.hazard ? defaults.hazard.slice() : null; if (mask & 256) { const [kindChanged] = readBitFields(reader, [1]); const kind = kindChanged ? reader.str() : defaults.hazard[0]; hazard = [kind, reader.u()]; } const mass = (mask & 512) ? reader.u() : defaults.mass; const inertia = (mask & 1024) ? reader.u() : defaults.inertia; const kind = (mask & 2048) ? reader.str() : defaults.kind; return { pf: 2, body: { type, kind, pose, vel, motor, rail, shape: [thickness, segments], collision, hazard, mass, inertia } }; } function writePhysicsEndpoint(writer, endpoint, row = []) { if (!endpoint) { writeBitFields(writer, [0], [1]); return; } writeBitFields(writer, [1], [1]); const kind = String(endpoint.kind || "item"); const kindCode = kind === "item" ? 0 : (kind === "tarinai" ? 1 : 2); writeBitFields(writer, [kindCode], [2]); if (kindCode === 2) writer.str(kind); const refIdx = Number.isInteger(Number(endpoint.refIdx)) ? Number(endpoint.refIdx) : -1; writer.u(refIdx >= 0 ? refIdx + 1 : 0); const indexed = refIdx >= 0; const tokenPresent = !indexed && endpoint.token != null && Number.isFinite(Number(endpoint.token)); let mask = 0; // A valid item/Tarinai index is the canonical reference. Text identity, // live tokens and fallback world coordinates are only needed when the // endpoint target is absent from the save. if (!indexed && endpoint.id) mask |= 1; if (!indexed && endpoint.type) mask |= 2; if (!indexed && endpoint.label) mask |= 4; if (tokenPresent) mask |= 8; if (Number(endpoint.lx) || 0) mask |= 16; if (Number(endpoint.ly) || 0) mask |= 32; if (endpoint.center) mask |= 64; if (!indexed && (Number(endpoint.x) || 0) !== (Number(row?.[1]) || 0)) mask |= 128; if (!indexed && (Number(endpoint.y) || 0) !== (Number(row?.[2]) || 0)) mask |= 256; if (endpoint.t != null && Number.isFinite(Number(endpoint.t))) mask |= 512; if (endpoint.port) mask |= 1024; writer.u(mask); if (mask & 1) writer.str(endpoint.id); if (mask & 2) writer.str(endpoint.type); if (mask & 4) writer.str(endpoint.label); if (mask & 8) writer.u(endpoint.token); if (mask & 16) writer.s(endpoint.lx); if (mask & 32) writer.s(endpoint.ly); if (mask & 128) writer.s((Number(endpoint.x) || 0) - (Number(row?.[1]) || 0)); if (mask & 256) writer.s((Number(endpoint.y) || 0) - (Number(row?.[2]) || 0)); if (mask & 512) writer.u(endpoint.t); if (mask & 1024) writer.str(endpoint.port); } function readPhysicsEndpoint(reader, row = []) { const [present] = readBitFields(reader, [1]); if (!present) return null; const [kindCode] = readBitFields(reader, [2]); const kind = kindCode === 0 ? "item" : (kindCode === 1 ? "tarinai" : reader.str()); const encodedRef = reader.u(); const refIdx = encodedRef ? encodedRef - 1 : -1; const mask = reader.u(); const id = (mask & 1) ? reader.str() : ""; const type = (mask & 2) ? reader.str() : ""; const label = (mask & 4) ? reader.str() : ""; const token = (mask & 8) ? reader.u() : null; const lx = (mask & 16) ? reader.s() : 0; const ly = (mask & 32) ? reader.s() : 0; const center = (mask & 64) ? 1 : 0; const x = (Number(row?.[1]) || 0) + ((mask & 128) ? reader.s() : 0); const y = (Number(row?.[2]) || 0) + ((mask & 256) ? reader.s() : 0); const t = (mask & 512) ? reader.u() : null; const port = (mask & 1024) ? reader.str() : ""; return { kind, refIdx, id, type, label, token, lx, ly, center, x, y, t, port }; } function writePhysicsConstraintExtra(writer, extra, type, row = []) { const constraint = extra?.constraint && typeof extra.constraint === "object" ? extra.constraint : {}; const defaultKind = defaultPhysicsKind(type); const kind = String(constraint.kind || defaultKind); const mid = Array.isArray(constraint.mid) ? constraint.mid : [row?.[1] || 0, (row?.[2] || 0) + 10, 0, 0]; let mask = 0; if (kind !== defaultKind) mask |= 1; if ((Number(constraint.length) || 80) !== 80) mask |= 2; if ((Number(mid[0]) || 0) !== (Number(row?.[1]) || 0) || (Number(mid[1]) || 0) !== (Number(row?.[2]) || 0) || (Number(mid[2]) || 0) !== 0 || (Number(mid[3]) || 0) !== 0) mask |= 4; writer.u(mask); if (mask & 1) writer.str(kind); writePhysicsEndpoint(writer, constraint.endpoints?.[0], row); writePhysicsEndpoint(writer, constraint.endpoints?.[1], row); if (mask & 2) writer.u(constraint.length || 0); if (mask & 4) { const defaults = [row?.[1] || 0, (row?.[2] || 0) + 10, 0, 0]; writeChangedSignedArray(writer, mid, defaults, 4); } } function readPhysicsConstraintExtra(reader, type, row = []) { const mask = reader.u(); const kind = (mask & 1) ? reader.str() : defaultPhysicsKind(type); const endpoints = [readPhysicsEndpoint(reader, row), readPhysicsEndpoint(reader, row)]; const length = (mask & 2) ? reader.u() : 80; const defaults = [row?.[1] || 0, (row?.[2] || 0) + 10, 0, 0]; const mid = (mask & 4) ? readChangedSignedArray(reader, defaults, 4) : defaults; return { pf: 2, constraint: { type, kind, endpoints, length, mid } }; } function writePhysicsItemExtra(writer, extra, typeId = 0, row = []) { const type = physicsTypeFromId(typeId); const valid = extra && typeof extra === "object" && Number(extra.pf) === 2 && (extra.body || extra.constraint); writeBitFields(writer, [valid ? 1 : 0], [1]); if (!valid) return; if (PHYSICS_BODY_TYPES.has(type)) writePhysicsBodyExtra(writer, extra, type, row); else writePhysicsConstraintExtra(writer, extra, type, row); } function readPhysicsItemExtra(reader, typeId = 0, row = []) { const [valid] = readBitFields(reader, [1]); if (!valid) return { pf: 2, missing: true }; const type = physicsTypeFromId(typeId); if (PHYSICS_BODY_TYPES.has(type)) return readPhysicsBodyExtra(reader, type, row); return readPhysicsConstraintExtra(reader, type, row); } const STRUCTURE_ITEM_TYPES = new Set(["grass_bed", "plushie"]); function normalTypeFromId(typeId = 0) { return SaveSchema.itemTypeValue(Number(typeId) || 0, ""); } function isPinSaveType(type = "") { return type === "pushpin" || type === "oshibyo" || Boolean(global.isPinType?.(type)); } function isRotatableSaveType(type = "") { return Boolean(global.isRotatableItemType?.(type)); } function isServingFoodSaveType(type = "") { return Boolean(global.isServingFoodType?.(type)); } function defaultAngleMilli(type = "") { try { return Math.round(Number(global.defaultItemAngle?.(type) || 0) * 1000); } catch (_) { return 0; } } function writeFixedSignedDefaults(writer, values = [], defaults = [], count = 0) { let mask = 0; for (let i = 0; i < count; i++) if ((Number(values?.[i]) || 0) !== (Number(defaults?.[i]) || 0)) mask |= (1 << i); writeBitFields(writer, [mask], [count]); for (let i = 0; i < count; i++) if (mask & (1 << i)) writer.s(values?.[i] || 0); } function readFixedSignedDefaults(reader, defaults = [], count = 0) { const [mask] = readBitFields(reader, [count]); const out = Array.from({ length: count }, (_, i) => Number(defaults?.[i]) || 0); for (let i = 0; i < count; i++) if (mask & (1 << i)) out[i] = reader.s(); return out; } function writeGenericExtra(writer, extra = []) { const list = Array.isArray(extra) ? extra : []; writer.u(list.length); for (const value of list) { if (typeof value === "string") { writer.b(1); writeSaveString(writer, value); } else { writer.b(0); writer.s(value || 0); } } } function readGenericExtra(reader) { const count = reader.u(); const out = []; for (let i = 0; i < count; i++) out.push(reader.b() === 1 ? readSaveString(reader) : reader.s()); return out; } function writeNormalItemExtra(writer, extra = [], typeId = 0, row = []) { const type = normalTypeFromId(typeId); const list = Array.isArray(extra) ? extra : []; if (STRUCTURE_ITEM_TYPES.has(type)) { const defaultMax = type === "plushie" ? 10 : 1000; // hp is identical to row amount and is reconstructed instead of stored. writeFixedSignedDefaults(writer, [list[1], list[2], list[3], list[4], list[5], list[6]], [defaultMax, -1, -1, 0, -1, 0], 6); } else if (type === "grass") writeFixedSignedDefaults(writer, list, [100, 100, 0, 0], 4); else if (type === "zunchi") writeFixedSignedDefaults(writer, list, [0, 100, 100], 3); else if (type === "signboard") writeSaveString(writer, list[0] || ""); else if (type === "robot_cleaner") writeFixedSignedDefaults(writer, list, [3, 0], 2); else if (type === "circuit_board") writeSaveString(writer, list[0] || "{}"); else if (type === "duplicator") writeFixedSignedDefaults(writer, list, [-1], 1); else if (type === "ball" || type === "balloon") writeFixedSignedDefaults(writer, list, [0, 0], 2); else if (type === "ant_nest") writeFixedSignedDefaults(writer, list, [0, 0], 2); else if (type === "ant_corpse" || type === "firecracker" || type === "flame_firecracker" || type === "fire") writeFixedSignedDefaults(writer, list, [0], 1); else if (type === "sticky_bomb") writeFixedSignedDefaults(writer, list, [0, -1, 0], 3); else if (type === "nest_box") writeFixedSignedDefaults(writer, list, [0, 0, 0, 0], 4); else if (isPinSaveType(type)) writeFixedSignedDefaults(writer, list, [0, -1, 0, 0, 0], 5); else if (type === "fan") { const angle = defaultAngleMilli(type); writeFixedSignedDefaults(writer, list, [angle, 0, angle, Math.round(35 * Math.PI / 180 * 1000), Math.round(48 * Math.PI / 180 * 1000), 0, angle], 7); } else if (type === "pressure_switch") writeFixedSignedDefaults(writer, list, [0, 0, 10, 220, 160, 360, 1080], 7); else if (isRotatableSaveType(type)) writeFixedSignedDefaults(writer, list, [defaultAngleMilli(type), 0], 2); else if (isServingFoodSaveType(type)) writeFixedSignedDefaults(writer, list, [row?.[3] || 0, row?.[3] || 0], 2); else writeGenericExtra(writer, list); } function readNormalItemExtra(reader, typeId = 0, row = []) { const type = normalTypeFromId(typeId); if (STRUCTURE_ITEM_TYPES.has(type)) { const defaultMax = type === "plushie" ? 10 : 1000; const rest = readFixedSignedDefaults(reader, [defaultMax, -1, -1, 0, -1, 0], 6); return [row?.[3] || 0, ...rest]; } if (type === "grass") return readFixedSignedDefaults(reader, [100, 100, 0, 0], 4); if (type === "zunchi") return readFixedSignedDefaults(reader, [0, 100, 100], 3); if (type === "signboard") return [readSaveString(reader)]; if (type === "robot_cleaner") return readFixedSignedDefaults(reader, [3, 0], 2); if (type === "circuit_board") return [readSaveString(reader)]; if (type === "duplicator") return readFixedSignedDefaults(reader, [-1], 1); if (type === "ball" || type === "balloon") return readFixedSignedDefaults(reader, [0, 0], 2); if (type === "ant_nest") return readFixedSignedDefaults(reader, [0, 0], 2); if (type === "ant_corpse" || type === "firecracker" || type === "flame_firecracker" || type === "fire") return readFixedSignedDefaults(reader, [0], 1); if (type === "sticky_bomb") return readFixedSignedDefaults(reader, [0, -1, 0], 3); if (type === "nest_box") return readFixedSignedDefaults(reader, [0, 0, 0, 0], 4); if (isPinSaveType(type)) return readFixedSignedDefaults(reader, [0, -1, 0, 0, 0], 5); if (type === "fan") { const angle = defaultAngleMilli(type); return readFixedSignedDefaults(reader, [angle, 0, angle, Math.round(35 * Math.PI / 180 * 1000), Math.round(48 * Math.PI / 180 * 1000), 0, angle], 7); } if (type === "pressure_switch") return readFixedSignedDefaults(reader, [0, 0, 10, 220, 160, 360, 1080], 7); if (isRotatableSaveType(type)) return readFixedSignedDefaults(reader, [defaultAngleMilli(type), 0], 2); if (isServingFoodSaveType(type)) return readFixedSignedDefaults(reader, [row?.[3] || 0, row?.[3] || 0], 2); return readGenericExtra(reader); } function extraKey(value) { try { return JSON.stringify(value == null ? [] : value); } catch (_) { return "[]"; } } function itemCoordKey(row = []) { const x = Math.max(0, Math.min(65535, toSafeUInt(row[1] || 0))); const y = Math.max(0, Math.min(65535, toSafeUInt(row[2] || 0))); return mortonKey(x, y); } function mortonKey(x = 0, y = 0) { let out = 0; for (let i = 0; i < 16; i++) out += (((x >> i) & 1) * (2 ** (2 * i))) + (((y >> i) & 1) * (2 ** (2 * i + 1))); return out; } function writeEncodedItemExtra(writer, row = [], typeId = 0) { const physics = isPhysicsTypeId(typeId); const before = writer.length; if (physics) writePhysicsItemExtra(writer, row[4], typeId, row); else writeNormalItemExtra(writer, row[4] || [], typeId, row); writer.addDetail(physics ? "physicsItemExtraEncodedBytes" : "normalItemExtraEncodedBytes", writer.length - before); } function readEncodedItemExtra(reader, row = [], typeId = 0) { return isPhysicsTypeId(typeId) ? readPhysicsItemExtra(reader, typeId, row) : readNormalItemExtra(reader, typeId, row); } function encodedItemExtraLength(row = [], typeId = 0) { const scratch = new BinaryWriter(); writeEncodedItemExtra(scratch, row, typeId); return scratch.length; } function itemBlocksFromRows(rows = []) { const groups = new Map(); for (const row of rows || []) { const typeId = row?.[0] || 0; if (!groups.has(typeId)) groups.set(typeId, []); groups.get(typeId).push(row); } const blocks = []; for (const [typeId, list] of [...groups.entries()].sort((a, b) => a[0] - b[0])) { list.sort((a, b) => itemCoordKey(a) - itemCoordKey(b) || (a[1] || 0) - (b[1] || 0) || (a[2] || 0) - (b[2] || 0)); blocks.push([typeId, list]); } return blocks; } function standardItemAmount(typeId = 0) { const type = SaveSchema.itemTypeValue(Number(typeId) || 0, ""); const structureDef = global.StructureRegistry?.get?.(type); if (Number.isFinite(Number(structureDef?.maxHp))) return toSafeInt(Number(structureDef.maxHp) * 10); const toolDef = typeof global.itemDefinition === "function" ? global.itemDefinition(type) : null; if (Number.isFinite(Number(toolDef?.amount))) return toSafeInt(Number(toolDef.amount) * 10); return 0; } function signedVarintLength(value = 0) { return encodeVarUint(zigzagEncode(value)).length; } function chooseAmountPlan(typeId = 0, list = []) { const standardAmount = standardItemAmount(typeId); const amounts = list.map(row => toSafeInt(row?.[3] || 0)); const candidates = new Set([standardAmount, ...amounts]); let best = null; for (const candidate of candidates) { let payloadBytes = candidate === standardAmount ? 0 : signedVarintLength(candidate - standardAmount); for (const amount of amounts) if (amount !== candidate) payloadBytes += signedVarintLength(amount - candidate); const plan = { standardAmount, commonAmount: candidate, useCustomAmountBase: candidate !== standardAmount, costBytes: payloadBytes, }; if (!best || plan.costBytes < best.costBytes || (plan.costBytes === best.costBytes && !plan.useCustomAmountBase && best.useCustomAmountBase)) best = plan; } return best || { standardAmount, commonAmount: standardAmount, useCustomAmountBase: false, costBytes: 0 }; } function chooseCommonExtraPlan(typeId = 0, list = [], commonAmount = 0) { if (isPhysicsTypeId(typeId) || !list.length) return { useCommonExtra: false, commonExtra: [], commonKey: "", costBits: Infinity }; const entries = list.map(row => ({ row, key: extraKey(row?.[4]), value: row?.[4] == null ? [] : row[4], encodedBytes: encodedItemExtraLength(row, typeId), })); const noCommonBits = entries.reduce((sum, entry) => sum + entry.encodedBytes * 8, 0); let best = { useCommonExtra: false, commonExtra: [], commonKey: "", costBits: noCommonBits }; const candidates = new Map(); for (const entry of entries) if (!candidates.has(entry.key)) candidates.set(entry.key, entry.value); for (const [key, value] of candidates) { const commonRow = [typeId, 0, 0, commonAmount, value]; let payloadBytes = encodedItemExtraLength(commonRow, typeId); for (const entry of entries) if (entry.key !== key) payloadBytes += entry.encodedBytes; const costBits = payloadBytes * 8 + entries.length; // one extra-different flag per item if (costBits < best.costBits) best = { useCommonExtra: true, commonExtra: value, commonKey: key, costBits }; } return best; } function prepareItemBlocks(rows = []) { return itemBlocksFromRows(rows).map(([typeId, list]) => { const physics = isPhysicsTypeId(typeId); const amountPlan = chooseAmountPlan(typeId, list); const extraPlan = chooseCommonExtraPlan(typeId, list, amountPlan.commonAmount); const rowPlans = list.map(row => ({ row, amountDifferent: toSafeInt(row?.[3] || 0) !== amountPlan.commonAmount, extraDifferent: extraPlan.useCommonExtra && extraKey(row?.[4]) !== extraPlan.commonKey, })); return { typeId, list, physics, category: physics ? "physicsItems" : "normalItems", ...amountPlan, ...extraPlan, rowPlans }; }); } function writeItemBlocks(writer, rows = []) { const blocks = prepareItemBlocks(rows); writer.category("itemIndex", () => writer.u(blocks.length)); for (const block of blocks) { writer.category("itemIndex", () => { writer.u(block.typeId); writer.u(block.list.length); }); } const flags = new CompactBitWriter(); for (const block of blocks) { flags.write(block.useCustomAmountBase ? 1 : 0, 1); flags.write(block.useCommonExtra ? 1 : 0, 1); } for (const block of blocks) { for (const plan of block.rowPlans) { flags.write(plan.amountDifferent ? 1 : 0, 1); if (block.useCommonExtra) flags.write(plan.extraDifferent ? 1 : 0, 1); } } const flagBytes = flags.finish(); writer.category("itemIndex", () => { writer.u(flagBytes.length); writer.raw(flagBytes); }); writer.setDetail("itemFlagPlaneBytes", flagBytes.length); for (const block of blocks) { writer.category(block.category, () => { if (block.useCustomAmountBase) writer.s(block.commonAmount - block.standardAmount); if (block.useCommonExtra) writeEncodedItemExtra(writer, [block.typeId, 0, 0, block.commonAmount, block.commonExtra], block.typeId); const coordState = { x: 0, y: 0 }; for (const plan of block.rowPlans) { const row = plan.row; writer.addDetail(block.physics ? "physicsItemCount" : "normalItemCount", 1); const x = toSafeInt(row[1] || 0); const y = toSafeInt(row[2] || 0); writer.s(x - coordState.x); writer.s(y - coordState.y); coordState.x = x; coordState.y = y; if (plan.amountDifferent) writer.s(toSafeInt(row[3] || 0) - block.commonAmount); if (!block.useCommonExtra || plan.extraDifferent) writeEncodedItemExtra(writer, row, block.typeId); if (writer.stats) { const rawJson = JSON.stringify(row[4] == null ? [] : row[4]); writer.addDetail(block.physics ? "physicsItemExtraRawUtf8Bytes" : "normalItemExtraRawUtf8Bytes", utf8Length(rawJson)); } } }); } } function readItemBlocks(reader) { const blockCount = reader.u(); const blocks = []; for (let b = 0; b < blockCount; b++) { const typeId = reader.u(); const count = reader.u(); blocks.push({ typeId, count, standardAmount: standardItemAmount(typeId), commonAmount: 0, useCustomAmountBase: false, useCommonExtra: false, rowFlags: [], }); } const flagByteLength = reader.u(); const flags = new CompactBitReader(reader.raw(flagByteLength)); for (const block of blocks) { block.useCustomAmountBase = Boolean(flags.read(1)); block.useCommonExtra = Boolean(flags.read(1)); } for (const block of blocks) { block.rowFlags = Array.from({ length: block.count }, () => ({ amountDifferent: Boolean(flags.read(1)), extraDifferent: block.useCommonExtra ? Boolean(flags.read(1)) : false, })); } const items = []; for (const block of blocks) { const { typeId, count, standardAmount, useCustomAmountBase, useCommonExtra } = block; const commonAmount = standardAmount + (useCustomAmountBase ? reader.s() : 0); const commonRow = [typeId, 0, 0, commonAmount, null]; if (useCommonExtra) commonRow[4] = readEncodedItemExtra(reader, commonRow, typeId); const coordState = { x: 0, y: 0 }; for (let i = 0; i < count; i++) { const x = coordState.x + reader.s(); const y = coordState.y + reader.s(); coordState.x = x; coordState.y = y; const rowFlags = block.rowFlags[i]; const amount = commonAmount + (rowFlags.amountDifferent ? reader.s() : 0); const row = [typeId, x, y, amount, null]; row[4] = useCommonExtra && !rowFlags.extraDifferent ? commonRow[4] : readEncodedItemExtra(reader, row, typeId); items.push(row); } } return items; } const ACHIEVEMENT_METADATA_BINARY_VERSION = 1; const ACHIEVEMENT_SPELL_BINARY_VERSION = 5; const ACHIEVEMENT_MASK_BYTES = 8; const ACHIEVEMENT_PROGRESS_BITS = Object.freeze([7, 4, 7, 12, 5, 5, 5, 11, 6, 10, 9, 10, 3, 17]); const ACHIEVEMENT_PROGRESS_MAX = Object.freeze([100, 15, 100, 3333, 30, 20, 20, 2047, 50, 666, 333, 721, 7, 131071]); function finiteNumberOrNull(value) { const n = Number(value); return Number.isFinite(n) ? n : null; } function quantizedTenths(value = 0) { return Math.round((Number(value) || 0) * 10); } function unquantizedTenths(value = 0) { return (Number(value) || 0) / 10; } function varUintLength(value = 0) { return encodeVarUint(value).length; } function writePositiveDeltaList(writer, values = []) { const list = [...new Set((Array.isArray(values) ? values : []).map(value => Math.max(1, toSafeUInt(value))))].sort((a, b) => a - b); writer.u(list.length); let previous = 0; for (const value of list) { writer.u(value - previous); previous = value; } } function readPositiveDeltaList(reader) { const count = reader.u(); const out = []; let previous = 0; for (let i = 0; i < count; i++) { previous += reader.u(); out.push(previous); } return out; } function achievementMaskBytesFromSpell(spell = null) { const bytes = new Uint8Array(ACHIEVEMENT_MASK_BYTES); if (!Array.isArray(spell)) return { bytes, baseMinute: 0, deltas: [], progress: [] }; if (Number(spell[0]) !== ACHIEVEMENT_SPELL_BINARY_VERSION) throw new Error("unsupported achievement spell version"); const low = Math.max(0, Math.min(0xffffffff, Math.floor(Number(spell[1]) || 0))); const high = Math.max(0, Math.min(0xffffffff, Math.floor(Number(spell[2]) || 0))); for (let i = 0; i < 4; i++) bytes[i] = Math.floor(low / (2 ** (i * 8))) & 255; for (let i = 0; i < 4; i++) bytes[i + 4] = Math.floor(high / (2 ** (i * 8))) & 255; const baseMinute = toSafeUInt(spell[3]); const deltas = Array.isArray(spell[4]) ? spell[4].map(toSafeUInt) : []; const progress = Array.isArray(spell[5]) ? spell[5] : []; let unlockedCount = 0; for (const byte of bytes) { let value = byte; while (value) { unlockedCount += value & 1; value >>>= 1; } } if (deltas.length !== unlockedCount) throw new Error("invalid achievement spell timestamps"); return { bytes, baseMinute, deltas, progress }; } function spellFromAchievementMaskBytes(bytes = [], baseMinute = 0, deltas = [], progress = []) { let low = 0; let high = 0; for (let i = 0; i < 4; i++) low += (Number(bytes[i]) || 0) * (2 ** (i * 8)); for (let i = 0; i < 4; i++) high += (Number(bytes[i + 4]) || 0) * (2 ** (i * 8)); return [ACHIEVEMENT_SPELL_BINARY_VERSION, low >>> 0, high >>> 0, toSafeUInt(baseMinute), deltas.map(toSafeUInt), progress]; } function writeAchievementProgress(writer, values = [], bits = ACHIEVEMENT_PROGRESS_BITS, maxima = ACHIEVEMENT_PROGRESS_MAX) { const normalized = maxima.map((max, index) => Math.max(0, Math.min(max, toSafeUInt(values?.[index])))); let sparseMask = 0; let sparseBytes = Math.ceil(maxima.length / 8) + 1; normalized.forEach((value, index) => { if (!value) return; sparseMask |= 1 << index; sparseBytes += varUintLength(value); }); const fixedBytes = Math.ceil(bits.reduce((sum, width) => sum + width, 0) / 8); const useSparse = sparseBytes < fixedBytes; writer.b(useSparse ? 1 : 0); if (useSparse) { writeBitFields(writer, [sparseMask], [maxima.length]); normalized.forEach((value, index) => { if (sparseMask & (1 << index)) writer.u(value); }); } else { writeBitFields(writer, normalized, bits); } } function readAchievementProgress(reader, bits = ACHIEVEMENT_PROGRESS_BITS, maxima = ACHIEVEMENT_PROGRESS_MAX) { const sparse = Boolean(reader.b()); if (!sparse) return readBitFields(reader, bits); const [mask] = readBitFields(reader, [maxima.length]); return maxima.map((max, index) => mask & (1 << index) ? Math.min(max, reader.u()) : 0); } function writeAchievementSpell(writer, spell = null) { const normalized = achievementMaskBytesFromSpell(spell); writer.raw(normalized.bytes); writer.u(normalized.baseMinute); for (const delta of normalized.deltas) writer.u(delta); writeAchievementProgress(writer, normalized.progress); } function readAchievementSpell(reader) { const bytes = reader.raw(ACHIEVEMENT_MASK_BYTES); let unlockedCount = 0; for (const byte of bytes) { let value = byte; while (value) { unlockedCount += value & 1; value >>>= 1; } } const baseMinute = reader.u(); const deltas = []; for (let i = 0; i < unlockedCount; i++) deltas.push(reader.u()); const progress = readAchievementProgress(reader); return spellFromAchievementMaskBytes(bytes, baseMinute, deltas, progress); } function writeAchievementMetadata(writer, metadata = null, tarinaiCount = 0, itemCount = 0) { const value = metadata && typeof metadata === "object" ? metadata : {}; const w = Array.isArray(value.w) ? value.w : []; writer.b(ACHIEVEMENT_METADATA_BINARY_VERSION); writeBitFields(writer, [Array.isArray(value.s) ? 1 : 0], [1]); writePositiveDeltaList(writer, w[0]); writePositiveDeltaList(writer, w[1]); writeBitFields(writer, [ Math.min(33, toSafeUInt(w[2])), Math.min(200, toSafeUInt(w[3])), Math.min(10, toSafeUInt(w[4])), ], [6, 8, 4]); const timeDefaults = [null, -1, null, 0, -1, -1]; const timeValues = [w[5], w[6], w[7], w[8], w[9], w[10]]; let timeMask = 0; const encodedTimes = []; timeValues.forEach((raw, index) => { const finite = finiteNumberOrNull(raw); const defaultValue = timeDefaults[index]; const sameAsDefault = finite == null ? defaultValue == null : finite === defaultValue; if (sameAsDefault) return; timeMask |= 1 << index; encodedTimes.push(quantizedTenths(finite == null ? defaultValue || 0 : finite)); }); writeBitFields(writer, [timeMask], [timeDefaults.length]); encodedTimes.forEach(value => writer.s(value)); const tarRows = Array.isArray(value.t) ? value.t : []; const activeTarRows = []; for (let index = 0; index < Math.min(tarinaiCount, tarRows.length); index++) { const row = Array.isArray(tarRows[index]) ? tarRows[index] : []; const feed = toSafeUInt(row[0]); const treatment = toSafeUInt(row[1]); const fightAt = finiteNumberOrNull(row[2]); const saunaAt = finiteNumberOrNull(row[3]); if (feed || treatment || fightAt != null || saunaAt != null) activeTarRows.push({ index, feed, treatment, fightAt, saunaAt }); } writer.u(activeTarRows.length); let previousTarIndex = -1; for (const row of activeTarRows) { writer.u(row.index - previousTarIndex - 1); previousTarIndex = row.index; } for (const row of activeTarRows) { writer.u(row.feed); writer.u(row.treatment); } const tarFlags = []; for (const row of activeTarRows) tarFlags.push(row.fightAt != null ? 1 : 0, row.saunaAt != null ? 1 : 0); if (tarFlags.length) writeBitFields(writer, tarFlags, tarFlags.map(() => 1)); for (const row of activeTarRows) { if (row.fightAt != null) writer.s(quantizedTenths(row.fightAt)); if (row.saunaAt != null) writer.s(quantizedTenths(row.saunaAt)); } const itemRows = Array.isArray(value.i) ? value.i : []; const activeItemRows = []; for (let index = 0; index < Math.min(itemCount, itemRows.length); index++) { const row = Array.isArray(itemRows[index]) ? itemRows[index] : []; const placed = Boolean(row[0]); const placedAt = finiteNumberOrNull(row[1]); if (placed || placedAt != null) activeItemRows.push({ index, placed, placedAt }); } writer.u(activeItemRows.length); let previousItemIndex = -1; for (const row of activeItemRows) { writer.u(row.index - previousItemIndex - 1); previousItemIndex = row.index; } const itemFlags = []; for (const row of activeItemRows) itemFlags.push(row.placed ? 1 : 0, row.placedAt != null ? 1 : 0); if (itemFlags.length) writeBitFields(writer, itemFlags, itemFlags.map(() => 1)); for (const row of activeItemRows) if (row.placedAt != null) writer.s(quantizedTenths(row.placedAt)); if (Array.isArray(value.s)) writeAchievementSpell(writer, value.s); } function readAchievementMetadata(reader, tarinaiCount = 0, itemCount = 0, schema = BINARY_SCHEMA_VERSION) { const version = reader.b(); if (version !== ACHIEVEMENT_METADATA_BINARY_VERSION) throw new Error("unsupported achievement metadata schema"); const [flags] = readBitFields(reader, [1]); const w = [readPositiveDeltaList(reader), readPositiveDeltaList(reader)]; const [directFeed, robotClean, enemyAntKills] = readBitFields(reader, [6, 8, 4]); w.push(directFeed, robotClean, enemyAntKills); const timeDefaults = [null, -1, null, 0, -1, -1]; const [timeMask] = readBitFields(reader, [timeDefaults.length]); for (let index = 0; index < timeDefaults.length; index++) { w.push(timeMask & (1 << index) ? unquantizedTenths(reader.s()) : timeDefaults[index]); } const t = Array.from({ length: tarinaiCount }, () => [0, 0, null, null]); const tarCount = reader.u(); const tarIndexes = []; let previousTarIndex = -1; for (let i = 0; i < tarCount; i++) { previousTarIndex += reader.u() + 1; if (previousTarIndex >= tarinaiCount) throw new Error("invalid achievement tarinai metadata index"); tarIndexes.push(previousTarIndex); } const tarCounters = tarIndexes.map(() => [reader.u(), reader.u()]); const tarFlagWidths = Array.from({ length: tarCount * 2 }, () => 1); const tarFlags = tarFlagWidths.length ? readBitFields(reader, tarFlagWidths) : []; tarIndexes.forEach((index, rowIndex) => { const fightAt = tarFlags[rowIndex * 2] ? unquantizedTenths(reader.s()) : null; const saunaAt = tarFlags[rowIndex * 2 + 1] ? unquantizedTenths(reader.s()) : null; t[index] = [tarCounters[rowIndex][0], tarCounters[rowIndex][1], fightAt, saunaAt]; }); const i = Array.from({ length: itemCount }, () => [0, null]); const itemRowCount = reader.u(); const itemIndexes = []; let previousItemIndex = -1; for (let row = 0; row < itemRowCount; row++) { previousItemIndex += reader.u() + 1; if (previousItemIndex >= itemCount) throw new Error("invalid achievement item metadata index"); itemIndexes.push(previousItemIndex); } const itemFlagWidths = Array.from({ length: itemRowCount * 2 }, () => 1); const itemFlags = itemFlagWidths.length ? readBitFields(reader, itemFlagWidths) : []; itemIndexes.forEach((index, rowIndex) => { const placed = itemFlags[rowIndex * 2] ? 1 : 0; const placedAt = itemFlags[rowIndex * 2 + 1] ? unquantizedTenths(reader.s()) : null; i[index] = [placed, placedAt]; }); const metadata = { w, t, i }; if (flags & 1) metadata.s = readAchievementSpell(reader, schema); return metadata; } function fieldValue(index = 0) { return FIELD_IDS[Number(index) | 0] || "garden"; } function summaryFromPayload(w = [], tarinaiCount = 0) { const config = typeof CONFIG !== "undefined" ? CONFIG : (global.CONFIG || {}); const dayLength = Math.max(1, Number(config.dayLength || 120)); const totalTime = (Number(w[3]) || 0) / 10; const day = Math.max(1, Math.floor(totalTime / dayLength) + 1); return [day, tarinaiCount, fieldValue(w[0])]; } function encodeBinarySnapshot(snapshot, stats = null) { if (!snapshot || snapshot.v !== Snapshot.version || snapshot.a !== "tj1") throw new Error("snapshot version mismatch"); const writer = new BinaryWriter(stats); activeStringWriteMap = null; try { const stringTable = collectStringCounts(snapshot); const hasStringTable = stringTable.length > 0; writer.category("format", () => writer.b(BINARY_SCHEMA_VERSION | (hasStringTable ? 0x80 : 0))); if (hasStringTable) writer.category("stringTable", () => writeStringTable(writer, stringTable)); else writer.setDetail("stringTableEntries", 0); const worldRow = Array.isArray(snapshot.w) ? snapshot.w.slice() : []; worldRow[8] = canonicalWorldSeed(worldRow[8] || ""); writer.category("world", () => writeWorld(writer, worldRow)); const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : []; writer.category("format", () => writer.u(tarinaiRows.length)); const tarCoordState = { x: 0, y: 0, age: 0, generation: 1 }; const worldSeed = worldRow[8]; const tarinaiMetas = tarinaiRows.map((row, index) => analyzeTarinaiRow(row || [], worldSeed, index)); writer.category("tarinaiBasic", () => writeTarinaiBitPlane(writer, tarinaiMetas)); tarinaiRows.forEach((row, index) => writer.category("tarinaiBasic", () => writeTarinaiPayload(writer, row || [], tarinaiMetas[index], tarCoordState, worldSeed, index))); const itemRows = Array.isArray(snapshot.i) ? snapshot.i : []; writeItemBlocks(writer, itemRows); writer.category("achievementMetadata", () => writeAchievementMetadata(writer, snapshot.g || null, tarinaiRows.length, itemRows.length)); return writer.out(); } finally { activeStringWriteMap = null; } } function decodeBinarySnapshot(bytes) { const reader = new BinaryReader(bytes); activeStringReadList = null; try { const formatHeader = reader.b(); const schema = formatHeader & 0x7f; const hasStringTable = Boolean(formatHeader & 0x80); if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema"); readStringTable(reader, hasStringTable); const w = readWorld(reader); const tCount = reader.u(); const t = []; const tarCoordState = { x: 0, y: 0, age: 0, generation: 1 }; const tarinaiHeaders = readTarinaiBitPlane(reader, tCount); for (let i = 0; i < tCount; i++) t.push(readTarinaiPayload(reader, tarinaiHeaders[i], tarCoordState, String(w?.[8] || ""), i)); const items = readItemBlocks(reader); let g = null; try { g = readAchievementMetadata(reader, tCount, items.length, schema); } catch (_) { throw new Error("invalid achievement metadata in binary save"); } if (reader.pos !== reader.bytes.length) throw new Error("trailing bytes in binary save"); return { v: Snapshot.version, a: "tj1", m: summaryFromPayload(w, t.length), w, t, i: items, g }; } finally { activeStringReadList = null; } } function finalizeEncodeStats(stats, rawBytes, frameInfo, hash, elapsedMs) { if (!stats) return null; for (const key of SAVE_STATS_SECTION_ORDER) stats.sections[key] = Number(stats.sections[key]) || 0; const frame = frameInfo?.frame || new Uint8Array(); stats.rawBinaryBytes = rawBytes.length; stats.binaryBytes = frame.length; stats.compressed = Boolean(frameInfo?.compressed); stats.compressedBytes = Number(frameInfo?.compressedBytes) || rawBytes.length; stats.compressionSavedBytes = Math.max(0, rawBytes.length - stats.compressedBytes); stats.hashChars = hash.length; stats.payloadChars = Math.max(0, hash.length - EXPORT_PREFIX.length); stats.encodeMs = Number(Math.max(0, elapsedMs).toFixed(3)); stats.accountedBytes = Object.values(stats.sections).reduce((sum, value) => sum + (Number(value) || 0), 0); stats.unaccountedBytes = stats.rawBinaryBytes - stats.accountedBytes; return stats; } function cloneStats(stats) { if (!stats) return null; return { ...stats, sections: { ...(stats.sections || {}) }, details: { ...(stats.details || {}) }, }; } function getLastEncodeStats() { return cloneStats(lastEncodeStats); } function getLastDecodeStats() { return cloneStats(lastDecodeStats); } async function encodeSnapshot(snapshot, options = {}) { const collectStats = options?.collectStats === true || debugSaveStatsEnabled(); const stats = collectStats ? createSaveStats(snapshot) : null; const startedAt = nowMs(); const rawBytes = encodeBinarySnapshot(snapshot, stats); const frameInfo = await makeSaveFrame(rawBytes); const hash = `${EXPORT_PREFIX}${base32768Encode(frameInfo.frame)}`; if (stats) { lastEncodeStats = finalizeEncodeStats(stats, rawBytes, frameInfo, hash, nowMs() - startedAt); try { global.dispatchEvent?.(new CustomEvent("tarinai-save-stats", { detail: cloneStats(lastEncodeStats) })); } catch (_) {} global.console?.info?.("[tarinai save stats]", cloneStats(lastEncodeStats)); } return hash; } async function decodeSnapshot(text) { const rawText = String(text || "").trim(); const collectStats = debugSaveStatsEnabled(); const startedAt = nowMs(); if (!rawText.startsWith(EXPORT_PREFIX)) throw new Error("unsupported Japanese save text"); const encoded = base32768Decode(rawText.slice(EXPORT_PREFIX.length)); const frame = await openSaveFrame(encoded); const snapshot = decodeBinarySnapshot(frame.raw); if (collectStats) { lastDecodeStats = { createdAt: Date.now(), binaryBytes: frame.frameBytes, rawBinaryBytes: frame.raw.length, compressed: frame.compressed, hashChars: rawText.length, decodeMs: Number(Math.max(0, nowMs() - startedAt).toFixed(3)), details: { tarinaiCount: Array.isArray(snapshot?.t) ? snapshot.t.length : 0, itemCount: Array.isArray(snapshot?.i) ? snapshot.i.length : 0, }, }; try { global.dispatchEvent?.(new CustomEvent("tarinai-load-stats", { detail: cloneStats(lastDecodeStats) })); } catch (_) {} } return snapshot; } global.TarinaiSaveCodec = { encodeSnapshot, decodeSnapshot, getLastEncodeStats, getLastDecodeStats, }; })(typeof window !== "undefined" ? window : globalThis);