139 lines
4.2 KiB
JavaScript
139 lines
4.2 KiB
JavaScript
"use strict";
|
|
|
|
(function (global) {
|
|
const Snapshot = global.TarinaiSnapshot;
|
|
if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_codec.js");
|
|
const EXPORT_PREFIX = "TN3!";
|
|
const SAVE_TEXT_ALPHABET = (() => {
|
|
let out = "";
|
|
for (let i = 33; i <= 126; i++) {
|
|
const ch = String.fromCharCode(i);
|
|
if (ch === "*" || ch === "'" || ch === "_") continue;
|
|
out += ch;
|
|
}
|
|
return out;
|
|
})();
|
|
const SAVE_TEXT_REVERSE = (() => {
|
|
const map = new Map();
|
|
for (let i = 0; i < SAVE_TEXT_ALPHABET.length; i++) map.set(SAVE_TEXT_ALPHABET[i], i);
|
|
return map;
|
|
})();
|
|
if (SAVE_TEXT_ALPHABET.length !== 91) throw new Error("save alphabet must contain 91 printable ASCII chars");
|
|
|
|
function base91Encode(bytes) {
|
|
let b = 0;
|
|
let n = 0;
|
|
let out = "";
|
|
for (const byte of bytes || []) {
|
|
b |= (byte & 255) << n;
|
|
n += 8;
|
|
if (n > 13) {
|
|
let v = b & 8191;
|
|
if (v > 88) {
|
|
b >>= 13;
|
|
n -= 13;
|
|
} else {
|
|
v = b & 16383;
|
|
b >>= 14;
|
|
n -= 14;
|
|
}
|
|
out += SAVE_TEXT_ALPHABET[v % 91] + SAVE_TEXT_ALPHABET[Math.floor(v / 91)];
|
|
}
|
|
}
|
|
if (n) {
|
|
out += SAVE_TEXT_ALPHABET[b % 91];
|
|
if (n > 7 || b > 90) out += SAVE_TEXT_ALPHABET[Math.floor(b / 91)];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function base91Decode(text) {
|
|
const clean = String(text || "").trim();
|
|
let v = -1;
|
|
let b = 0;
|
|
let n = 0;
|
|
const out = [];
|
|
for (const ch of clean) {
|
|
const c = SAVE_TEXT_REVERSE.get(ch);
|
|
if (c == null) continue;
|
|
if (v < 0) {
|
|
v = c;
|
|
} else {
|
|
v += c * 91;
|
|
b |= v << n;
|
|
n += (v & 8191) > 88 ? 13 : 14;
|
|
do {
|
|
out.push(b & 255);
|
|
b >>= 8;
|
|
n -= 8;
|
|
} while (n > 7);
|
|
v = -1;
|
|
}
|
|
}
|
|
if (v >= 0) out.push((b | (v << n)) & 255);
|
|
return new Uint8Array(out);
|
|
}
|
|
|
|
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 === "D") {
|
|
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());
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
async function encodeSnapshot(snapshot) {
|
|
if (!snapshot || snapshot.v !== Snapshot.version) throw new Error("snapshot version mismatch");
|
|
const json = JSON.stringify(snapshot);
|
|
const bytes = new TextEncoder().encode(json);
|
|
let codec = "R";
|
|
let packed = bytes;
|
|
try {
|
|
const compressed = await compressBytes(bytes);
|
|
if (compressed.length < bytes.length) {
|
|
codec = "D";
|
|
packed = compressed;
|
|
}
|
|
} catch (error) {
|
|
console.warn("save compression fallback", error);
|
|
}
|
|
return `${EXPORT_PREFIX}${codec}${base91Encode(packed)}`;
|
|
}
|
|
|
|
async function decodeSnapshot(text) {
|
|
const raw = String(text || "").trim();
|
|
if (!raw) throw new Error("empty import text");
|
|
if (raw.startsWith("{") || raw.startsWith("[")) {
|
|
const data = JSON.parse(raw);
|
|
if (data?.v !== Snapshot.version) throw new Error("unsupported save version");
|
|
return data;
|
|
}
|
|
if (!raw.startsWith(EXPORT_PREFIX)) throw new Error("unsupported save text");
|
|
const codec = raw.charAt(EXPORT_PREFIX.length) || "R";
|
|
const payload = raw.slice(EXPORT_PREFIX.length + 1);
|
|
const packed = base91Decode(payload);
|
|
const bytes = await decompressBytes(packed, codec);
|
|
const json = new TextDecoder().decode(bytes);
|
|
const data = JSON.parse(json);
|
|
if (data?.v !== Snapshot.version) throw new Error("unsupported save version");
|
|
return data;
|
|
}
|
|
|
|
global.TarinaiSaveCodec = {
|
|
EXPORT_PREFIX,
|
|
SAVE_TEXT_ALPHABET,
|
|
base91Encode,
|
|
base91Decode,
|
|
encodeSnapshot,
|
|
decodeSnapshot,
|
|
};
|
|
})(typeof window !== "undefined" ? window : globalThis);
|