// OVE — Script ⇢ Hash share codec (v7.8 — scheme ▼, "big-base packing"). // // Turns a script into ONE short shareable Unicode string and back, exactly: // // Bass Bloom:[▼䷀…一…CJK/Hangul digits…㘎〠⼁⼃㘚⑔⑒⑑⑔⑕⑓⑑⑓⑖] // └─ label │└──────────────┘└─┬┘└ check digits // scheme ▼ 〠 = header separator // // WHY IT'S SHORT — the compression ladder (each layer feeds the next): // // 1. DICTIONARY — ~300 built-in words → 1 symbol each: every function // (sin(, cos(, if(, … WITH their open paren), every audio variable // (bass, mid, treb, vol_att, beatage, …), every header key and value, // statement openers ("x = " ⇦ "green = " ⇩), the AVS idioms // ("hue = hue + dt * ", "t = t + dt * ", " * 0.", ") * ", "\n\n[point]\n"), // ~70 number literals (6.28318, 3.14159, 0.55, 0.25, …) and the // user-named core: ♤ line return, ♡ beats, ♠ beatage, ♦ space, // ♩♪♫♬ the four sections. // // 2. BIG-BASE PACKING — the clever part. The dictionary output (a string // over ~40–120 distinct symbols) is read as ONE BIG NUMBER in base // |alphabet| and rewritten in base 32,164 — one digit per character, // the digits drawn from the CJK Unified (20,992) + Hangul (11,172) // blocks. That carries ~15 bits per character where base-62 (the // classic "1400 → Ma" trick) carries 5.95 — so "1400" costs ~0.85 of // a character, and a whole preset rides at roughly a THIRD of its // source length. The decoder gets the alphabet back from a bitmap // header (the dictionary is built-in, so "which symbols are used" is // 22 bitmap digits, not 80 literal ones). // // 3. PAIR PACKING (fallback) — scripts too short for the header to pay // off keep the v7.7 body instead: dictionary + two leftover // characters folded into one CJK code point (the classic layer — // it reads nicer for small scripts, and the encoder simply picks // whichever body is shorter). // // Guarantees (unchanged from v7.7): // • LOSSLESS — decode(encode(s)) === s for ANY text (comments, weird // whitespace, even scripts that don't compile). Anything the dictionary // doesn't cover passes through untouched, and every reserved symbol // that legitimately appears in a script is escaped ("\♠" → literal ♠). // • COPY-SAFE — no raw whitespace or control characters anywhere in the // string (folded to symbols / control pictures), so chat apps and // social sites can't collapse it. Emoji-variation selectors and // zero-width chars that platforms inject while copying are stripped on // decode (and escaped on encode, so a script that really contains them // still round-trips). // • SELF-CHECKING — the last 8 chars are check digits (①–⑯ as hex) over // an FNV-1a digest of the script, so a string that was cut off by a // character limit or altered in transit is rejected honestly instead // of decoding into garbage. // // The "Name:" prefix is cosmetic (the script's own `name:` header rides // inside the body untouched) — it just makes the string self-describing in // a social feed. // // ▲ strings from v7.7 still decode (their tables are kept below, decode // only). ▼ is everything since. // ── scheme markers ───────────────────────────────────────────────────────── /** v7.7 strings (dictionary + pair packing). Decode-only. */ const SCHEME_V1 = '▲' /** v7.8+ strings (dictionary + big-base or pair, whichever is shorter). */ const SCHEME_V2 = '▼' /** Escape char — prefixes any reserved symbol that appears literally. */ const ESCAPE = '\\' /** * Header separator (U+3020 POSTAL MARK FACE). Splits the payload digits * from the bitmap header. Reserved, so it only ever appears in a body as * an escaped pair — the decoder can treat the FIRST bare 〠 as structural. */ const SEP = '〠' interface DictEntry { key: string symbol: string } // ── the dictionary ───────────────────────────────────────────────────────── /** * The substitution dictionary. Longest key wins at every position, so * "smoothstep(" resolves before "smoothstep", "beatage" before "beat" and * "hue = hue + dt * " before "hue = ". Symbols are single BMP code points, * none of them ASCII. * * COMPATIBILITY CONTRACT: the symbols of this table (plus the noise * symbols below) form the frozen BITMAP_LIST that ▼ headers are built * from. Adding entries to DICT changes BITMAP_LIST and silently breaks * every ▼ string encoded by an older build — after v7.8 ships, new * dictionary entries require a new scheme marker (▽), not an edit here. */ const DICT: DictEntry[] = [ // ── user-named core references ───────────────────────────────────────── { key: '\n', symbol: '♤' }, // line return { key: ' ', symbol: '♦' }, // space { key: 'beats', symbol: '♡' }, { key: 'beatage', symbol: '♠' }, { key: '\t', symbol: '♣' }, // tab // ── line ends (auto-ported presets are full of these) ────────────────── { key: '\r\n', symbol: '⼀' }, { key: ';\r\n', symbol: '⼁' }, { key: ';\n', symbol: '⼂' }, { key: '; ', symbol: '⼃' }, // ── whitespace runs ──────────────────────────────────────────────────── { key: ' ', symbol: '▣' }, // 4-space indent { key: ' ', symbol: '·' }, // 2-space indent { key: '\r', symbol: '⏎' }, // lone carriage return // ── section headers (music notes) ────────────────────────────────────── { key: '[init]', symbol: '♩' }, { key: '[frame]', symbol: '♪' }, { key: '[beat]', symbol: '♫' }, { key: '[point]', symbol: '♬' }, // ── blank line + section (the generated-preset skeleton) ─────────────── { key: '\n\n', symbol: '⽞' }, { key: '\n\n[init]\n', symbol: '⽟' }, { key: '\n\n[frame]\n', symbol: '⽠' }, { key: '\n\n[beat]\n', symbol: '⽡' }, { key: '\n\n[point]\n', symbol: '⽢' }, // ── comments ─────────────────────────────────────────────────────────── { key: '# ', symbol: '♯' }, { key: '#', symbol: '♭' }, { key: '//', symbol: '♮' }, // ── header keys ──────────────────────────────────────────────────────── { key: 'description', symbol: '♖' }, { key: 'linewidth: ', symbol: '⼅' }, { key: 'blend: additive', symbol: '⼆' }, { key: 'mode: lines', symbol: '⼇' }, { key: 'mode: dots', symbol: '⼈' }, { key: 'linewidth', symbol: '♚' }, { key: 'persist', symbol: '♙' }, { key: 'author', symbol: '♕' }, { key: 'ysign', symbol: '♛' }, { key: 'blend', symbol: '♘' }, { key: 'name', symbol: '♔' }, { key: 'mode', symbol: '♗' }, { key: ': ', symbol: '⼄' }, // every other header's colon // ── header values (zodiac) ───────────────────────────────────────────── { key: 'additive', symbol: '♋' }, { key: 'lines', symbol: '♈' }, { key: 'dots', symbol: '♉' }, { key: 'solid', symbol: '♊' }, { key: 'alpha', symbol: '♌' }, // ── built-in variables (the engine's full injected set) ──────────────── { key: 'bass_att', symbol: '♎' }, { key: 'mid_att', symbol: '♏' }, { key: 'treb_att', symbol: '♐' }, { key: 'vol_att', symbol: '⼉' }, { key: 'beatphase', symbol: '⼊' }, { key: 'beatconf', symbol: '⼋' }, { key: 'bass', symbol: '♑' }, { key: 'treb', symbol: '♓' }, { key: 'frame', symbol: '⼌' }, { key: 'time', symbol: '⼍' }, { key: 'mid', symbol: '♒' }, { key: 'vol', symbol: '☽' }, { key: 'beat', symbol: '◉' }, { key: 'red', symbol: '☾' }, { key: 'blue', symbol: '⚑' }, { key: 'skip', symbol: '⚐' }, { key: 'green', symbol: '☿' }, { key: 'aspect', symbol: '⚶' }, { key: 'chroma', symbol: '⼏' }, { key: 'dt', symbol: '◐' }, { key: 'pi', symbol: '◑' }, { key: 'tau', symbol: '◒' }, { key: 'bpm', symbol: '⼎' }, { key: 'hue', symbol: '⋊' }, { key: 'rad', symbol: '⋅' }, { key: 'flash', symbol: '∘' }, { key: 'sv', symbol: '⼐' }, // per-point spectrum level // ── functions WITH their open paren (a call costs one fewer symbol) ──── { key: 'smoothstep(', symbol: '⼑' }, { key: 'gmegabuf(', symbol: '⼒' }, { key: 'getkbmouse(', symbol: '⼓' }, { key: 'megabuf(', symbol: '⼔' }, { key: 'getspec(', symbol: '⼕' }, { key: 'getosc(', symbol: '⼖' }, { key: 'gettime(', symbol: '⼗' }, { key: 'invsqrt(', symbol: '⼘' }, { key: 'sigmoid(', symbol: '⼙' }, { key: 'atan2(', symbol: '⼚' }, { key: 'inrange(', symbol: '⼛' }, { key: 'log10(', symbol: '⼜' }, { key: 'floor(', symbol: '⼝' }, { key: 'above(', symbol: '⼞' }, { key: 'below(', symbol: '⼟' }, { key: 'equal(', symbol: '⼠' }, { key: 'round(', symbol: '⼡' }, { key: 'clamp(', symbol: '⼢' }, { key: 'lerp(', symbol: '⼣' }, { key: 'asin(', symbol: '⼤' }, { key: 'acos(', symbol: '⼥' }, { key: 'atan(', symbol: '⼦' }, { key: 'sign(', symbol: '⼧' }, { key: 'band(', symbol: '⼨' }, { key: 'bnot(', symbol: '⼩' }, { key: 'hsvr(', symbol: '⼪' }, { key: 'hsvg(', symbol: '⼫' }, { key: 'hsvb(', symbol: '⼬' }, { key: 'ceil(', symbol: '⼭' }, { key: 'frac(', symbol: '⼮' }, { key: 'pow(', symbol: '⼯' }, { key: 'exp(', symbol: '⼰' }, { key: 'log(', symbol: '⼱' }, { key: 'int(', symbol: '⼲' }, { key: 'min(', symbol: '⼳' }, { key: 'max(', symbol: '⼴' }, { key: 'abs(', symbol: '⼵' }, { key: 'sin(', symbol: '⼶' }, { key: 'cos(', symbol: '⼷' }, { key: 'tan(', symbol: '⼸' }, { key: 'if(', symbol: '⼹' }, { key: 'rnd(', symbol: '⼺' }, { key: 'rand(', symbol: '⼻' }, { key: 'loop(', symbol: '⼼' }, // ── functions without paren (for the times they're not calls) ────────── { key: 'smoothstep', symbol: '⌒' }, { key: 'gmegabuf', symbol: '✥' }, { key: 'getkbmouse', symbol: '◶' }, { key: 'atan2', symbol: '⌗' }, { key: 'megabuf', symbol: '✤' }, { key: 'getspec', symbol: '▤' }, { key: 'getosc', symbol: '▥' }, { key: 'gettime', symbol: '◷' }, { key: 'log10', symbol: '㏑' }, { key: 'invsqrt', symbol: '√' }, { key: 'inrange', symbol: '⋔' }, { key: 'sigmoid', symbol: '∿' }, { key: 'floor', symbol: '⌊' }, { key: 'above', symbol: '≻' }, { key: 'below', symbol: '≺' }, { key: 'asin', symbol: '⌠' }, { key: 'acos', symbol: '⌡' }, { key: 'atan', symbol: '⌐' }, { key: 'ceil', symbol: '⌈' }, { key: 'sqr', symbol: '□' }, { key: 'sqrt', symbol: '∛' }, { key: 'pow', symbol: '⌃' }, { key: 'exp', symbol: 'ℯ' }, { key: 'log', symbol: '㏒' }, { key: 'round', symbol: '◎' }, { key: 'int', symbol: '⊺' }, { key: 'frac', symbol: '∵' }, { key: 'sign', symbol: '±' }, { key: 'bnot', symbol: '¬' }, { key: 'band', symbol: '∧' }, { key: 'bor', symbol: '∨' }, { key: 'equal', symbol: '≟' }, { key: 'lerp', symbol: '↝' }, { key: 'clamp', symbol: '⊜' }, { key: 'rand', symbol: '⚄' }, { key: 'rnd', symbol: '⚅' }, { key: 'sin', symbol: '◠' }, { key: 'cos', symbol: '◡' }, { key: 'tan', symbol: '◮' }, { key: 'abs', symbol: '¦' }, { key: 'min', symbol: '≪' }, { key: 'max', symbol: '≫' }, { key: 'if', symbol: '∴' }, { key: 'loop', symbol: '↻' }, // ── near-universal user variables (mined from the preset library) ────── { key: 'rings', symbol: '⼽' }, { key: 'spokes', symbol: '⼾' }, { key: 'travel', symbol: '⼿' }, { key: 'bars', symbol: '⽀' }, { key: 'segs', symbol: '⽁' }, { key: 'ring', symbol: '⽂' }, { key: 'spoke', symbol: '⽃' }, { key: 'fade', symbol: '⽄' }, { key: 'size', symbol: '⽅' }, { key: 'rot', symbol: '⽆' }, { key: 'seg', symbol: '⽇' }, { key: 'col', symbol: '⽈' }, { key: 'lum', symbol: '⽉' }, { key: 'amp', symbol: '⽊' }, { key: 'spin', symbol: '⽋' }, { key: 'drift', symbol: '⽌' }, { key: 'orbit', symbol: '⽍' }, { key: 'pulse', symbol: '⽎' }, { key: 'glow', symbol: '⽏' }, { key: 'phase', symbol: '⽐' }, { key: 'wobble', symbol: '⽑' }, { key: 'angle', symbol: '⽒' }, // ── structural idioms (mined — the top repeated shapes) ──────────────── { key: ' * 0.', symbol: '⽓' }, { key: ' * (0.', symbol: '⽔' }, { key: '(0.', symbol: '⽕' }, { key: ') * ', symbol: '⽖' }, { key: 't * ', symbol: '⽗' }, { key: '5 + ', symbol: '⽘' }, { key: ' + dt * ', symbol: '⽙' }, { key: '(a)', symbol: '⽚' }, { key: 'hue = hue + dt * ', symbol: '⽛' }, { key: 't = t + dt * ', symbol: '⽜' }, { key: ' / aspect', symbol: '⽝' }, // ── number literals (mined from the library) ─────────────────────────── { key: '6.28318', symbol: '⌇' }, // 2π, written out in most scripts { key: '3.14159', symbol: '⽣' }, { key: '3.1415', symbol: '⽤' }, { key: '0.55', symbol: '§' }, { key: '0.45', symbol: '¶' }, { key: '0.35', symbol: '※' }, { key: '0.25', symbol: '•' }, { key: '0.85', symbol: '⊢' }, { key: '0.75', symbol: '⽥' }, { key: '0.65', symbol: '⽦' }, { key: '0.95', symbol: '⽧' }, { key: '0.15', symbol: '⽨' }, { key: '0.12', symbol: '⽩' }, { key: '0.22', symbol: '⽪' }, { key: '0.42', symbol: '⽫' }, { key: '0.05', symbol: '⽬' }, { key: '0.06', symbol: '⽭' }, { key: '0.04', symbol: '⽮' }, { key: '0.07', symbol: '⽯' }, { key: '0.08', symbol: '⽰' }, { key: '0.96', symbol: '⽱' }, { key: '0.97', symbol: '⽲' }, { key: '2.09', symbol: '⽳' }, { key: '4.19', symbol: '⽴' }, { key: '0.5', symbol: '†' }, { key: '0.3', symbol: '‡' }, { key: '0.4', symbol: '⊤' }, { key: '0.7', symbol: '⊥' }, { key: '0.6', symbol: '⊣' }, { key: '0.9', symbol: '⽵' }, { key: '0.8', symbol: '⽶' }, { key: '0.2', symbol: '⽷' }, { key: '0.1', symbol: '⽸' }, { key: '1.3', symbol: '⋈' }, { key: '1.2', symbol: '⋉' }, { key: '1.0', symbol: '⽹' }, { key: '100', symbol: '⽺' }, { key: '2.5', symbol: '⽻' }, { key: '0.11', symbol: '⽼' }, { key: '0.18', symbol: '⽽' }, { key: '0.92', symbol: '⽾' }, { key: '0.99', symbol: '⽿' }, { key: '1.5', symbol: '⾀' }, { key: '0.98', symbol: '⾁' }, { key: '0.13', symbol: '⾂' }, { key: '0.14', symbol: '⾃' }, { key: '0.16', symbol: '⾄' }, { key: '0.17', symbol: '⾅' }, { key: '0.19', symbol: '⾆' }, { key: '0.21', symbol: '⾇' }, { key: '0.28', symbol: '⾈' }, { key: '0.31', symbol: '⾉' }, { key: '0.32', symbol: '⾊' }, { key: '0.33', symbol: '⾋' }, { key: '0.38', symbol: '⾌' }, { key: '0.44', symbol: '⾍' }, { key: '0.48', symbol: '⾎' }, { key: '0.52', symbol: '⾏' }, { key: '0.58', symbol: '⾐' }, { key: '0.62', symbol: '⾑' }, { key: '0.72', symbol: '⾒' }, { key: '0.78', symbol: '⾓' }, { key: '0.82', symbol: '⾔' }, { key: '0.88', symbol: '⾕' }, { key: '1.1', symbol: '⾖' }, { key: '1.4', symbol: '⾗' }, { key: '1.6', symbol: '⾘' }, { key: '1.7', symbol: '⾙' }, { key: '1.8', symbol: '⾚' }, { key: '2.0', symbol: '⾛' }, { key: '2.2', symbol: '⾜' }, { key: '2.4', symbol: '⾝' }, { key: '2.6', symbol: '⾞' }, { key: '3.0', symbol: '⾟' }, { key: '3.5', symbol: '⾠' }, { key: '0.87', symbol: '⾡' }, { key: '0.24', symbol: '⾢' }, { key: '0.26', symbol: '⾣' }, { key: '0.27', symbol: '⾤' }, { key: '0.36', symbol: '⾥' }, // ── spaced operators (arrows) ────────────────────────────────────────── { key: ' = ', symbol: '←' }, { key: ' + ', symbol: '→' }, { key: ' * ', symbol: '↑' }, { key: ' - ', symbol: '↓' }, { key: ' / ', symbol: '∙' }, { key: ', ', symbol: '⇄' }, // ── statement openers (double arrows) ────────────────────────────────── { key: 'green = ', symbol: '⇩' }, { key: 'blue = ', symbol: '⇪' }, { key: 'x = ', symbol: '⇦' }, { key: 'y = ', symbol: '⇨' }, { key: 'red = ', symbol: '⇧' }, { key: 'hue = ', symbol: '⇱' }, { key: 't = ', symbol: '⇲' }, { key: 'n = ', symbol: '⇴' }, ] /** Noise chars platforms inject into copies (VS16, zero-widths). The * encoder NEVER emits these raw — a script that legitimately contains one * gets a dedicated symbol — so the decoder can strip every raw occurrence * as injected mangling. */ const NOISE_SYMBOLS = new Map([ ['\uFE0E', '⌸'], ['\uFE0F', '⌹'], ['\u200B', '⌺'], ['\u200C', '⌻'], ['\u200D', '⌼'], ['\u2060', '⌽'], ['\uFEFF', '⌾'], ]) const NOISE_CHAR_FROM_SYMBOL = new Map( [...NOISE_SYMBOLS.entries()].map(([c, s]) => [s, c]), ) // ── derived tables (built once, self-checked by construction) ───────────── /** Longest key first, so greedy matching always prefers the longest hit. */ const DICT_SORTED = [...DICT].sort((a, b) => b.key.length - a.key.length) /** Entries grouped by first char, longest first — the encode lookup. */ const BY_FIRST_CHAR = new Map() for (const entry of DICT_SORTED) { const list = BY_FIRST_CHAR.get(entry.key[0]) if (list) list.push(entry) else BY_FIRST_CHAR.set(entry.key[0], [entry]) } /** Symbol → raw text (decode lookup). Throws on duplicate symbols — a * broken table must fail loudly at import, never corrupt a share string. */ const SYMBOL_TO_TEXT = new Map() for (const { key, symbol } of DICT) { if (SYMBOL_TO_TEXT.has(symbol)) throw new Error(`share-codec: duplicate dictionary symbol ${symbol}`) SYMBOL_TO_TEXT.set(symbol, key) } const ALL_SYMBOLS = new Set(DICT.map((e) => e.symbol)) for (const s of NOISE_SYMBOLS.values()) ALL_SYMBOLS.add(s) /** * The FROZEN bitmap list — every dictionary + noise symbol, sorted by code * point. A ▼ header says "these of the list are in this string's alphabet" * as a bitmap, 14 bits per digit character. This array is part of the ▼ * wire format: changing it breaks every ▼ string ever encoded (see the * DICT compatibility contract above). */ const BITMAP_LIST: string[] = [...ALL_SYMBOLS].sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)) const BITMAP_INDEX = new Map(BITMAP_LIST.map((s, i) => [s, i])) const DICT_BITMAP_DIGITS = Math.ceil(BITMAP_LIST.length / 14) // 14 bits per digit const ASCII_BITMAP_DIGITS = Math.ceil(128 / 14) /** * Every character that can never appear raw in a body — emitted by the * encoder only with a "\" escape in front (pair form) or folded into the * bitmaps (base form). Covers the dictionary symbols, both scheme markers, * the separator, the checksum alphabet (①–⑯), the control-picture range * (␀–␡) and the invisible characters chat platforms inject. */ const RESERVED = new Set([ESCAPE, SCHEME_V1, SCHEME_V2, SEP]) for (const s of ALL_SYMBOLS) RESERVED.add(s) for (let c = 0x2460; c <= 0x246f; c++) RESERVED.add(String.fromCharCode(c)) // ①–⑯ for (let c = 0x2400; c <= 0x2421; c++) RESERVED.add(String.fromCharCode(c)) // ␀–␡ for (const c of ['\uFE0E', '\uFE0F', '\u200B', '\u200C', '\u200D', '\u2060', '\uFEFF']) { RESERVED.add(c) } const PLATFORM_NOISE = /[\uFE0E\uFE0F\u200B\u200C\u200D\u2060\uFEFF]/g /** Whitespace or platform noise — safe to shave off the string's edges. */ function isEdgeTrimable(c: string): boolean { return c === ' ' || c === '\n' || c === '\r' || c === '\t' || PLATFORM_NOISE.test(c) } // ── pair packing (shared by the ▼ fallback form and legacy ▲ strings) ────── /** * Two adjacent characters from this 64-char alphabet fold into ONE code * point at PAIR_BASE + 64·a + b (a common-CJK block — assigned since * Unicode 1.1, text presentation, renders everywhere, copy/paste-safe). * The block is reserved, so CJK that legitimately appears in a script is * escaped and still round-trips. */ const PAIR_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_.' const PAIR_BASE = 0x4e00 const PAIR_END = PAIR_BASE + PAIR_ALPHABET.length * PAIR_ALPHABET.length - 1 // 0x5dff const PAIR_INDEX = new Map() for (let i = 0; i < PAIR_ALPHABET.length; i++) PAIR_INDEX.set(PAIR_ALPHABET[i], i) for (let c = PAIR_BASE; c <= PAIR_END; c++) RESERVED.add(String.fromCharCode(c)) // ── big-base packing ─────────────────────────────────────────────────────── /** * The digit alphabet for the base form: CJK Unified Ideographs (20,992) + * Hangul syllables (11,172) = 32,164 digits — every one assigned, text * presentation, in every CJK-capable font, and copy/paste-safe. 14.98 * bits per character, vs 5.95 for classic base-62. */ const DIGIT_BLOCKS: Array<[number, number]> = [ [0x4e00, 0x9fff], // CJK Unified Ideographs [0xac00, 0xd7a3], // Hangul syllables ] const DIGIT_BLOCK0_SIZE = DIGIT_BLOCKS[0][1] - DIGIT_BLOCKS[0][0] + 1 const BIG_BASE = DIGIT_BLOCK0_SIZE + (DIGIT_BLOCKS[1][1] - DIGIT_BLOCKS[1][0] + 1) // 32164 const BIG_BASE_N = BigInt(BIG_BASE) const BITS_PER_DIGIT = 14 // floor(log2(32164)) -- bitmap digits carry 14 bits function digitChar(d: number): string { return d < DIGIT_BLOCK0_SIZE ? String.fromCharCode(DIGIT_BLOCKS[0][0] + d) : String.fromCharCode(DIGIT_BLOCKS[1][0] + (d - DIGIT_BLOCK0_SIZE)) } /** Digit char → value, or -1 when the char isn't a digit. */ function digitValue(c: string): number { const cc = c.charCodeAt(0) if (cc >= DIGIT_BLOCKS[0][0] && cc <= DIGIT_BLOCKS[0][1]) return cc - DIGIT_BLOCKS[0][0] if (cc >= DIGIT_BLOCKS[1][0] && cc <= DIGIT_BLOCKS[1][1]) return cc - DIGIT_BLOCKS[1][0] + DIGIT_BLOCK0_SIZE return -1 } /** Bits → exactly `digits` digit chars (left-padded with digit 0). */ function bitsToDigits(bits: boolean[], digits: number): string { let value = 0n for (const b of bits) value = value * 2n + (b ? 1n : 0n) const ds: number[] = [] let v = value while (v > 0n) { ds.push(Number(v % BIG_BASE_N)) v /= BIG_BASE_N } while (ds.length < digits) ds.push(0) if (ds.length > digits) throw new Error('share-codec: bitmap overflow') let out = '' for (let k = ds.length - 1; k >= 0; k--) out += digitChar(ds[k]) return out } /** The first `count` digit chars of `s` → bits (big-endian, `bits` long). */ function digitsToBits(s: string, count: number, bits: number): boolean[] { let value = 0n for (let i = 0; i < count; i++) { const d = digitValue(s[i]) if (d < 0) throw new Error('share-codec: bad bitmap digit') value = value * BIG_BASE_N + BigInt(d) } const out: boolean[] = [] for (let i = bits - 1; i >= 0; i--) out.push((value >> BigInt(i)) & 1n ? true : false) return out } /** * Largest body (in symbols) the big-base packer will accept — packing is * O(n²) in BigInt arithmetic, and 16,000 symbols packs in ~150 ms while a * 50,000-symbol body would take seconds. Longer scripts fall back to the * linear pair form (still ~55% shorter). */ const PACK_SYMBOL_LIMIT = 16_000 /** * Pack a dictionary body into the base form: * * 〠 * * The payload is the whole body as one big number (a leading sentinel * symbol — the alphabet's last — guarantees leading-alphabet-symbol zeros * survive). The header rebuilds the body's alphabet for the decoder: which * frozen dictionary symbols are used (bitmap), which ASCII chars are used * (bitmap), and any remaining non-ASCII chars verbatim. */ function packBase(body: string): string | null { const n = body.length if (n < 2 || n > PACK_SYMBOL_LIMIT) return null const used = new Set() for (let i = 0; i < n; i++) used.add(body[i]) const alphabet = [...used].sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)) const m = alphabet.length if (m < 2) return null const indexOf = new Map() alphabet.forEach((s, idx) => indexOf.set(s, idx)) // value = sentinel·m^n + body as a base-m number let value = BigInt(m - 1) for (let i = 0; i < n; i++) value = value * BigInt(m) + BigInt(indexOf.get(body[i])!) let payload = '' const ds: number[] = [] let v = value while (v > 0n) { ds.push(Number(v % BIG_BASE_N)) v /= BIG_BASE_N } for (let k = ds.length - 1; k >= 0; k--) payload += digitChar(ds[k]) // header bitmaps const dictBits: boolean[] = [] for (const s of BITMAP_LIST) dictBits.push(used.has(s)) const asciiBits: boolean[] = [] for (let c = 0; c < 128; c++) asciiBits.push(used.has(String.fromCharCode(c))) let tail = '' for (const s of alphabet) { const cc = s.charCodeAt(0) if (cc >= 128 && !BITMAP_INDEX.has(s)) tail += s } return ( payload + SEP + bitsToDigits(dictBits, DICT_BITMAP_DIGITS) + bitsToDigits(asciiBits, ASCII_BITMAP_DIGITS) + tail ) } /** Inverse of packBase — returns null on any structural violation. */ function unpackBase(packed: string): string | null { // first bare SEP splits payload | header (payload digits are CJK/Hangul, // so no digit can ever be mistaken for the separator) let sepIdx = -1 for (let i = 0; i < packed.length; i++) { if (packed[i] === SEP) { sepIdx = i break } } if (sepIdx < 1 || sepIdx === packed.length - 1) return null let value = 0n for (let i = 0; i < sepIdx; i++) { const d = digitValue(packed[i]) if (d < 0) return null value = value * BIG_BASE_N + BigInt(d) } if (value <= 0n) return null const header = packed.slice(sepIdx + 1) if (header.length < DICT_BITMAP_DIGITS + ASCII_BITMAP_DIGITS) return null let dictBits: boolean[] let asciiBits: boolean[] try { dictBits = digitsToBits(header, DICT_BITMAP_DIGITS, BITMAP_LIST.length) asciiBits = digitsToBits(header.slice(DICT_BITMAP_DIGITS), ASCII_BITMAP_DIGITS, 128) } catch { return null } const tail = header.slice(DICT_BITMAP_DIGITS + ASCII_BITMAP_DIGITS) // rebuild the alphabet: frozen symbols + ASCII + verbatim tail, sorted const alphabet: string[] = [] dictBits.forEach((on, i) => { if (on) alphabet.push(BITMAP_LIST[i]) }) asciiBits.forEach((on, c) => { if (on) alphabet.push(String.fromCharCode(c)) }) const seen = new Set(alphabet) for (let i = 0; i < tail.length; i++) { const c = tail[i] if (seen.has(c)) return null // tail must be deduped seen.add(c) alphabet.push(c) } alphabet.sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)) const m = alphabet.length if (m < 2) return null const M = BigInt(m) const idxs: number[] = [] while (value > 0n) { idxs.push(Number(value % M)) value /= M } let s = '' for (let k = idxs.length - 1; k >= 0; k--) { const idx = idxs[k] if (idx >= m) return null s += alphabet[idx] } if (s.length < 1) return null return s.slice(1) // strip the sentinel } // ── checksum ────────────────────────────────────────────────────────────── /** FNV-1a 32-bit over the UTF-16 code units — fast, tiny, plenty here. */ function fnv1a(s: string): number { let h = 0x811c9dc5 for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i) h = Math.imul(h, 0x01000193) >>> 0 } return h >>> 0 } /** Encode a 32-bit digest as 8 "circled digit" hex chars (① = 0 … ⑯ = f). */ function checkDigits(value: number): string { let out = '' for (let shift = 28; shift >= 0; shift -= 4) { out += String.fromCharCode(0x2460 + ((value >>> shift) & 0xf)) } return out } function isCheckDigit(c: string): boolean { const cc = c.charCodeAt(0) return cc >= 0x2460 && cc <= 0x246f } // ── name label ──────────────────────────────────────────────────────────── /** Pull the `name:` header out of a script (before the first [section]). */ function extractScriptName(script: string): string { for (const line of script.split('\n')) { const t = line.trim() if (t.startsWith('[')) break const m = /^name\s*:\s*(.*)$/i.exec(t) if (m) { let v = m[1].trim() if ( v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) ) { v = v.slice(1, -1).trim() } if (v) return v } } return '' } /** Make the label safe for the `Name:[…]` wrapper (no : [ ] or newlines). */ function sanitizeShareName(raw: string): string { const cleaned = raw .replace(/[\[\]:\r\n\t]+/g, ' ') .replace(/\s+/g, ' ') .trim() .slice(0, 48) .trim() return cleaned || 'OVE' } // ── encode ──────────────────────────────────────────────────────────────── /** Substitute the dictionary into the script text (escaping as needed). */ function encodeDictBody(script: string): string { let out = '' let i = 0 const n = script.length while (i < n) { const c = script[i] // legitimate noise chars → their dedicated symbols (never raw) const noiseSym = NOISE_SYMBOLS.get(c) if (noiseSym !== undefined) { out += noiseSym i++ continue } // reserved / escape char → escaped literal if (c === ESCAPE || RESERVED.has(c)) { out += ESCAPE + c i++ continue } // dictionary match (longest first) — also folds \n \r \t into symbols const entries = BY_FIRST_CHAR.get(c) if (entries) { let matched = false for (const e of entries) { if (script.startsWith(e.key, i)) { out += e.symbol i += e.key.length matched = true break } } if (matched) continue } // other control characters → Unicode control pictures (copy-safe) const cc = c.charCodeAt(0) if (cc < 0x20 || cc === 0x7f) { out += cc === 0x7f ? '\u2421' : String.fromCharCode(0x2400 + cc) i++ continue } out += c i++ } return out } /** Dictionary + pair packing interleaved (the fallback form, v7.7-style). */ function encodePairBody(script: string): string { let out = '' let i = 0 const n = script.length while (i < n) { const c = script[i] const noiseSym = NOISE_SYMBOLS.get(c) if (noiseSym !== undefined) { out += noiseSym i++ continue } if (c === ESCAPE || RESERVED.has(c)) { out += ESCAPE + c i++ continue } const entries = BY_FIRST_CHAR.get(c) if (entries) { let matched = false for (const e of entries) { if (script.startsWith(e.key, i)) { out += e.symbol i += e.key.length matched = true break } } if (matched) continue } const cc = c.charCodeAt(0) if (cc < 0x20 || cc === 0x7f) { out += cc === 0x7f ? '\u2421' : String.fromCharCode(0x2400 + cc) i++ continue } // pair packing — two adjacent [0-9a-zA-Z_.] chars → one CJK code point const ai = PAIR_INDEX.get(c) if (ai !== undefined) { const bi = PAIR_INDEX.get(script[i + 1]) if (bi !== undefined) { out += String.fromCharCode(PAIR_BASE + ai * PAIR_ALPHABET.length + bi) i += 2 continue } } out += c i++ } return out } export interface ScriptHash { /** The full share string: `Name:[▼…①①①…]`. */ hash: string /** The label used in the prefix (sanitized). */ name: string /** script.length (UTF-16 units) — for the stats line. */ scriptLength: number /** hash.length — for the stats line. */ hashLength: number } /** * Encode a script into its share string. Never throws, never validates the * script — sharing a script that doesn't compile is a feature (ask for help, * get it debugged). `name` overrides the label; by default the `name:` * header is used, falling back to "OVE". */ export function encodeScriptHash(script: string, name?: string): ScriptHash { const label = sanitizeShareName(name ?? extractScriptName(script)) const dictBody = encodeDictBody(script) const packed = packBase(dictBody) let body: string if (packed !== null) { const pairBody = encodePairBody(script) body = packed.length < pairBody.length ? packed : pairBody } else { body = encodePairBody(script) } const hash = `${label}:[${SCHEME_V2}${body}${checkDigits(fnv1a(script))}]` return { hash, name: label, scriptLength: script.length, hashLength: hash.length } } // ── decode ──────────────────────────────────────────────────────────────── /** Expand a dictionary body back into the original text. */ function decodeDictBody(body: string): string { let out = '' for (let i = 0; i < body.length; i++) { const c = body[i] if (c === ESCAPE) { const next = body[i + 1] if (next !== undefined) { out += next i++ } else { out += c // trailing lone escape -- be lenient } continue } const cc = c.charCodeAt(0) // control pictures → the control chars they stand for if (cc >= 0x2400 && cc <= 0x2420) { out += String.fromCharCode(cc - 0x2400) continue } if (cc === 0x2421) { out += '\u007f' continue } // dictionary symbols → raw text const expansion = SYMBOL_TO_TEXT.get(c) if (expansion !== undefined) { out += expansion continue } // legitimate noise symbols → their original chars const noiseChar = NOISE_CHAR_FROM_SYMBOL.get(c) if (noiseChar !== undefined) { out += noiseChar continue } out += c } return out } /** Expand a pair-form body (dictionary + pair slots) into the script. */ function decodePairBody(body: string): string { let out = '' for (let i = 0; i < body.length; i++) { const c = body[i] if (c === ESCAPE) { const next = body[i + 1] if (next !== undefined) { out += next i++ } else { out += c } continue } const cc = c.charCodeAt(0) if (cc >= 0x2400 && cc <= 0x2420) { out += String.fromCharCode(cc - 0x2400) continue } if (cc === 0x2421) { out += '\u007f' continue } const expansion = SYMBOL_TO_TEXT.get(c) if (expansion !== undefined) { out += expansion continue } const noiseChar = NOISE_CHAR_FROM_SYMBOL.get(c) if (noiseChar !== undefined) { out += noiseChar continue } if (cc >= PAIR_BASE && cc <= PAIR_END) { const k = cc - PAIR_BASE out += PAIR_ALPHABET[Math.floor(k / PAIR_ALPHABET.length)] + PAIR_ALPHABET[k % PAIR_ALPHABET.length] continue } out += c } return out } // ── legacy ▲ support (v7.7 strings — decode only) ────────────────────────── const LEGACY_DICT: DictEntry[] = [ { key: '\n', symbol: '♤' }, { key: ' ', symbol: '♦' }, { key: 'beats', symbol: '♡' }, { key: 'beatage', symbol: '♠' }, { key: '\t', symbol: '♣' }, { key: ' ', symbol: '▣' }, { key: ' ', symbol: '·' }, { key: '\r', symbol: '⏎' }, { key: '[init]', symbol: '♩' }, { key: '[frame]', symbol: '♪' }, { key: '[beat]', symbol: '♫' }, { key: '[point]', symbol: '♬' }, { key: '# ', symbol: '♯' }, { key: '#', symbol: '♭' }, { key: '//', symbol: '♮' }, { key: 'name', symbol: '♔' }, { key: 'author', symbol: '♕' }, { key: 'description', symbol: '♖' }, { key: 'mode', symbol: '♗' }, { key: 'blend', symbol: '♘' }, { key: 'persist', symbol: '♙' }, { key: 'linewidth', symbol: '♚' }, { key: 'ysign', symbol: '♛' }, { key: 'lines', symbol: '♈' }, { key: 'dots', symbol: '♉' }, { key: 'solid', symbol: '♊' }, { key: 'additive', symbol: '♋' }, { key: 'alpha', symbol: '♌' }, { key: ' = ', symbol: '←' }, { key: ' + ', symbol: '→' }, { key: ' * ', symbol: '↑' }, { key: ' - ', symbol: '↓' }, { key: ', ', symbol: '⇄' }, { key: 'x = ', symbol: '⇦' }, { key: 'y = ', symbol: '⇨' }, { key: 'red = ', symbol: '⇧' }, { key: 'green = ', symbol: '⇩' }, { key: 'blue = ', symbol: '⇪' }, { key: 'hue = ', symbol: '⇱' }, { key: 't = ', symbol: '⇲' }, { key: 'n = ', symbol: '⇴' }, { key: 'bass_att', symbol: '♎' }, { key: 'mid_att', symbol: '♏' }, { key: 'treb_att', symbol: '♐' }, { key: 'bass', symbol: '♑' }, { key: 'mid', symbol: '♒' }, { key: 'treb', symbol: '♓' }, { key: 'vol', symbol: '☽' }, { key: 'beat', symbol: '◉' }, { key: 'red', symbol: '☾' }, { key: 'green', symbol: '☿' }, { key: 'blue', symbol: '⚑' }, { key: 'skip', symbol: '⚐' }, { key: 'aspect', symbol: '⚶' }, { key: 'dt', symbol: '◐' }, { key: 'pi', symbol: '◑' }, { key: 'tau', symbol: '◒' }, { key: 'smoothstep', symbol: '⌒' }, { key: 'gmegabuf', symbol: '✥' }, { key: 'getkbmouse', symbol: '◶' }, { key: 'megabuf', symbol: '✤' }, { key: 'getspec', symbol: '▤' }, { key: 'getosc', symbol: '▥' }, { key: 'gettime', symbol: '◷' }, { key: 'atan2', symbol: '⌗' }, { key: 'log10', symbol: '㏑' }, { key: 'invsqrt', symbol: '√' }, { key: 'sigmoid', symbol: '∿' }, { key: 'inrange', symbol: '⋔' }, { key: 'hsvr', symbol: '⬥' }, { key: 'hsvg', symbol: '⬦' }, { key: 'hsvb', symbol: '⬧' }, { key: 'above', symbol: '≻' }, { key: 'below', symbol: '≺' }, { key: 'asin', symbol: '⌠' }, { key: 'acos', symbol: '⌡' }, { key: 'atan', symbol: '⌐' }, { key: 'ceil', symbol: '⌈' }, { key: 'floor', symbol: '⌊' }, { key: 'sqr', symbol: '□' }, { key: 'sqrt', symbol: '∛' }, { key: 'pow', symbol: '⌃' }, { key: 'exp', symbol: 'ℯ' }, { key: 'log', symbol: '㏒' }, { key: 'round', symbol: '◎' }, { key: 'int', symbol: '⊺' }, { key: 'frac', symbol: '∵' }, { key: 'sign', symbol: '±' }, { key: 'bnot', symbol: '¬' }, { key: 'band', symbol: '∧' }, { key: 'bor', symbol: '∨' }, { key: 'equal', symbol: '≟' }, { key: 'lerp', symbol: '↝' }, { key: 'clamp', symbol: '⊜' }, { key: 'rand', symbol: '⚄' }, { key: 'rnd', symbol: '⚅' }, { key: 'sin', symbol: '◠' }, { key: 'cos', symbol: '◡' }, { key: 'tan', symbol: '◮' }, { key: 'abs', symbol: '¦' }, { key: 'min', symbol: '≪' }, { key: 'max', symbol: '≫' }, { key: 'if', symbol: '∴' }, { key: 'loop', symbol: '↻' }, { key: '6.28318', symbol: '⌇' }, { key: '0.5', symbol: '†' }, { key: '0.3', symbol: '‡' }, { key: '0.25', symbol: '•' }, { key: '0.55', symbol: '§' }, { key: '0.45', symbol: '¶' }, { key: '0.35', symbol: '※' }, { key: '0.4', symbol: '⊤' }, { key: '0.7', symbol: '⊥' }, { key: '0.85', symbol: '⊢' }, { key: '0.6', symbol: '⊣' }, { key: '1.3', symbol: '⋈' }, { key: '1.2', symbol: '⋉' }, { key: ' / ', symbol: '∙' }, { key: 'hue', symbol: '⋊' }, { key: 'rad', symbol: '⋅' }, { key: 'flash', symbol: '∘' }, ] const LEGACY_SYMBOL_TO_TEXT = new Map() for (const { key, symbol } of LEGACY_DICT) LEGACY_SYMBOL_TO_TEXT.set(symbol, key) for (const [c, s] of NOISE_SYMBOLS) LEGACY_SYMBOL_TO_TEXT.set(s, c) /** Expand a v7.7 ▲ body. */ function decodeLegacyBody(body: string): string { let out = '' for (let i = 0; i < body.length; i++) { const c = body[i] if (c === ESCAPE) { const next = body[i + 1] if (next !== undefined) { out += next i++ } else { out += c } continue } const cc = c.charCodeAt(0) if (cc >= 0x2400 && cc <= 0x2420) { out += String.fromCharCode(cc - 0x2400) continue } if (cc === 0x2421) { out += '\u007f' continue } const expansion = LEGACY_SYMBOL_TO_TEXT.get(c) if (expansion !== undefined) { out += expansion continue } if (cc >= PAIR_BASE && cc <= PAIR_END) { const k = cc - PAIR_BASE out += PAIR_ALPHABET[Math.floor(k / PAIR_ALPHABET.length)] + PAIR_ALPHABET[k % PAIR_ALPHABET.length] continue } out += c } return out } // ── decode entry point ──────────────────────────────────────────────────── export type HashDecodeResult = | { ok: true; script: string; name: string | null } | { ok: false; error: string } /** * Decode a pasted share string back into the original script. Accepts the * full `Name:[…]` form or a bare `[…]` body, ▼ (current) and ▲ (v7.7) * schemes; strips the invisible characters platforms sprinkle into copies; * and verifies the check digits so a truncated or altered string is * rejected instead of decoding into a corrupted script. */ export function decodeScriptHash(input: string): HashDecodeResult { if (input.length > 40_000) { return { ok: false, error: 'That text is far too long to be a share string.' } } // Trim whitespace AND stray noise from the edges (the encoder never emits // either raw, so edge removal is always safe). let text = input while (text.length > 0 && isEdgeTrimable(text[0])) text = text.slice(1) while (text.length > 0 && isEdgeTrimable(text[text.length - 1])) text = text.slice(0, -1) if ( text.length >= 2 && ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) ) { text = text.slice(1, -1) while (text.length > 0 && isEdgeTrimable(text[text.length - 1])) text = text.slice(0, -1) } // Any raw noise left is injected mangling (legit noise is symbol-coded), // so it can be stripped everywhere without ever eating original content. text = text.replace(PLATFORM_NOISE, '') // `Name:[▼…]`, `[▼…]`, `Name:[▲…]` or `[▲…]` let payload: string | null = null let name: string | null = null const full = /^([\s\S]*?):\[([▼▲][\s\S]*)\]$/.exec(text) if (full) { name = full[1].trim() || null payload = full[2] } else if (/^\[([▼▲][\s\S]*)\]$/.test(text)) { payload = text.slice(1, -1) } if (payload === null) { // a string that still shows the "…:[▼/▲" signature was cut off // somewhere after the opening — a character limit ate the end const looksCutOff = /:\[[▼▲]/.test(text) || /^\[[▼▲]/.test(text) return { ok: false, error: looksCutOff ? 'This share string looks cut off -- it should end with "]". Copy the whole string and try again.' : 'This does not look like an OVE share string -- it should read like "My Script:[▼…]".', } } // trailing 8 check digits if (payload.length < 9 || !isCheckDigit(payload[payload.length - 8])) { return { ok: false, error: 'This share string looks cut off -- the 8 check digits at its end are missing. Copy the whole string and try again.', } } const digits = payload.slice(-8) for (const d of digits) { if (!isCheckDigit(d)) { return { ok: false, error: 'This share string looks cut off -- the 8 check digits at its end are missing. Copy the whole string and try again.', } } } const marker = payload[0] const body = payload.slice(1, -8) let script: string if (marker === SCHEME_V2) { // base form ⇔ the body contains a bare (unescaped) 〠 — the separator // between payload digits and the bitmap header. The pair form escapes // every 〠, and base-form payload digits can never be one. let hasBareSep = false for (let i = 0; i < body.length; i++) { if (body[i] === ESCAPE) i++ else if (body[i] === SEP) { hasBareSep = true break } } if (hasBareSep) { const dictBody = unpackBase(body) if (dictBody === null) { return { ok: false, error: 'This share string is damaged -- its packing header does not parse. Copy the whole string and try again.', } } script = decodeDictBody(dictBody) } else { script = decodePairBody(body) } } else { script = decodeLegacyBody(body) } if (checkDigits(fnv1a(script)) !== digits) { return { ok: false, error: 'The check digits do not match -- the string was altered or cut off while copying. Copy the whole string and try again.', } } return { ok: true, script, name } } // ── legend for the UI ───────────────────────────────────────────────────── /** A few representative symbol meanings, shown in the share dialogs. */ export const SHARE_LEGEND: ReadonlyArray<{ symbol: string; meaning: string }> = [ { symbol: '♤', meaning: 'line return' }, { symbol: '♦', meaning: 'space' }, { symbol: '♡', meaning: 'beats' }, { symbol: '♠', meaning: 'beatage' }, { symbol: '一', meaning: 'one packed digit -- 15 bits of script' }, ]