Hash
The save file format never stores field names. Each entry is identified only by a 32-bit hash.
Algorithm
Section titled “Algorithm”Field names are hashed with Murmur3 x86 32-bit using seed 0 and the UTF-8 encoding of the name.
For reference, the constants the game uses are:
c1 = 0xCC9E2D51c2 = 0x1B873593- Block rotate
15, mix rotate13, mix multiplier5, mix constant0xE6546B64 - Finalizer multipliers
0x85EBCA6Band0xC2B2AE35, shift16 / 13 / 16
TypeScript reference implementation
Section titled “TypeScript reference implementation”This is the implementation ltdsave.app uses itself, lifted verbatim from src/lib/sav/hash.ts.
const C1 = 0xcc9e2d51;const C2 = 0x1b873593;
export function murmur3_x86_32(input: string, seed = 0): number { const bytes = new TextEncoder().encode(input); return murmur3_x86_32_bytes(bytes, seed);}
export function murmur3_x86_32_bytes(bytes: Uint8Array, seed = 0): number { const len = bytes.length; const nBlocks = (len / 4) | 0; let h1 = seed >>> 0;
for (let i = 0; i < nBlocks; i++) { const o = i * 4; let k1 = (bytes[o] | (bytes[o + 1] << 8) | (bytes[o + 2] << 16) | (bytes[o + 3] << 24)) >>> 0; k1 = Math.imul(k1, C1); k1 = (k1 << 15) | (k1 >>> 17); k1 = Math.imul(k1, C2);
h1 ^= k1; h1 = (h1 << 13) | (h1 >>> 19); h1 = (Math.imul(h1, 5) + 0xe6546b64) | 0; }
let k1 = 0; const tail = nBlocks * 4; switch (len & 3) { case 3: k1 ^= bytes[tail + 2] << 16; // fallthrough case 2: k1 ^= bytes[tail + 1] << 8; // fallthrough case 1: k1 ^= bytes[tail]; k1 = Math.imul(k1, C1); k1 = (k1 << 15) | (k1 >>> 17); k1 = Math.imul(k1, C2); h1 ^= k1; }
h1 ^= len; h1 ^= h1 >>> 16; h1 = Math.imul(h1, 0x85ebca6b); h1 ^= h1 >>> 13; h1 = Math.imul(h1, 0xc2b2ae35); h1 ^= h1 >>> 16; return h1 >>> 0;}Collisions
Section titled “Collisions”Every name in the save schema produces a distinct hash. There are no known collisions.