tarinai/js/save_codec.js
2026-07-03 00:45:22 +09:00

879 lines
35 KiB
JavaScript

"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 RAW_CODEC = "raw";
const DEFLATE_CODEC = "deflate";
const DICT_CODEC = "dict";
const DICT_DEFLATE_CODEC = "dict-deflate";
const SAVE_HEADER_CHARS = "\u3089\u308A\u308B\u308C\u308D\u308F\u3092\u3093\u3083\u3085\u3087\u3063";
const SAVE_HEADER_CODECS = Object.freeze([RAW_CODEC, DEFLATE_CODEC, DICT_CODEC, DICT_DEFLATE_CODEC]);
const {
BINARY_SCHEMA_VERSION,
FIELD_IDS,
STRUCTURE_TYPE_IDS,
SERVING_FOOD_TYPE_IDS,
PIN_TYPE_IDS,
ROTATABLE_ITEM_TYPE_IDS,
JSON_EXTRA_ITEM_TYPE_IDS,
ITEM_TYPE_IDS,
} = SaveSchema;
const FIRE_TYPE_ID = ITEM_TYPE_IDS.indexOf("fire");
const FIRECRACKER_TYPE_ID = ITEM_TYPE_IDS.indexOf("firecracker");
const FLAME_FIRECRACKER_TYPE_ID = ITEM_TYPE_IDS.indexOf("flame_firecracker");
const BALLOON_TYPE_ID = ITEM_TYPE_IDS.indexOf("balloon");
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const SAVE_TEXT_BITS = 12;
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));
};
// \u6b63\u898f\u5316\u3067\u5206\u89e3\u3055\u308c\u306b\u304f\u3044\u4eee\u540d\u3068CJK\u7d71\u5408\u6f22\u5b57\u3060\u3051\u3092\u4f7f\u3046\u3002
addRange(0x3041, 0x3096); // \u3072\u3089\u304c\u306a
addRange(0x30A1, 0x30FA); // \u30ab\u30bf\u30ab\u30ca
addRange(0x4E00, 0x9FFF); // \u5e38\u7528\u6f22\u5b57\u3092\u542b\u3080CJK\u7d71\u5408\u6f22\u5b57\u3002\u5fc5\u8981\u6570\u306b\u9054\u3057\u305f\u3089\u505c\u6b62\u3002
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 4096 Japanese chars");
function jp4096Encode(bytes) {
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
let b = 0;
let n = 0;
let out = "";
for (const byte of source) {
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 jp4096Decode(text, remainder = 0) {
let b = 0;
let n = 0;
const out = [];
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) {
out.push(b & 255);
b >>= 8;
n -= 8;
}
}
if ((Number(remainder) || 0) === 2 && out.length) out.pop();
return new Uint8Array(out);
}
function saveHeader(codec = RAW_CODEC, byteLength = 0) {
const codecIndex = Math.max(0, SAVE_HEADER_CODECS.indexOf(codec));
const remainder = ((Number(byteLength) || 0) % 3 + 3) % 3;
return SAVE_HEADER_CHARS.charAt(codecIndex * 3 + remainder);
}
function parseSaveHeader(ch = "") {
const index = SAVE_HEADER_CHARS.indexOf(String(ch || "").charAt(0));
if (index < 0) throw new Error("unsupported Japanese save header");
return {
codec: SAVE_HEADER_CODECS[Math.floor(index / 3)] || RAW_CODEC,
remainder: index % 3,
};
}
const BYTE_DICTIONARY = Object.freeze([
[0, 0, 0],
[0, 0, 0, 0],
[100, 100, 100, 100],
[0, 0, 100],
[0, 0, 0, 100],
[1, 0, 0],
[0, 1, 0],
].map(row => Uint8Array.from(row)));
const DICT_ESCAPE = 255;
function dictionaryTransform(bytes) {
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
const out = [];
for (let i = 0; i < source.length;) {
let match = -1;
for (let d = BYTE_DICTIONARY.length - 1; d >= 0; d--) {
const entry = BYTE_DICTIONARY[d];
if (i + entry.length > source.length) continue;
let ok = true;
for (let j = 0; j < entry.length; j++) {
if (source[i + j] !== entry[j]) { ok = false; break; }
}
if (ok) { match = d; break; }
}
if (match >= 0) {
out.push(DICT_ESCAPE, match + 1);
i += BYTE_DICTIONARY[match].length;
} else {
const byte = source[i++];
if (byte === DICT_ESCAPE) out.push(DICT_ESCAPE, 0);
else out.push(byte);
}
}
return Uint8Array.from(out);
}
function dictionaryRestore(bytes) {
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
const out = [];
for (let i = 0; i < source.length; i++) {
const byte = source[i];
if (byte !== DICT_ESCAPE) { out.push(byte); continue; }
const code = source[++i];
if (code == null) throw new Error("truncated Japanese save dictionary payload");
if (code === 0) out.push(DICT_ESCAPE);
else {
const entry = BYTE_DICTIONARY[code - 1];
if (!entry) throw new Error("invalid Japanese save dictionary token");
for (const v of entry) out.push(v);
}
}
return Uint8Array.from(out);
}
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);
}
}
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 s = String(value || "");
if (s.length < 2 || utf8Length(s) < 4) return;
counts.set(s, (counts.get(s) || 0) + 1);
};
add(snapshot?.w?.[8]);
for (const row of snapshot?.t || []) add(row?.[1]);
for (const row of snapshot?.i || []) {
const typeId = row?.[0];
const extra = row?.[4];
if (typeId === 22) add(extra?.[0]);
else if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) add(JSON.stringify(extra || []));
}
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, snapshot) {
const list = collectStringCounts(snapshot);
writer.u(list.length);
activeStringWriteMap = list.length ? new Map(list.map((value, i) => [value, i])) : null;
for (const value of list) writer.str(value);
}
function readStringTable(reader) {
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));
}
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) {
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 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 commonItemAmountNeeded(typeId) {
if (STRUCTURE_TYPE_IDS.has(typeId)) return false;
if (SERVING_FOOD_TYPE_IDS.has(typeId)) return false;
return true;
}
function defaultStructureHp10(typeId) {
if (ITEM_TYPE_IDS[typeId] === "grass_bed") return 1000;
if (ITEM_TYPE_IDS[typeId] === "plushie") return 10;
return 10;
}
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 : [];
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
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;
writeBitFields(writer, [mask], [4]);
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
writeSaveString(writer, w[8] || ""); // worldSeed
if (mask & 8) writer.u(w[9] || 0); // birthSerial
}
function readWorld(reader) {
const [field, ground, mood, weather] = readBitFields(reader, [2, 3, 4, 3]);
const tick = reader.u();
const [mask] = readBitFields(reader, [4]);
const lastBirthAt = (mask & 1) ? reader.s() : -9990;
const generation = (mask & 2) ? reader.u() : 1;
const deadCount = (mask & 4) ? reader.u() : 0;
const seed = readSaveString(reader);
const birthSerial = (mask & 8) ? reader.u() : 0;
return [field, ground, mood, tick, weather, lastBirthAt, generation, deadCount, seed, birthSerial];
}
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;
}
function writeTarinai(writer, row = [], 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 genetics = Array.isArray(row[18]) ? row[18] : [];
const fightStats = Array.isArray(row[19]) ? row[19] : [];
const birthSeed = row[1] || "";
const flags = row[0] || 0;
writeBitFields(writer, [row[0] || 0], [10]);
writeSaveString(writer, 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; }
writeBitFields(writer, [row[6] || 0, row[7] || 0, row[8] || 0], [8, 8, 8]);
const defaultPersonality = seedPersonalityArray(birthSeed, flags);
const defaultTimers = [0, 0, 0, 0, 0, 0, 0, 0, 100];
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;
writeBitFields(writer, [bodyMask], [5]);
if (bodyMask & 1) writeFixedDiffPacked(writer, defaultPersonality, row[10], 4, 8);
if (bodyMask & 2) writeIndexList(writer, row[11]);
if (bodyMask & 4) writeRelationships(writer, row[12]);
if (bodyMask & 8) writeTimers(writer, row[13]);
if (bodyMask & 16) writeModes(writer, row[14]);
let tailMask = 0;
if (pinIdx >= 0) tailMask |= 1;
if (nestIdx >= 0) tailMask |= 2;
if (stateId || stateTarget >= 0) tailMask |= 4;
if (arrayHasValues(genetics, seedGeneticsArray(birthSeed, flags))) tailMask |= 8;
if (arrayHasValues(fightStats, [0, 0, 0])) tailMask |= 16;
writeBitFields(writer, [tailMask], [5]);
if (tailMask & 1) writer.u(pinIdx);
if (tailMask & 2) writer.u(nestIdx);
if (tailMask & 4) { writeBitFields(writer, [stateId], [1]); writer.s(stateTarget); }
if (tailMask & 8) writeSignedDiffList(writer, seedGeneticsArray(birthSeed, flags), genetics, 5);
if (tailMask & 16) writeSignedDiffList(writer, [0, 0, 0], fightStats, 3);
}
function readTarinai(reader, coordState = null) {
const flags = readBitFields(reader, [10])[0];
const birthSeed = readSaveString(reader);
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, energy, sleepPressure] = readBitFields(reader, [8, 8, 8]);
const [bodyMask] = readBitFields(reader, [5]);
const needs = defaultNeedsFromVitals(hunger, energy, sleepPressure);
const current = (bodyMask & 1) ? readFixedDiffPacked(reader, seedPersonalityArray(birthSeed, flags), 4, 8) : seedPersonalityArray(birthSeed, flags);
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 [tailMask] = readBitFields(reader, [5]);
const pinIdx = (tailMask & 1) ? reader.u() : -1;
const nestIdx = (tailMask & 2) ? reader.u() : -1;
const stateInfo = (tailMask & 4) ? [readBitFields(reader, [1])[0], reader.s()] : [0, -1];
const genetics = (tailMask & 8) ? readSignedDiffList(reader, seedGeneticsArray(birthSeed, flags), 5) : [];
const fightStats = (tailMask & 16) ? readSignedDiffList(reader, [0, 0, 0], 3) : [];
return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo, genetics, fightStats];
}
function writeItemExtra(writer, typeId, extra = []) {
if (STRUCTURE_TYPE_IDS.has(typeId)) {
const defaultHp = defaultStructureHp10(typeId);
const hp = toSafeInt(extra[0] || 0);
const maxHp = toSafeInt(extra[1] || 0);
let mask = 0;
if (valuesDiffer(hp, defaultHp)) mask |= 1;
if (valuesDiffer(maxHp, defaultHp)) mask |= 2;
if ((extra[2] ?? -1) >= 0) mask |= 4;
if ((extra[3] ?? -1) >= 0) mask |= 8;
if (extra[4]) mask |= 16;
if ((extra[5] ?? -1) >= 0) mask |= 32;
if (valuesDiffer(extra[6], 0)) mask |= 64;
writeBitFields(writer, [mask], [7]);
if (mask & 1) writer.s(hp);
if (mask & 2) writer.s(maxHp);
if (mask & 4) writer.u(extra[2]);
if (mask & 8) writer.u(extra[3]);
if (mask & 32) writer.u(extra[5]);
if (mask & 64) writer.s(extra[6] || 0);
} else if (typeId === 0) { // grass
writer.s(extra[0] || 0);
let mask = 0;
if (valuesDiffer(extra[1], 100)) mask |= 1;
if (valuesDiffer(extra[2], 0)) mask |= 2;
if (extra[3]) mask |= 4;
writeBitFields(writer, [mask], [3]);
if (mask & 1) writer.s(extra[1] || 0);
if (mask & 2) writer.s(extra[2] || 0);
} else if (typeId === 1) { // zunchi
writeBitFields(writer, [extra[0] || 0], [2]);
let mask = 0;
if (valuesDiffer(extra[1], 100)) mask |= 1;
if (valuesDiffer(extra[2], 0)) mask |= 2;
writeBitFields(writer, [mask], [2]);
if (mask & 1) writer.s(extra[1] || 0);
if (mask & 2) writer.s(extra[2] || 0);
} else if (typeId === 22) { // signboard
writeSaveString(writer, extra[0] || "");
} else if (typeId === 6) { // duplicator
writer.s(extra[0] ?? -1);
} else if (typeId === 42) { // robot_cleaner
writeBitFields(writer, [Math.max(0, Math.min(0x3f, Number(extra[0] ?? 3) | 0)), extra[1] ? 1 : 0], [6, 1]);
} else if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
writeSaveString(writer, JSON.stringify(extra || []));
} else if (typeId === 21 || typeId === BALLOON_TYPE_ID) { // ball/balloon
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 === FIRECRACKER_TYPE_ID || typeId === FLAME_FIRECRACKER_TYPE_ID || typeId === FIRE_TYPE_ID) { // firecracker/flame/fire
writer.s(extra[0] || 0);
} else if (PIN_TYPE_IDS.has(typeId)) {
const target = extra[1] ?? -1;
let mask = 0;
if (target >= 0) mask |= 1;
if (valuesDiffer(extra[2], 0)) mask |= 2;
if (valuesDiffer(extra[3], 0)) mask |= 4;
if (valuesDiffer(extra[4], 0)) mask |= 8;
writeBitFields(writer, [extra[0] || 0, mask], [2, 4]);
if (mask & 1) writer.u(target);
if (mask & 2) writer.s(extra[2] || 0);
if (mask & 4) writer.s(extra[3] || 0);
if (mask & 8) writer.s(extra[4] || 0);
} else if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) {
let mask = 0;
if (valuesDiffer(extra[0], 0)) mask |= 1;
if (extra[1]) mask |= 2;
writeBitFields(writer, [mask], [2]);
if (mask & 1) writer.s(extra[0] || 0);
} else if (SERVING_FOOD_TYPE_IDS.has(typeId)) {
writer.s(extra[0] || 0);
if (valuesDiffer(extra[1], extra[0])) { writer.u(1); writer.s(extra[1] || 0); }
else writer.u(0);
}
}
function readItemExtra(reader, typeId) {
if (STRUCTURE_TYPE_IDS.has(typeId)) {
const defaultHp = defaultStructureHp10(typeId);
const [mask] = readBitFields(reader, [7]);
const hp = (mask & 1) ? reader.s() : defaultHp;
const maxHp = (mask & 2) ? reader.s() : defaultHp;
return [
hp,
maxHp,
(mask & 4) ? reader.u() : -1,
(mask & 8) ? reader.u() : -1,
(mask & 16) ? 1 : 0,
(mask & 32) ? reader.u() : -1,
(mask & 64) ? reader.s() : 0,
];
}
if (typeId === 0) {
const growth = reader.s();
const [mask] = readBitFields(reader, [3]);
return [growth, (mask & 1) ? reader.s() : 100, (mask & 2) ? reader.s() : 0, (mask & 4) ? 1 : 0];
}
if (typeId === 1) {
const [stage] = readBitFields(reader, [2]);
const [mask] = readBitFields(reader, [2]);
return [stage, (mask & 1) ? reader.s() : 100, (mask & 2) ? reader.s() : 0];
}
if (typeId === 22) return [readSaveString(reader)];
if (typeId === 6) return [reader.s()];
if (typeId === 42) return readBitFields(reader, [6, 1]);
if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
try {
const parsed = JSON.parse(readSaveString(reader) || "[]");
return (Array.isArray(parsed) || (parsed && typeof parsed === "object")) ? parsed : [];
} catch (_) {
return [];
}
}
if (typeId === 21 || typeId === BALLOON_TYPE_ID) return [reader.s(), reader.s()];
if (typeId === 23) return [reader.s(), reader.s()];
if (typeId === 24) return [reader.s()];
if (typeId === FIRECRACKER_TYPE_ID || typeId === FLAME_FIRECRACKER_TYPE_ID || typeId === FIRE_TYPE_ID) return [reader.s()];
if (PIN_TYPE_IDS.has(typeId)) {
const [state, mask] = readBitFields(reader, [2, 4]);
return [
state,
(mask & 1) ? reader.u() : -1,
(mask & 2) ? reader.s() : 0,
(mask & 4) ? reader.s() : 0,
(mask & 8) ? reader.s() : 0,
];
}
if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) {
const [mask] = readBitFields(reader, [2]);
return [(mask & 1) ? reader.s() : 0, (mask & 2) ? 1 : 0];
}
if (SERVING_FOOD_TYPE_IDS.has(typeId)) {
const remaining = reader.s();
return [remaining, reader.u() ? reader.s() : remaining];
}
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) {
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);
writeItemExtra(writer, typeId, row[4] || []);
}
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 = []) {
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);
}
}
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 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) {
if (!snapshot || snapshot.v !== Snapshot.version || snapshot.a !== "tj1") throw new Error("snapshot version mismatch");
const writer = new BinaryWriter();
activeStringWriteMap = null;
try {
writer.b(BINARY_SCHEMA_VERSION);
writer.u(Snapshot.version);
writeStringTable(writer, snapshot);
writeWorld(writer, snapshot.w || []);
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
writer.u(tarinaiRows.length);
const tarCoordState = { x: 0, y: 0, age: 0, generation: 1 };
for (const row of tarinaiRows) writeTarinai(writer, row || [], tarCoordState);
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
writeItemBlocks(writer, itemRows);
return writer.out();
} finally {
activeStringWriteMap = null;
}
}
function decodeBinarySnapshot(bytes) {
const reader = new BinaryReader(bytes);
activeStringReadList = null;
try {
const schema = reader.b();
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
const snapshotVersion = reader.u();
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
readStringTable(reader);
const w = readWorld(reader);
const tCount = reader.u();
const t = [];
const tarCoordState = { x: 0, y: 0, age: 0, generation: 1 };
for (let i = 0; i < tCount; i++) t.push(readTarinai(reader, tarCoordState));
const items = readItemBlocks(reader);
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 };
} finally {
activeStringReadList = null;
}
}
async function packBytesForSave(bytes) {
const candidates = [{ codec: RAW_CODEC, packed: bytes }];
const dict = dictionaryTransform(bytes);
if (dict.length < bytes.length) candidates.push({ codec: DICT_CODEC, packed: dict });
try {
const compressed = await compressBytes(bytes);
if (compressed.length < bytes.length) candidates.push({ codec: DEFLATE_CODEC, packed: compressed });
if (dict.length < bytes.length) {
const dictCompressed = await compressBytes(dict);
if (dictCompressed.length < bytes.length) candidates.push({ codec: DICT_DEFLATE_CODEC, packed: dictCompressed });
}
} catch (error) { console.warn("save compression fallback", error); }
return candidates.sort((a, b) => a.packed.length - b.packed.length)[0];
}
async function encodeSnapshot(snapshot) {
const bytes = encodeBinarySnapshot(snapshot);
const { codec, packed } = await packBytesForSave(bytes);
return `${EXPORT_PREFIX}${saveHeader(codec, packed.length)}${jp4096Encode(packed)}`;
}
async function decodeSnapshot(text) {
const raw = String(text || "").trim();
if (!raw.startsWith(EXPORT_PREFIX)) throw new Error("unsupported Japanese save text");
const header = parseSaveHeader(raw.charAt(EXPORT_PREFIX.length));
const payload = raw.slice(EXPORT_PREFIX.length + 1);
const packed = jp4096Decode(payload, header.remainder);
const packedCodec = header.codec === DICT_DEFLATE_CODEC ? DEFLATE_CODEC : (header.codec === DICT_CODEC ? RAW_CODEC : header.codec);
let bytes = await decompressBytes(packed, packedCodec);
if (header.codec === DICT_CODEC || header.codec === DICT_DEFLATE_CODEC) bytes = dictionaryRestore(bytes);
return decodeBinarySnapshot(bytes);
}
global.TarinaiSaveCodec = {
encodeSnapshot,
decodeSnapshot,
};
})(typeof window !== "undefined" ? window : globalThis);