This commit is contained in:
33333-33333 2026-07-29 20:52:11 +09:00
commit 091afc2b5c
77 changed files with 4405 additions and 8223 deletions

View file

@ -24,6 +24,7 @@
"normalItems",
"physicsItems",
"achievementMetadata",
"extendedState",
]);
let lastEncodeStats = null;
let lastDecodeStats = null;
@ -477,12 +478,6 @@
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 = [];
@ -526,14 +521,14 @@
return false;
}
function writeTimers(writer, timers = []) {
const defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100];
const defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100, 0];
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 defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100, 0];
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();
@ -696,19 +691,6 @@
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) {
@ -765,7 +747,7 @@
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 defaultTimers = [0, 0, 0, 0, 0, 0, 0, 0, 100, 0];
const genetics = Array.isArray(row[18]) ? row[18] : [];
const fightStats = Array.isArray(row[19]) ? row[19] : [];
let bodyMask = 0;
@ -776,7 +758,7 @@
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 stateId = Math.max(0, Math.min(15, Number(row[17]?.[0]) || 0));
const stateTarget = row[17]?.[1] ?? -1;
const geneticsBase = seedGeneticsArray(birth.seed, flags);
let linkMask = 0;
@ -786,7 +768,7 @@
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))),
energy: Math.max(0, Math.min(240, Math.round(Number(row[7]) || 0))),
sleepPressure: Math.max(0, Math.min(100, Math.round(Number(row[8]) || 0))),
};
}
@ -794,11 +776,11 @@
const TARINAI_COLUMN_LAYOUT = Object.freeze([
["flags", 10],
["hunger", 7],
["energy", 7],
["energy", 8],
["sleepPressure", 7],
["bodyMask", 5],
["linkMask", 2],
["stateId", 1],
["stateId", 4],
["geneticsMask", 5],
["fightMask", 3],
]);
@ -899,7 +881,7 @@
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 timers = (bodyMask & 8) ? readTimers(reader) : [0, 0, 0, 0, 0, 0, 0, 0, 100, 0];
const modes = (bodyMask & 16) ? readModes(reader) : [0, 0, 0];
const pinIdx = (linkMask & 1) ? reader.u() : -1;
const nestIdx = (linkMask & 2) ? reader.u() : -1;
@ -1473,13 +1455,16 @@
return items;
}
const ACHIEVEMENT_METADATA_BINARY_VERSION = 1;
const ACHIEVEMENT_SPELL_BINARY_VERSION = 8;
const ACHIEVEMENT_METADATA_BINARY_VERSION = 3;
const ACHIEVEMENT_SPELL_BINARY_VERSION = 11;
const ACHIEVEMENT_MASK_BYTES = 10;
const ACHIEVEMENT_PROGRESS_BITS = Object.freeze([7, 4, 7, 12, 5, 5, 5, 11, 6, 10, 9, 10, 3, 17, 5, 14, 7]);
const ACHIEVEMENT_PROGRESS_MAX = Object.freeze([100, 15, 100, 3333, 30, 20, 20, 2047, 50, 666, 333, 721, 7, 131071, 30, 10000, 100]);
const ACHIEVEMENT_PROGRESS_BITS_V9 = Object.freeze([7, 4, 7, 12, 5, 5, 5, 11, 6, 10, 9, 10, 3, 17, 5, 14, 7]);
const ACHIEVEMENT_PROGRESS_MAX_V9 = Object.freeze([100, 15, 100, 3333, 30, 20, 20, 2047, 50, 666, 333, 721, 7, 131071, 30, 10000, 100]);
const ACHIEVEMENT_PROGRESS_BITS = Object.freeze([...ACHIEVEMENT_PROGRESS_BITS_V9, 4, 6, 8]);
const ACHIEVEMENT_PROGRESS_MAX = Object.freeze([...ACHIEVEMENT_PROGRESS_MAX_V9, 10, 33, 200]);
function finiteNumberOrNull(value) {
if (value === null || value === undefined) return null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
@ -1501,9 +1486,9 @@
}
function achievementMaskBytesFromSpell(spell = null) {
const bytes = new Uint8Array(ACHIEVEMENT_MASK_BYTES);
if (!Array.isArray(spell)) return { bytes, baseMinute: 0, deltas: [], progress: [] };
if (!Array.isArray(spell)) return { bytes, baseMinute: 0, deltas: [], progress: [], mysteryDrugIds: [] };
const version = Number(spell[0]);
if (version !== ACHIEVEMENT_SPELL_BINARY_VERSION) throw new Error("unsupported achievement spell version");
if (![9, 10, ACHIEVEMENT_SPELL_BINARY_VERSION].includes(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)));
const extra = Math.max(0, Math.min(0xffff, Math.floor(Number(spell[3]) || 0)));
@ -1514,21 +1499,26 @@
const baseMinute = toSafeUInt(spell[4]);
const deltas = Array.isArray(spell[5]) ? spell[5].map(toSafeUInt) : [];
const progress = Array.isArray(spell[6]) ? spell[6] : [];
const mysteryDrugIds = version >= 10 && Array.isArray(spell[7])
? [...new Set(spell[7].map(value => String(value || "")).filter(Boolean))].slice(0, 10)
: [];
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 };
return { bytes, baseMinute, deltas, progress, mysteryDrugIds };
}
function spellFromAchievementMaskBytes(bytes = [], baseMinute = 0, deltas = [], progress = []) {
function spellFromAchievementMaskBytes(bytes = [], baseMinute = 0, deltas = [], progress = [], version = ACHIEVEMENT_SPELL_BINARY_VERSION, mysteryDrugIds = []) {
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, (Number(bytes[8]) || 0) + (Number(bytes[9]) || 0) * 256, toSafeUInt(baseMinute), deltas.map(toSafeUInt), progress];
const spell = [version, low >>> 0, high >>> 0, (Number(bytes[8]) || 0) + (Number(bytes[9]) || 0) * 256, toSafeUInt(baseMinute), deltas.map(toSafeUInt), progress];
if (version >= 10) spell.push([...new Set((mysteryDrugIds || []).map(value => String(value || "")).filter(Boolean))].slice(0, 10));
return spell;
}
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]))));
@ -1560,9 +1550,11 @@
writer.raw(normalized.bytes);
writer.u(normalized.baseMinute);
for (const delta of normalized.deltas) writer.u(delta);
writeAchievementProgress(writer, normalized.progress);
writeAchievementProgress(writer, normalized.progress, ACHIEVEMENT_PROGRESS_BITS, ACHIEVEMENT_PROGRESS_MAX);
writer.u(normalized.mysteryDrugIds.length);
for (const id of normalized.mysteryDrugIds) writer.str(id);
}
function readAchievementSpell(reader) {
function readAchievementSpell(reader, metadataVersion = ACHIEVEMENT_METADATA_BINARY_VERSION) {
const bytes = reader.raw(ACHIEVEMENT_MASK_BYTES);
let unlockedCount = 0;
for (const byte of bytes) {
@ -1572,8 +1564,21 @@
const baseMinute = reader.u();
const deltas = [];
for (let i = 0; i < unlockedCount; i++) deltas.push(reader.u());
const progress = readAchievementProgress(reader, ACHIEVEMENT_PROGRESS_BITS, ACHIEVEMENT_PROGRESS_MAX);
return spellFromAchievementMaskBytes(bytes, baseMinute, deltas, progress);
const current = metadataVersion >= 3;
const progress = readAchievementProgress(
reader,
current ? ACHIEVEMENT_PROGRESS_BITS : ACHIEVEMENT_PROGRESS_BITS_V9,
current ? ACHIEVEMENT_PROGRESS_MAX : ACHIEVEMENT_PROGRESS_MAX_V9,
);
const mysteryDrugIds = [];
if (current) {
const count = Math.min(10, reader.u());
for (let i = 0; i < count; i++) {
const id = String(reader.str() || "");
if (id && !mysteryDrugIds.includes(id)) mysteryDrugIds.push(id);
}
}
return spellFromAchievementMaskBytes(bytes, baseMinute, deltas, progress, current ? ACHIEVEMENT_SPELL_BINARY_VERSION : 9, mysteryDrugIds);
}
function writeAchievementMetadata(writer, metadata = null, tarinaiCount = 0, itemCount = 0) {
const value = metadata && typeof metadata === "object" ? metadata : {};
@ -1588,19 +1593,27 @@
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 timeDefaults = [null, -1, null, 0, -1, -1, -1];
const timeValues = [w[5], w[6], w[7], w[8], w[9], w[10], w[11]];
// Two-bit state per value: 0 = schema default, 1 = explicit null,
// 2 = explicit finite number. This preserves null separately from 0/-1.
const timeStates = [];
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));
if (finite == null) {
timeStates.push(defaultValue == null ? 0 : 1);
return;
}
if (finite === defaultValue) {
timeStates.push(0);
return;
}
timeStates.push(2);
encodedTimes.push(quantizedTenths(finite));
});
writeBitFields(writer, [timeMask], [timeDefaults.length]);
writeBitFields(writer, timeStates, timeStates.map(() => 2));
encodedTimes.forEach(value => writer.s(value));
const tarRows = Array.isArray(value.t) ? value.t : [];
@ -1611,12 +1624,21 @@
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 });
const slaveDepth = toSafeUInt(row[4]);
const kingDepth = toSafeUInt(row[5]);
if (feed || treatment || fightAt != null || saunaAt != null || slaveDepth || kingDepth) {
activeTarRows.push({ index, feed, treatment, fightAt, saunaAt, slaveDepth, kingDepth });
}
}
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); }
for (const row of activeTarRows) {
writer.u(row.feed);
writer.u(row.treatment);
writer.u(row.slaveDepth);
writer.u(row.kingDepth);
}
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));
@ -1645,18 +1667,22 @@
}
function readAchievementMetadata(reader, tarinaiCount = 0, itemCount = 0) {
const version = reader.b();
if (version !== ACHIEVEMENT_METADATA_BINARY_VERSION) throw new Error("unsupported achievement metadata schema");
if (![2, ACHIEVEMENT_METADATA_BINARY_VERSION].includes(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]);
const timeDefaults = version >= 3 ? [null, -1, null, 0, -1, -1, -1] : [null, -1, null, 0, -1, -1];
const timeStates = readBitFields(reader, timeDefaults.map(() => 2));
for (let index = 0; index < timeDefaults.length; index++) {
w.push(timeMask & (1 << index) ? unquantizedTenths(reader.s()) : timeDefaults[index]);
const state = timeStates[index];
if (state === 0) w.push(timeDefaults[index]);
else if (state === 1) w.push(null);
else if (state === 2) w.push(unquantizedTenths(reader.s()));
else throw new Error("invalid achievement metadata time state");
}
const t = Array.from({ length: tarinaiCount }, () => [0, 0, null, null]);
const t = Array.from({ length: tarinaiCount }, () => [0, 0, null, null, 0, 0]);
const tarCount = reader.u();
const tarIndexes = [];
let previousTarIndex = -1;
@ -1665,13 +1691,22 @@
if (previousTarIndex >= tarinaiCount) throw new Error("invalid achievement tarinai metadata index");
tarIndexes.push(previousTarIndex);
}
const tarCounters = tarIndexes.map(() => [reader.u(), reader.u()]);
const tarCounters = tarIndexes.map(() => version >= 3
? [reader.u(), reader.u(), reader.u(), reader.u()]
: [reader.u(), reader.u(), 0, 0]);
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];
t[index] = [
tarCounters[rowIndex][0],
tarCounters[rowIndex][1],
fightAt,
saunaAt,
tarCounters[rowIndex][2],
tarCounters[rowIndex][3],
];
});
const i = Array.from({ length: itemCount }, () => [0, null]);
@ -1691,7 +1726,7 @@
i[index] = [placed, placedAt];
});
const metadata = { w, t, i };
if (flags & 1) metadata.s = readAchievementSpell(reader);
if (flags & 1) metadata.s = readAchievementSpell(reader, version);
return metadata;
}
@ -1730,6 +1765,7 @@
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
writeItemBlocks(writer, itemRows);
writer.category("achievementMetadata", () => writeAchievementMetadata(writer, snapshot.g || null, tarinaiRows.length, itemRows.length));
writer.category("extendedState", () => writer.str(JSON.stringify(snapshot.x || {})));
return writer.out();
} finally {
activeStringWriteMap = null;
@ -1743,7 +1779,7 @@
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");
if (![54, BINARY_SCHEMA_VERSION].includes(schema)) throw new Error("unsupported binary save schema");
readStringTable(reader, hasStringTable);
const w = readWorld(reader);
const tCount = reader.u();
@ -1758,8 +1794,10 @@
} catch (_) {
throw new Error("invalid achievement metadata in binary save");
}
let x = {};
try { x = JSON.parse(reader.str() || "{}"); } catch (_) { throw new Error("invalid extended state 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 };
return { v: Snapshot.version, a: "tj1", m: summaryFromPayload(w, t.length), w, t, i: items, r: [], g, x };
} finally {
activeStringReadList = null;
}