"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 = "たり"; const RAW_CODEC = "生"; const DEFLATE_CODEC = "縮"; const { BINARY_SCHEMA_VERSION, MIN_BINARY_SCHEMA_VERSION, FIELD_IDS, GROUND_IDS, ITEM_TYPE_IDS, STRUCTURE_TYPE_IDS, SERVING_FOOD_TYPE_IDS, PIN_TYPE_IDS, ROTATABLE_ITEM_TYPE_IDS, } = SaveSchema; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); const SAVE_TEXT_BITS = 11; const SAVE_TEXT_BASE = 1 << SAVE_TEXT_BITS; const SAVE_TEXT_ALPHABET = (() => { const chars = []; const addRange = (from, to) => { for (let code = from; code <= to && chars.length < SAVE_TEXT_BASE; code++) chars.push(String.fromCharCode(code)); }; // 正規化で分解されにくい仮名とCJK統合漢字だけを使う。 addRange(0x3041, 0x3096); // ひらがな addRange(0x30A1, 0x30FA); // カタカナ addRange(0x4E00, 0x9FFF); // 常用漢字を含むCJK統合漢字。必要数に達したら停止。 return chars.join(""); })(); const SAVE_TEXT_REVERSE = (() => { const map = new Map(); let index = 0; for (const ch of SAVE_TEXT_ALPHABET) map.set(ch, index++); return map; })(); if ([...SAVE_TEXT_ALPHABET].length !== SAVE_TEXT_BASE) throw new Error("save alphabet must contain 2048 Japanese chars"); function writeVaruintToArray(out, value = 0) { let n = toSafeUInt(value); while (n >= 128) { out.push((n % 128) | 128); n = Math.floor(n / 128); } out.push(n & 127); } function readVaruintFromArray(bytes, start = 0) { let n = 0; let mul = 1; for (let i = 0; i < 10; i++) { const byte = bytes[start + i]; if (byte == null) throw new Error("truncated Japanese save length"); n += (byte & 127) * mul; if (!(byte & 128)) return { value: n, next: start + i + 1 }; mul *= 128; } throw new Error("invalid Japanese save length"); } function jp2048Encode(bytes) { const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []); const packet = []; writeVaruintToArray(packet, source.length); for (const byte of source) packet.push(byte & 255); let b = 0; let n = 0; let out = ""; for (const byte of packet) { b |= (byte & 255) << n; n += 8; while (n >= SAVE_TEXT_BITS) { out += SAVE_TEXT_ALPHABET[b & (SAVE_TEXT_BASE - 1)]; b >>= SAVE_TEXT_BITS; n -= SAVE_TEXT_BITS; } } if (n > 0) out += SAVE_TEXT_ALPHABET[b & (SAVE_TEXT_BASE - 1)]; return out; } function jp2048Decode(text) { let b = 0; let n = 0; const packet = []; for (const ch of String(text || "").trim()) { const value = SAVE_TEXT_REVERSE.get(ch); if (value == null) continue; b |= value << n; n += SAVE_TEXT_BITS; while (n >= 8) { packet.push(b & 255); b >>= 8; n -= 8; } } const { value: length, next } = readVaruintFromArray(packet, 0); if (packet.length < next + length) throw new Error("truncated Japanese save payload"); return new Uint8Array(packet.slice(next, next + length)); } 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() { this.bytes = []; } u(value = 0) { let n = toSafeUInt(value); while (n >= 128) { this.bytes.push((n % 128) | 128); n = Math.floor(n / 128); } this.bytes.push(n & 127); } s(value = 0) { this.u(zigzagEncode(value)); } b(value = 0) { this.bytes.push((Number(value) || 0) & 255); } str(value = "") { const bytes = textEncoder.encode(String(value || "")); this.u(bytes.length); for (const byte of bytes) this.bytes.push(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++]; } 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); } } 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 makeProfile(snapshot = {}) { const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : []; const itemRows = Array.isArray(snapshot.i) ? snapshot.i : []; return { format: "TJ1", schema: BINARY_SCHEMA_VERSION, alphabetSize: SAVE_TEXT_BASE, counts: { tarinai: tarinaiRows.length, items: itemRows.length }, sections: {}, itemTypes: {}, compression: {}, }; } function addProfileBytes(profile, key, bytes = 0) { if (!profile || !key) return; const n = Math.max(0, Number(bytes) || 0); profile.sections[key] = (profile.sections[key] || 0) + n; } function trackBytes(writer, profile, key, fn) { if (!profile || !writer) return fn(); const start = writer.bytes.length; const result = fn(); addProfileBytes(profile, key, writer.bytes.length - start); return result; } function addItemTypeProfile(profile, typeId, bytes = 0) { if (!profile) return; const type = ITEM_TYPE_IDS[typeId] || `type_${typeId}`; const entry = profile.itemTypes[type] || { count: 0, bytes: 0 }; entry.count += 1; entry.bytes += Math.max(0, Number(bytes) || 0); profile.itemTypes[type] = entry; } function finalizeProfile(profile, rawBytes, packedBytes, text, codec = "R") { if (!profile) return null; const raw = rawBytes?.length || 0; const packed = packedBytes?.length || 0; const payloadChars = Math.max(0, String(text || "").length - EXPORT_PREFIX.length - 1); profile.sections.totalRaw = raw; profile.compression = { codec, rawBytes: raw, packedBytes: packed, packedDelta: raw - packed, ratio: raw > 0 ? packed / raw : 0, prefixChars: EXPORT_PREFIX.length + 1, payloadChars, totalChars: String(text || "").length, alphabetSize: SAVE_TEXT_BASE, containsBacktick: false, japaneseText: true, bitsPerChar: SAVE_TEXT_BITS, asciiOnly: false, }; return profile; } async function compressBytes(bytes) { if (typeof CompressionStream === "function") { const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw")); return new Uint8Array(await new Response(stream).arrayBuffer()); } return bytes; } async function decompressBytes(bytes, codec = "R") { if (codec === DEFLATE_CODEC) { if (typeof DecompressionStream !== "function") throw new Error("deflate decoder is not available in this browser"); const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw")); return new Uint8Array(await new Response(stream).arrayBuffer()); } if (codec !== RAW_CODEC) throw new Error("unsupported Japanese save codec"); return bytes; } function writeFixed(writer, arr = [], count = 0, signed = true) { for (let i = 0; i < count; i++) signed ? writer.s(arr[i] || 0) : writer.u(arr[i] || 0); } function readFixed(reader, count = 0, signed = true) { const out = []; for (let i = 0; i < count; i++) out.push(signed ? reader.s() : reader.u()); return out; } 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 generatedNameCode(value = "") { const name = String(value || ""); if (!name.startsWith("は")) return -1; const stop = name.endsWith("っ"); const core = name.slice(1, stop ? -1 : undefined); if (core.length > 6) return -1; let bits = 0; for (let i = 0; i < core.length; i++) { const ch = core.charAt(i); if (ch !== "う" && ch !== "ぅ") return -1; if (ch === "ぅ") bits |= (1 << i); } return core.length | (stop ? 8 : 0) | (bits << 4); } function nameFromGeneratedCode(code = 0) { const middleLen = code & 7; const stop = !!(code & 8); const bits = code >> 4; let name = "は"; for (let i = 0; i < middleLen; i++) name += (bits & (1 << i)) ? "ぅ" : "う"; if (stop) name += "っ"; return name; } function writeName(writer, value = "") { const code = generatedNameCode(value); if (code >= 0) { writer.u(code + 1); return; } writer.u(0); writer.str(value || ""); } function readName(reader) { const code = reader.u(); if (code > 0) return nameFromGeneratedCode(code - 1); return reader.str(); } function valuesDiffer(a, b) { return (Number(a) || 0) !== (Number(b) || 0); } function writeFixedDiff(writer, base = [], current = [], count = 0, signed = false) { let mask = 0; for (let i = 0; i < count; i++) if (valuesDiffer(current?.[i], base?.[i])) mask |= (1 << i); writer.u(mask); for (let i = 0; i < count; i++) if (mask & (1 << i)) signed ? writer.s(current[i] || 0) : writer.u(current[i] || 0); } function readFixedDiff(reader, base = [], count = 0, signed = false) { const out = Array.from({ length: count }, (_, i) => base?.[i] || 0); const mask = reader.u(); for (let i = 0; i < count; i++) if (mask & (1 << i)) out[i] = signed ? reader.s() : reader.u(); return out; } 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 commonItemAmountNeeded(typeId) { if (STRUCTURE_TYPE_IDS.has(typeId)) return false; if (SERVING_FOOD_TYPE_IDS.has(typeId)) return false; return true; } function writeRelationships(writer, rows = []) { const list = Array.isArray(rows) ? rows : []; writer.u(list.length); for (const row of list) { writer.u(row?.[0] || 0); writer.s(row?.[1] || 0); writer.s(row?.[2] || 0); writer.u(row?.[3] || 0); } } function readRelationships(reader) { const count = reader.u(); const out = []; for (let i = 0; i < count; i++) out.push([reader.u(), reader.s(), reader.s(), reader.u()]); return out; } 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 writer.s(w[5] || 0); // lastBirthAt10 can be negative writer.u(w[6] || 1); // generation writer.u(w[7] || 0); // dead count writer.str(w[8] || ""); // worldSeed writer.u(w[9] || 0); // birthSerial } function readWorld(reader) { const schema = reader.schemaVersion || BINARY_SCHEMA_VERSION; const groundBits = schema >= 6 ? 3 : 2; const [field, rawGround, mood, weather] = readBitFields(reader, [2, groundBits, 4, 3]); // Schema 5 and earlier used [soil, concrete, blanket, foot_massage] in two bits. // Schema 6 inserts the new high-growth dirt at index 1, so old indices are remapped here. const ground = schema >= 6 ? rawGround : ([0, 2, 3, 4][Number(rawGround) | 0] ?? 0); return [field, ground, mood, reader.u(), weather, reader.s(), reader.u(), reader.u(), reader.str(), reader.u()]; } function seedPersonalityArray(seed = "", flags = 0) { // ずんちどれいは誕生時性格も全軸 -1 なので、差分基準も0固定にする。 if ((Number(flags) || 0) & (1 << 5)) return [0, 0, 0, 0]; return global.TarinaiSeedFactory?.personalityArray?.(seed) || [100, 100, 100, 100]; } function writeTarinai(writer, row = [], profile = null, coordState = null) { const pinIdx = row[15] ?? -1; const nestIdx = row[16] ?? -1; const stateId = row[17]?.[0] || 0; const stateTarget = row[17]?.[1] ?? -1; const birthSeed = row[1] || ""; trackBytes(writer, profile, "tarinai.flags", () => writeBitFields(writer, [row[0] || 0], [10])); trackBytes(writer, profile, "tarinai.seed", () => writer.str(birthSeed)); trackBytes(writer, profile, "tarinai.core", () => { const x = toSafeInt(row[2] || 0); const y = toSafeInt(row[3] || 0); writer.s(x - (coordState?.x || 0)); writer.s(y - (coordState?.y || 0)); if (coordState) { coordState.x = x; coordState.y = y; } writer.u(row[4] || 0); writer.u(row[5] || 1); writeBitFields(writer, [row[6] || 0, row[7] || 0, row[8] || 0], [8, 8, 8]); }); trackBytes(writer, profile, "tarinai.needs", () => writeFixedPacked(writer, row[9], 6, 6, 2)); trackBytes(writer, profile, "tarinai.traits", () => { writeFixedDiffPacked(writer, seedPersonalityArray(birthSeed, row[0] || 0), row[10], 4, 8); }); trackBytes(writer, profile, "tarinai.family", () => { writeIndexList(writer, row[11]); }); trackBytes(writer, profile, "tarinai.relationships", () => writeRelationships(writer, row[12])); trackBytes(writer, profile, "tarinai.timers", () => writeTimers(writer, row[13])); trackBytes(writer, profile, "tarinai.modes", () => writeModes(writer, row[14])); trackBytes(writer, profile, "tarinai.tail", () => { let tailMask = 0; if (pinIdx >= 0) tailMask |= 1; if (nestIdx >= 0) tailMask |= 2; if (stateId || stateTarget >= 0) tailMask |= 4; writeBitFields(writer, [tailMask], [3]); if (tailMask & 1) writer.u(pinIdx); if (tailMask & 2) writer.u(nestIdx); if (tailMask & 4) { writeBitFields(writer, [stateId], [3]); writer.s(stateTarget); } }); } function readTarinai(reader, coordState = null) { const flags = readBitFields(reader, [10])[0]; const birthSeed = reader.str(); const x = (coordState?.x || 0) + reader.s(); const y = (coordState?.y || 0) + reader.s(); if (coordState) { coordState.x = x; coordState.y = y; } const age = reader.u(); const generation = reader.u(); const [hunger, energy, sleepPressure] = readBitFields(reader, [8, 8, 8]); const needs = readFixedPacked(reader, 6, 6, 2); const current = readFixedDiffPacked(reader, seedPersonalityArray(birthSeed, flags), 4, 8); const parents = readIndexList(reader); const relationships = readRelationships(reader); const timers = readTimers(reader); const modes = readModes(reader); const [tailMask] = readBitFields(reader, [3]); const pinIdx = (tailMask & 1) ? reader.u() : -1; const nestIdx = (tailMask & 2) ? reader.u() : -1; const stateInfo = (tailMask & 4) ? [readBitFields(reader, [3])[0], reader.s()] : [0, -1]; return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo]; } function writeItemExtra(writer, typeId, extra = []) { if (STRUCTURE_TYPE_IDS.has(typeId)) { writer.s(extra[0] || 0); writer.s(extra[1] || 0); writer.s(extra[2] ?? -1); writer.s(extra[3] ?? -1); writer.u(extra[4] || 0); writer.s(extra[5] ?? -1); } else if (typeId === 0) { // grass writer.s(extra[0] || 0); writer.s(extra[1] || 0); writer.s(extra[2] || 0); writer.u(extra[3] || 0); } else if (typeId === 1) { // zunchi writer.u(extra[0] || 0); writer.s(extra[1] || 0); writer.s(extra[2] || 0); } else if (typeId === 22) { // signboard writer.str(extra[0] || ""); } else if (typeId === 6) { // duplicator writer.s(extra[0] ?? -1); } else if (typeId === 39) { // rotator writer.str(JSON.stringify(extra || [])); } else if (typeId === 21) { // ball writer.s(extra[0] || 0); writer.s(extra[1] || 0); } else if (typeId === 23) { // ant_nest writer.s(extra[0] || 0); writer.s(extra[1] || 0); } else if (typeId === 24) { // ant_corpse writer.s(extra[0] || 0); } else if (typeId === 25) { // firecracker writer.s(extra[0] || 0); } else if (PIN_TYPE_IDS.has(typeId)) { writer.u(extra[0] || 0); writer.s(extra[1] ?? -1); writer.s(extra[2] || 0); writer.s(extra[3] || 0); writer.s(extra[4] || 0); } else if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) { writer.s(extra[0] || 0); writer.u(extra[1] || 0); } else if (SERVING_FOOD_TYPE_IDS.has(typeId)) { writer.s(extra[0] || 0); writer.s(extra[1] || 0); } } function readItemExtra(reader, typeId) { if (STRUCTURE_TYPE_IDS.has(typeId)) return [reader.s(), reader.s(), reader.s(), reader.s(), reader.u(), reader.s()]; if (typeId === 0) return [reader.s(), reader.s(), reader.s(), reader.u()]; if (typeId === 1) return [reader.u(), reader.s(), reader.s()]; if (typeId === 22) return [reader.str()]; if (typeId === 6) return [reader.s()]; if (typeId === 39) return [reader.str()]; if (typeId === 21) return [reader.s(), reader.s()]; if (typeId === 23) return [reader.s(), reader.s()]; if (typeId === 24) return [reader.s()]; if (typeId === 25) return [reader.s()]; if (PIN_TYPE_IDS.has(typeId)) return [reader.u(), reader.s(), reader.s(), reader.s(), reader.s()]; if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) return (reader.schemaVersion || BINARY_SCHEMA_VERSION) >= 5 ? [reader.s(), reader.u()] : []; if (SERVING_FOOD_TYPE_IDS.has(typeId)) return [reader.s(), reader.s()]; 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 writeItemPayload(writer, row = [], typeId = 0, coordState = null, profile = null) { const start = writer.bytes.length; trackBytes(writer, profile, "items.core", () => { const x = toSafeInt(row[1] || 0); const y = toSafeInt(row[2] || 0); writer.s(x - (coordState?.x || 0)); writer.s(y - (coordState?.y || 0)); if (coordState) { coordState.x = x; coordState.y = y; } if (commonItemAmountNeeded(typeId)) writer.s(row[3] || 0); }); trackBytes(writer, profile, "items.extra", () => writeItemExtra(writer, typeId, row[4] || [])); addItemTypeProfile(profile, typeId, writer.bytes.length - start); } function readItemPayload(reader, typeId = 0, coordState = null) { const x = (coordState?.x || 0) + reader.s(); const y = (coordState?.y || 0) + reader.s(); if (coordState) { coordState.x = x; coordState.y = y; } const amount = commonItemAmountNeeded(typeId) ? reader.s() : 0; const extra = readItemExtra(reader, typeId); let restoredAmount = amount; if (STRUCTURE_TYPE_IDS.has(typeId)) restoredAmount = extra[0] || 0; else if (SERVING_FOOD_TYPE_IDS.has(typeId)) restoredAmount = extra[0] || 0; return [typeId, x, y, restoredAmount, extra]; } 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 writeItemBlocks(writer, rows = [], profile = null) { const blocks = itemBlocksFromRows(rows); writer.u(blocks.length); for (const [typeId, list] of blocks) { writer.u(typeId); writer.u(list.length); const coordState = { x: 0, y: 0 }; for (const row of list) writeItemPayload(writer, row, typeId, coordState, profile); } } function readItemBlocks(reader) { const blockCount = reader.u(); const items = []; for (let b = 0; b < blockCount; b++) { const typeId = reader.u(); const count = reader.u(); const coordState = { x: 0, y: 0 }; for (let i = 0; i < count; i++) items.push(readItemPayload(reader, typeId, coordState)); } return items; } function fieldValue(index = 0) { return FIELD_IDS[Number(index) | 0] || "garden"; } function groundValue(index = 0) { return GROUND_IDS[Number(index) | 0] || "soil"; } function summaryFromPayload(w = [], tarinaiCount = 0, itemCount = 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); const time10 = Math.round((totalTime - (day - 1) * dayLength) * 10); return [day, tarinaiCount, fieldValue(w[0]), time10, itemCount, groundValue(w[1])]; } function encodeBinarySnapshot(snapshot, options = {}) { if (!snapshot || snapshot.v !== Snapshot.version || snapshot.a !== "tj1") throw new Error("snapshot version mismatch"); const profile = options?.profile ? makeProfile(snapshot) : null; const writer = new BinaryWriter(); trackBytes(writer, profile, "header", () => { writer.b(BINARY_SCHEMA_VERSION); writer.u(Snapshot.version); }); trackBytes(writer, profile, "world", () => writeWorld(writer, snapshot.w || [])); const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : []; trackBytes(writer, profile, "tarinai.count", () => writer.u(tarinaiRows.length)); const tarinaiStart = writer.bytes.length; const tarCoordState = { x: 0, y: 0 }; for (const row of tarinaiRows) writeTarinai(writer, row || [], profile, tarCoordState); addProfileBytes(profile, "tarinai.total", writer.bytes.length - tarinaiStart); const itemRows = Array.isArray(snapshot.i) ? snapshot.i : []; const itemStart = writer.bytes.length; trackBytes(writer, profile, "items.blocks", () => writeItemBlocks(writer, itemRows, profile)); addProfileBytes(profile, "items.total", writer.bytes.length - itemStart); const bytes = writer.out(); if (profile) return { bytes, profile }; return bytes; } function decodeBinarySnapshot(bytes) { const reader = new BinaryReader(bytes); const schema = reader.b(); if (schema < MIN_BINARY_SCHEMA_VERSION || schema > BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema"); reader.schemaVersion = schema; const snapshotVersion = reader.u(); if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version"); const w = readWorld(reader); const tCount = reader.u(); const t = []; const tarCoordState = { x: 0, y: 0 }; for (let i = 0; i < tCount; i++) t.push(readTarinai(reader, tarCoordState)); const items = readItemBlocks(reader); const itemCount = items.length; if (reader.pos !== reader.bytes.length) throw new Error("trailing bytes in binary save"); return { v: Snapshot.version, a: "tj1", c: Date.now(), m: summaryFromPayload(w, t.length, items.length), w, t, i: items }; } async function packBytesForSave(bytes) { let codec = RAW_CODEC; let packed = bytes; try { const compressed = await compressBytes(bytes); if (compressed.length < bytes.length) { codec = DEFLATE_CODEC; packed = compressed; } } catch (error) { console.warn("save compression fallback", error); } return { codec, packed }; } async function encodeSnapshotWithProfile(snapshot) { const encoded = encodeBinarySnapshot(snapshot, { profile: true }); const bytes = encoded.bytes || encoded; const profile = encoded.profile || null; const { codec, packed } = await packBytesForSave(bytes); const text = `${EXPORT_PREFIX}${codec}${jp2048Encode(packed)}`; finalizeProfile(profile, bytes, packed, text, codec); return { text, profile }; } async function encodeSnapshot(snapshot) { return (await encodeSnapshotWithProfile(snapshot)).text; } async function decodeSnapshot(text) { const raw = String(text || "").trim(); if (!raw.startsWith(EXPORT_PREFIX)) throw new Error("unsupported Japanese save text"); const codec = raw.charAt(EXPORT_PREFIX.length) || RAW_CODEC; const payload = raw.slice(EXPORT_PREFIX.length + 1); const packed = jp2048Decode(payload); const bytes = await decompressBytes(packed, codec); return decodeBinarySnapshot(bytes); } global.TarinaiSaveCodec = { EXPORT_PREFIX, RAW_CODEC, DEFLATE_CODEC, SAVE_TEXT_ALPHABET, SAVE_TEXT_BASE, SAVE_TEXT_BITS, jp2048Encode, jp2048Decode, encodeSnapshot, encodeSnapshotWithProfile, decodeSnapshot, encodeBinarySnapshot, decodeBinarySnapshot, BinaryWriter, BinaryReader, }; })(typeof window !== "undefined" ? window : globalThis);