torn-crack

Simple Cracking Helper

当前为 2025-09-21 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         torn-crack
// @namespace    torn-crack
// @version      0.0.9
// @description  Simple Cracking Helper
// @author       SirAua [3785905]
// @match        *://www.torn.com/page.php?sid=crimes*
// @grant        GM_xmlhttpRequest
// @connect      gitlab.com
// @connect      supabase.co
// @connect      *.supabase.co
// @license      mit
// ==/UserScript==

// 🔴Clear the Wordlist Cache🔴

(function () {
    'use strict';

    if (window.CRACK_INJECTED) return;
    window.CRACK_INJECTED = true;

    /* --------------------------
       Config / Constants
       -------------------------- */
    const debug = true;
    const UPDATE_INTERVAL = 800;
    const MAX_SUG = 8;
    const MIN_LENGTH = 4;
    const MAX_LENGTH = 10;

    const WORDLIST_URL =
        'https://gitlab.com/kalilinux/packages/seclists/-/raw/kali/master/Passwords/Common-Credentials/Pwdb_top-1000000.txt?ref_type=heads';
    const BIG_WORDLIST_URL =
        'https://gitlab.com/kalilinux/packages/seclists/-/raw/kali/master/Passwords/Common-Credentials/Pwdb_top-10000000.txt?ref_type=heads';

    const DOWNLOAD_MIN_DELTA = 20;

    const SUPABASE_COUNT_URL =
        'https://mthndavliqfbtaplgfau.supabase.co/functions/v1/get-words/count';
    const SUPABASE_WORDS_URL =
        'https://mthndavliqfbtaplgfau.supabase.co/functions/v1/get-words/words';
    const SUPABASE_ADD_WORD_URL =
        'https://mthndavliqfbtaplgfau.supabase.co/functions/v1/add-word';

    const DB_NAME = 'crack';
    const STORE_NAME = 'dictionary';
    const STATUS_PREF_KEY = 'crack_show_badge';
    const BASE_LIST_PREF_KEY = 'crack_base_wordlist'; // '1M' | '10M'
    const EXCL_STORAGE_PREFIX = 'crack_excl_';

    const CRACK_10M_CHUNK_MB = Number(localStorage.getItem('crack_10m_chunk_mb')) || 4;
    const CRACK_10M_CONCURRENCY = Number(localStorage.getItem('crack_10m_conc')) || 4;
    const CRACK_10M_COMMIT_LINES = Number(localStorage.getItem('crack_10m_commit_lines')) || 200000;
    const CRACK_10M_PAUSE_MS = Number(localStorage.getItem('crack_10m_pause_ms')) || 5;

    /* --------------------------
       State
       -------------------------- */
    let dict = [];
    let dictLoaded = false;
    let dictLoading = false;
    let supabaseWords = new Set();
    let statusEl = null;
    const prevRowStates = new Map();
    const panelUpdateTimers = new Map();

    const LAST_INPUT = { key: null, time: 0 };

    /* --------------------------
       Utilities
       -------------------------- */
    function crackLog(...args) { if (debug) console.log('[Crack]', ...args); }
    function getBoolPref(key, def = true) {
        const v = localStorage.getItem(key); return v === null ? def : v === '1';
    }
    function setBoolPref(key, val) { localStorage.setItem(key, val ? '1' : '0'); }

    function getBaseWordlistPref() {
        const v = localStorage.getItem(BASE_LIST_PREF_KEY);
        return v === '10M' ? '10M' : '1M';
    }
    function setBaseWordlistPref(v) { localStorage.setItem(BASE_LIST_PREF_KEY, v === '10M' ? '10M' : '1M'); }

    function ensureStatusBadge() {
        if (statusEl) return statusEl;
        statusEl = document.createElement('div');
        statusEl.id = '__crack_status';
        statusEl.style.cssText = `
      position: fixed; right: 10px; bottom: 40px; z-index: 10000;
      background:#000; color:#0f0; border:1px solid #0f0; border-radius:6px;
      padding:6px 8px; font-size:11px; font-family:monospace; opacity:0.9;
    `;
        statusEl.textContent = 'Dictionary: Idle';
        document.body.appendChild(statusEl);
        const show = getBoolPref(STATUS_PREF_KEY, true);
        statusEl.style.display = show ? 'block' : 'none';
        return statusEl;
    }
    const __statusSinks = new Set();
    function registerStatusSink(el) { if (el) __statusSinks.add(el); }
    function unregisterStatusSink(el) { if (el) __statusSinks.delete(el); }
    function setStatus(msg) {
        const text = `Dictionary: ${msg}`;
        const badge = ensureStatusBadge();
        if (badge.textContent !== text) badge.textContent = text;
        __statusSinks.forEach(el => { if (el && el.textContent !== text) el.textContent = text; });
        crackLog('STATUS →', msg);
    }
    function gmRequest(opts) {
        return new Promise((resolve, reject) => {
            try {
                GM_xmlhttpRequest({ ...opts, onload: resolve, onerror: reject, ontimeout: reject });
            } catch (err) { reject(err); }
        });
    }
    function getHeader(headers, name) {
        const re = new RegExp('^' + name + ':\\s*(.*)$', 'mi');
        const m = headers && headers.match ? headers.match(re) : null;
        return m ? m[1].trim() : null;
    }

    /* --------------------------
       IndexedDB helpers
       -------------------------- */
    function openDB() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(DB_NAME, 1);
            request.onupgradeneeded = () => {
                const db = request.result;
                if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME);
            };
            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error);
        });
    }
    async function idbSet(key, value) {
        const db = await openDB();
        return new Promise((resolve, reject) => {
            const tx = db.transaction(STORE_NAME, 'readwrite');
            tx.objectStore(STORE_NAME).put(value, key);
            tx.oncomplete = resolve; tx.onerror = () => reject(tx.error);
        });
    }
    async function idbGet(key) {
        const db = await openDB();
        return new Promise((resolve, reject) => {
            const tx = db.transaction(STORE_NAME, 'readonly');
            const req = tx.objectStore(STORE_NAME).get(key);
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }
    async function idbClear() {
        const db = await openDB();
        return new Promise((resolve, reject) => {
            const tx = db.transaction(STORE_NAME, 'readwrite');
            tx.objectStore(STORE_NAME).clear();
            tx.oncomplete = resolve; tx.onerror = () => reject(tx.error);
        });
    }
    async function clearLocalDictCache() {
        await idbClear();
        crackLog('Cleared cached dictionary from IndexedDB');
        setStatus('Cleared cache — reload');
    }

    /* --------------------------
       Key capture
       -------------------------- */
    function captureKey(k) {
        if (!k) return;
        const m = String(k).match(/^[A-Za-z0-9._]$/);
        if (!m) return;
        LAST_INPUT.key = k.toUpperCase();
        LAST_INPUT.time = performance.now();
    }
    window.addEventListener('keydown', (e) => {
        if (e.metaKey || e.ctrlKey || e.altKey) return;
        captureKey(e.key);
    }, true);

    async function commitBucketsToIDB(buckets) {
        for (const lenStr of Object.keys(buckets)) {
            const L = Number(lenStr);
            const newArr = Array.from(buckets[lenStr]);
            let existing = await idbGet(`len_${L}`);
            if (!existing) existing = [];
            const merged = Array.from(new Set([...existing, ...newArr]));
            await idbSet(`len_${L}`, merged);
            dict[L] = merged;
        }
    }

    async function fetchAndIndex1M(url, onProgress) {
        setStatus('Downloading base wordlist (1M)…');
        const res = await gmRequest({ method: 'GET', url, timeout: 30000, responseType: 'text' });
        setStatus('Indexing 1M…');

        const lines = (res.responseText || '').split(/\r?\n/);
        const buckets = {};
        let processed = 0;

        for (const raw of lines) {
            processed++;
            const word = (raw || '').trim().toUpperCase();
            if (!word) continue;
            if (!/^[A-Z0-9_.]+$/.test(word)) continue;
            const L = word.length;
            if (L < MIN_LENGTH || L > MAX_LENGTH) continue;
            if (!buckets[L]) buckets[L] = new Set();
            buckets[L].add(word);

            if (processed % 5000 === 0 && typeof onProgress === 'function') {
                onProgress({ phase: '1M-index', processed, pct: null });
                await new Promise(r => setTimeout(r, 0));
            }
        }

        await commitBucketsToIDB(buckets);

        const perLengthCounts = {};
        for (let L = MIN_LENGTH; L <= MAX_LENGTH; L++) {
            perLengthCounts[L] = (await idbGet(`len_${L}`))?.length || 0;
        }
        setStatus('1M cached');
        return { totalProcessed: processed, perLengthCounts };
    }

    async function bigDownloadStream(url, onProgress) {
        setStatus('Preparing 10M download…');
        const { len: totalSize, ranges } = await bigHead(url);
        if (!ranges || !totalSize) {
            return singleThreadFallback(url, onProgress);
        }

        const CHUNK = (CRACK_10M_CHUNK_MB > 0 ? CRACK_10M_CHUNK_MB : 4) * 1024 * 1024; // bytes
        const CONC = Math.max(1, Math.min(8, CRACK_10M_CONCURRENCY));
        const COMMIT_EVERY = Math.max(50000, CRACK_10M_COMMIT_LINES);
        const PAUSE_MS = Math.max(0, CRACK_10M_PAUSE_MS);

        const stage = {};
        for (let L = MIN_LENGTH; L <= MAX_LENGTH; L++) stage[L] = new Set();

        let processed = 0;
        let bytesRead = 0;
        let stagedSinceCommit = 0;

        const wordRe = /^[A-Z0-9_.]+$/;
        const decoder = new TextDecoder('utf-8');

        const commitNow = async () => {
            const buckets = {};
            for (let L = MIN_LENGTH; L <= MAX_LENGTH; L++) {
                if (stage[L].size) buckets[L] = stage[L];
            }
            if (Object.keys(buckets).length) {
                await commitBucketsToIDB(buckets);
                for (let L = MIN_LENGTH; L <= MAX_LENGTH; L++) stage[L].clear();
                stagedSinceCommit = 0;
                if (typeof onProgress === 'function') {
                    const pct = Math.min(100, Math.round((bytesRead / totalSize) * 100));
                    onProgress({ phase: '10M-commit', processed, pct });
                }
            }
        };

        async function worker(i) {
            let offset = i * CHUNK;
            let partial = '';
            while (offset < totalSize) {
                const start = offset;
                const end = Math.min(totalSize - 1, start + CHUNK - 1);
                const res = await bigGetRangeArrayBuffer(url, start, end);
                if (!res || res.status >= 400) throw new Error(`10M download error (${res ? res.status : 'n/a'})`);

                const buf = res.response;
                const text = decoder.decode(buf);
                bytesRead += (end - start + 1);

                const full = partial + text;
                const lines = full.split(/\r?\n/);
                partial = lines.pop() || '';

                for (let raw of lines) {
                    raw = raw ? raw.trim().toUpperCase() : '';
                    if (!raw) continue;
                    if (!wordRe.test(raw)) continue;
                    const L = raw.length;
                    if (L < MIN_LENGTH || L > MAX_LENGTH) continue;
                    stage[L].add(raw);
                    processed++;
                    stagedSinceCommit++;
                }

                if (stagedSinceCommit >= COMMIT_EVERY) {
                    await commitNow();
                }

                if (typeof onProgress === 'function') {
                    const pct = Math.min(100, Math.round((bytesRead / totalSize) * 100));
                    onProgress({ phase: '10M-stream', processed, pct });
                }

                if (PAUSE_MS) await new Promise(r => setTimeout(r, PAUSE_MS));
                offset += CONC * CHUNK;
            }

            if (partial) {
                const w = partial.trim().toUpperCase();
                if (w && wordRe.test(w) && w.length >= MIN_LENGTH && w.length <= MAX_LENGTH) {
                    stage[w.length].add(w);
                    processed++;
                    stagedSinceCommit++;
                }
            }
        }

        const workers = [];
        for (let i = 0; i < CONC; i++) workers.push(worker(i));
        await Promise.all(workers);

        await commitNow();

        setStatus('10M download finished');
        return { totalProcessed: processed };
    }

    async function bigGetRangeArrayBuffer(url, start, end) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                headers: { Range: `bytes=${start}-${end}` },
                responseType: 'arraybuffer',
                onload: (r) => resolve(r),
                onerror: (r) => resolve(r),
                ontimeout: (r) => resolve(r),
            });
        });
    }

    async function singleThreadFallback(url, onProgress) {
        return { totalProcessed: 0 };
    }

    async function downloadBaseWordlist(mode = '1M', onProgress) {
        mode = mode === '10M' ? '10M' : '1M';
        try {
            if (typeof onProgress === 'function') onProgress({ phase: 'start', mode });
            if (mode === '1M') {
                const stats = await fetchAndIndex1M(WORDLIST_URL, onProgress);
                if (typeof onProgress === 'function') onProgress({ phase: 'done', mode, stats });
                return { mode: '1M', stats };
            } else {
                const stats = await bigDownloadStream(BIG_WORDLIST_URL, onProgress);
                if (typeof onProgress === 'function') onProgress({ phase: 'done', mode, stats });
                return { mode: '10M', stats };
            }
        } catch (err) {
            crackLog('downloadBaseWordlist failed', err);
            setStatus('Download failed');
            throw err;
        }
    }

    async function bigHead(url) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'HEAD',
                url,
                onload: (r) => {
                    const len = parseInt(getHeader(r.responseHeaders, 'Content-Length') || '0', 10) || 0;
                    const ranges = (getHeader(r.responseHeaders, 'Accept-Ranges') || '').toLowerCase().includes('bytes');
                    resolve({ len, ranges });
                },
                onerror: () => resolve({ len: 0, ranges: false }),
            });
        });
    }

    /* --------------------------
       Supabase sync
       -------------------------- */
    async function mergeSupabaseIntoCache(words) {
        if (!words || !words.length) return 0;
        let added = 0;
        for (const w of words) {
            const len = w.length;
            if (len < MIN_LENGTH || len > MAX_LENGTH) continue;
            let chunk = await idbGet(`len_${len}`);
            if (!chunk) chunk = [];
            if (!chunk.includes(w)) {
                chunk.push(w);
                await idbSet(`len_${len}`, chunk);
                if (!dict[len]) dict[len] = [];
                if (!dict[len].includes(w)) dict[len].push(w);
                added++;
            }
        }
        return added;
    }

    async function fetchRemoteMeta() {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'HEAD',
                url: SUPABASE_COUNT_URL,
                timeout: 10000,
                onload: async (res) => {
                    const count = Number(getHeader(res.responseHeaders, 'X-Total-Count') || 0);
                    const etag = getHeader(res.responseHeaders, 'ETag') || '';
                    await idbSet('sb_remote_count', count);
                    await idbSet('sb_remote_etag', etag);
                    resolve({ count, etag });
                },
                onerror: () => resolve({ count: 0, etag: '' }),
            });
        });
    }

    async function downloadCommunityWordlist(ifNoneMatchEtag) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: SUPABASE_WORDS_URL,
                headers: ifNoneMatchEtag ? { 'If-None-Match': ifNoneMatchEtag } : {},
                timeout: 20000,
                onload: async (res) => {
                    const etag = getHeader(res.responseHeaders, 'ETag') || '';
                    const count = Number(getHeader(res.responseHeaders, 'X-Total-Count') || 0);
                    if (etag) await idbSet('sb_remote_etag', etag);
                    if (count) await idbSet('sb_remote_count', count);

                    if (res.status === 304) { crackLog('Community word-list unchanged (304).'); resolve(0); return; }
                    if (res.status !== 200) { crackLog('Download failed, status:', res.status); resolve(0); return; }

                    const arr = JSON.parse(res.responseText);
                    const up = arr.map(w => (typeof w === 'string' ? w.toUpperCase() : '')).filter(Boolean);
                    supabaseWords = new Set(up);
                    const added = await mergeSupabaseIntoCache(up);

                    await idbSet('sb_last_downloaded_count', count || up.length);
                    resolve(added);
                },
                onerror: () => resolve(0),
            });
        });
    }

    async function checkRemoteAndMaybeDownload(force = false) {
        const { count: remoteCount, etag } = await fetchRemoteMeta();
        const lastDownloaded = (await idbGet('sb_last_downloaded_count')) || 0;
        const delta = Math.max(0, remoteCount - lastDownloaded);

        if (!force && delta < DOWNLOAD_MIN_DELTA) {
            crackLog(`Skip download: delta=${delta} < ${DOWNLOAD_MIN_DELTA}`);
            await idbSet('sb_pending_delta', delta);
            return 0;
        }

        setStatus(force ? 'Manual sync…' : `Syncing (+${delta})…`);
        const added = await downloadCommunityWordlist(etag);
        await idbSet('sb_pending_delta', 0);
        return added;
    }

    async function sendWordToSupabase(word) {
        if (!SUPABASE_ADD_WORD_URL) {
            crackLog('SUPABASE_ADD_WORD_URL not set. skipping send:', word);
            return;
        }
        const payload = { word: word.toUpperCase() };
        GM_xmlhttpRequest({
            method: 'POST',
            url: SUPABASE_ADD_WORD_URL,
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify(payload),
            onload: (res) => {
                crackLog('sent to supabase', payload, 'status', res.status);
                setStatus(`Send ${payload.word} to the community wordlist!`);
                supabaseWords.add(payload.word);
            },
            onerror: (err) => crackLog('failed to send to supabase', err)
        });
    }

    /* --------------------------
       Exclusions + suggestions
       -------------------------- */
    function loadExclusions(rowKey, len) {
        const raw = sessionStorage.getItem(EXCL_STORAGE_PREFIX + rowKey + '_' + len);
        let arr = [];
        if (raw) { try { arr = JSON.parse(raw); } catch { } }
        const out = new Array(len);
        for (let i = 0; i < len; i++) {
            const s = Array.isArray(arr[i]) ? arr[i] : (typeof arr[i] === 'string' ? arr[i].split('') : []);
            out[i] = new Set(s.map(c => String(c || '').toUpperCase()).filter(Boolean));
        }
        return out;
    }
    function saveExclusions(rowKey, len, sets) {
        const arr = new Array(len);
        for (let i = 0; i < len; i++) arr[i] = Array.from(sets[i] || new Set());
        sessionStorage.setItem(EXCL_STORAGE_PREFIX + rowKey + '_' + len, JSON.stringify(arr));
    }
    function schedulePanelUpdate(panel) {
        if (!panel) return;
        const key = panel.dataset.rowkey;
        if (panelUpdateTimers.has(key)) clearTimeout(panelUpdateTimers.get(key));
        panelUpdateTimers.set(key, setTimeout(() => {
            panel.updateSuggestions();
            panelUpdateTimers.delete(key);
        }, 50));
    }
    function addExclusion(rowKey, pos, letter, len) {
        letter = String(letter || '').toUpperCase();
        if (!letter) return;
        const sets = loadExclusions(rowKey, len);
        if (!sets[pos]) sets[pos] = new Set();
        const before = sets[pos].size;
        sets[pos].add(letter);
        if (sets[pos].size !== before) {
            saveExclusions(rowKey, len, sets);
            const panel = document.querySelector(`.__crackhelp_panel[data-rowkey="${rowKey}"]`);
            schedulePanelUpdate(panel);
        }
    }

    async function suggest(pattern, rowKey) {
        const len = pattern.length;
        if (len < MIN_LENGTH || len > MAX_LENGTH) return [];
        if (!dict[len]) {
            const chunk = await idbGet(`len_${len}`); if (!chunk) return [];
            dict[len] = chunk;
        }
        const maxCandidates = MAX_SUG * 50;
        const worker = new Worker(URL.createObjectURL(new Blob([`
      self.onmessage = function(e) {
        const { dictChunk, pattern, max } = e.data;
        const regex = new RegExp('^' + pattern.replace(/[*]/g, '.') + '$');
        const out = [];
        for (const word of dictChunk) {
          if (regex.test(word)) out.push(word);
          if (out.length >= max) break;
        }
        self.postMessage(out);
      };
    `], { type: 'application/javascript' })));
        const candidates = await new Promise((resolve) => {
            worker.onmessage = (e) => { worker.terminate(); resolve([...new Set(e.data)]); };
            worker.postMessage({ dictChunk: dict[len], pattern: pattern.toUpperCase(), max: maxCandidates });
        });

        const exSets = loadExclusions(rowKey, len);
        const filtered = candidates.filter(w => {
            for (let i = 0; i < len; i++) {
                const s = exSets[i];
                if (s && s.has(w[i])) return false;
            }
            return true;
        });
        return filtered.slice(0, MAX_SUG);
    }

    function prependPanelToRow(row, pat, rowKey) {
        let panel = row.querySelector('.__crackhelp_panel');

        if (!panel) {
            panel = document.createElement('div');
            panel.className = '__crackhelp_panel';
            panel.dataset.rowkey = rowKey;
            panel.dataset.pattern = pat;
            panel.style.cssText = 'background:#000; font-size:10px; text-align:center; position:absolute; z-index:9999;';

            const listDiv = document.createElement('div');
            listDiv.style.cssText = 'margin-top:2px;';
            panel.appendChild(listDiv);

            panel.updateSuggestions = async function () {
                const curPat = panel.dataset.pattern || '';
                const curRowKey = panel.dataset.rowkey;

                if (!dictLoaded && dictLoading) {
                    if (!listDiv.firstChild || listDiv.firstChild.textContent !== '(loading dictionary…)') {
                        listDiv.innerHTML = '<span style="padding:2px;color:#ff0;">(loading dictionary…)</span>';
                    }
                    return;
                }

                const sugs = await suggest(curPat, curRowKey);
                let i = 0;
                for (; i < sugs.length; i++) {
                    let sp = listDiv.children[i];
                    if (!sp) { sp = document.createElement('span'); sp.style.cssText = 'padding:2px;color:#0f0;'; listDiv.appendChild(sp); }
                    if (sp.textContent !== sugs[i]) sp.textContent = sugs[i];
                    if (sp.style.color !== 'rgb(0, 255, 0)' && sp.style.color !== '#0f0') sp.style.color = '#0f0';
                }
                while (listDiv.children.length > sugs.length) listDiv.removeChild(listDiv.lastChild);

                if (sugs.length === 0) {
                    if (!listDiv.firstChild) {
                        const sp = document.createElement('span');
                        sp.textContent = dictLoaded ? '(no matches)' : '(loading dictionary…)';
                        sp.style.color = dictLoaded ? '#a00' : '#ff0';
                        listDiv.appendChild(sp);
                    } else {
                        const sp = listDiv.firstChild;
                        const txt = dictLoaded ? '(no matches)' : '(loading dictionary…)';
                        if (sp.textContent !== txt) sp.textContent = txt;
                        sp.style.color = dictLoaded ? '#a00' : '#ff0';
                    }
                }
            };

            row.prepend(panel);
        } else {
            panel.dataset.pattern = pat;
        }
        schedulePanelUpdate(panel);

        return panel;
    }

    async function isWordInLocalDict(word) {
        const len = word.length;
        if (!dict[len]) {
            const chunk = await idbGet(`len_${len}`); if (!chunk) return false;
            dict[len] = chunk;
        }
        return dict[len].includes(word);
    }
    async function addWordToLocalCache(word) {
        const len = word.length;
        if (len < MIN_LENGTH || len > MAX_LENGTH) return;
        let chunk = await idbGet(`len_${len}`); if (!chunk) chunk = [];
        if (!chunk.includes(word)) {
            chunk.push(word); await idbSet(`len_${len}`, chunk);
            if (!dict[len]) dict[len] = [];
            if (!dict[len].includes(word)) dict[len].push(word);
            crackLog('Added to local cache:', word);
        }
    }

    function getRowKey(crimeOption, idx) {
        if (!crimeOption.dataset.crackKey) crimeOption.dataset.crackKey = String(idx ?? 0);
        return crimeOption.dataset.crackKey;
    }

    function attachSlotSensors(crimeOption, rowKey) {
        if (crimeOption.dataset.crackDelegated === '1') return;
        crimeOption.dataset.crackDelegated = '1';

        const slotSelector = '[class^="charSlot"]:not([class*="charSlotDummy"])';
        const badLineSelector = '[class*="incorrectGuessLine"]';

        const onVisualCue = (ev) => {
            const t = ev.target;
            const slot = t.closest && t.closest(slotSelector);
            if (!slot || !crimeOption.contains(slot)) return;

            const slots = crimeOption.querySelectorAll(slotSelector);
            const i = Array.prototype.indexOf.call(slots, slot);
            if (i < 0) return;
            if (getComputedStyle(slot).borderColor === 'rgb(130, 201, 30)') return;

            const now = performance.now();
            const shown = (slot.textContent || '').trim();
            if (shown && /^[A-Za-z0-9._]$/.test(shown)) return;

            const prev = prevRowStates.get(rowKey) || null;
            const hasRowLastInput = !!(prev && prev.lastInput && (now - prev.lastInput.time) <= 1800 && prev.lastInput.i === i);
            const isIncorrectLineEvent = t.matches && t.matches(badLineSelector);
            const freshGlobal = (now - (LAST_INPUT.time || 0)) <= 1800;

            let letter = null;
            if (hasRowLastInput) letter = prev.lastInput.letter;
            else if (isIncorrectLineEvent && freshGlobal && LAST_INPUT.key) letter = LAST_INPUT.key.toUpperCase();
            else return;

            if (!/^[A-Za-z0-9._]$/.test(letter)) return;

            const len = slots.length;
            addExclusion(rowKey, i, letter, len);

            const panel = document.querySelector(`.__crackhelp_panel[data-rowkey="${rowKey}"]`);
            if (panel && panel.updateSuggestions) schedulePanelUpdate(panel);
        };

        crimeOption.addEventListener('animationstart', onVisualCue, true);
        crimeOption.addEventListener('transitionend', onVisualCue, true);
    }

    function scanCrimePage() {
        if (!location.href.endsWith('cracking')) return;

        const currentCrime = document.querySelector('[class^="currentCrime"]');
        if (!currentCrime) return;

        const container = currentCrime.querySelector('[class^="virtualList"]');
        if (!container) return;

        const crimeOptions = container.querySelectorAll('[class^="crimeOptionWrapper"]');

        for (const crimeOption of crimeOptions) {
            let patText = '';
            const rowKey = getRowKey(crimeOption, [...crimeOptions].indexOf(crimeOption));
            attachSlotSensors(crimeOption, rowKey);

            const charSlots = crimeOption.querySelectorAll('[class^="charSlot"]:not([class*="charSlotDummy"])');
            const curChars = [];
            for (const charSlot of charSlots) {
                let ch = (charSlot.textContent || '').trim().toUpperCase();
                curChars.push(ch ? ch : '*');
            }
            patText = curChars.join('');

            const now = performance.now();
            const len = curChars.length;

            const prev = prevRowStates.get(rowKey) || { chars: Array(len).fill('*') };

            for (let i = 0; i < len; i++) {
                const was = prev.chars[i];
                const is = curChars[i];
                if (was === '*' && is !== '*') prev.lastInput = { i, letter: is, time: now };
                if (was !== '*' && is === '*') {
                    if (prev.lastInput && prev.lastInput.i === i && prev.lastInput.letter === was && (now - prev.lastInput.time) <= 1800) {
                        addExclusion(rowKey, i, was, len);
                    }
                }
            }
            prevRowStates.set(rowKey, { chars: curChars, lastInput: prev.lastInput, time: now });

            if (!/[*]/.test(patText)) {
                const newWord = patText.toUpperCase();
                if (!/^[A-Z0-9_.]+$/.test(newWord)) {
                    crackLog('Revealed word contains invalid chars. skippin:', newWord);
                } else {
                    (async () => {
                        const localHas = await isWordInLocalDict(newWord);
                        const supHas = supabaseWords.has(newWord);
                        if (!localHas && !supHas) {
                            await addWordToLocalCache(newWord);
                            sendWordToSupabase(newWord);
                        } else if (supHas && !localHas) {
                            await addWordToLocalCache(newWord);
                        }
                    })();
                }
            }

            if (!/^[*]+$/.test(patText)) prependPanelToRow(crimeOption, patText, rowKey);
        }
    }

    /* --------------------------
       Dict load
       -------------------------- */
    function needReloadAfterBaseLoad() {
        try {
            if (sessionStorage.getItem('__crack_base_reload_done') === '1') return false;
            sessionStorage.setItem('__crack_base_reload_done', '1');
            return true;
        } catch { return true; }
    }

    async function loadDict() {
        if (dictLoaded || dictLoading) return;
        dictLoading = true;
        setStatus('Loading from cache…');

        let hasData = false;
        dict = [];
        for (let len = MIN_LENGTH; len <= MAX_LENGTH; len++) {
            const chunk = await idbGet(`len_${len}`);
            if (chunk && chunk.length) { dict[len] = chunk; hasData = true; }
        }

        if (!hasData) {
            crackLog('No cache found. Downloading dictionary…');
            try {
                const choice = getBaseWordlistPref(); // '1M' or '10M'
                await downloadBaseWordlist(choice, ({ phase, pct, processed }) => {
                    if (phase === '1M-index') setStatus(`Indexing 1M… processed ${processed}`);
                    if (phase === '10M-stream') setStatus(`Streaming 10M… ${pct ?? ''}% (processed ~${(processed || 0).toLocaleString()})`);
                    if (phase === '10M-commit') setStatus(`Merging 10M… ${pct ?? ''}%`);
                });
                if (needReloadAfterBaseLoad()) {
                    setStatus('Dictionary cached — reloading…');
                    setTimeout(() => location.reload(), 120);
                    return;
                }
            } catch (e) {
                crackLog('Failed to download base wordlist:', e);
            }
        } else {
            crackLog('Dictionary loaded from IndexedDB');
        }

        dictLoaded = true;
        dictLoading = false;
        setStatus('Ready');

        setStatus('Ready (database sync…)');
        checkRemoteAndMaybeDownload(false)
            .then(async (added) => {
                const remoteCount = await idbGet('sb_remote_count');
                const delta = await idbGet('sb_pending_delta');
                if (added && added > 0) setStatus(`Ready (+${added}, remote: ${remoteCount})`);
                else if (delta && delta > 0) setStatus(`Ready (remote +${delta} pending)`);
                else setStatus(`Ready (remote: ${remoteCount ?? 'n/a'})`);
            })
            .catch(() => setStatus('Ready'));
    }

    /* --------------------------
       Settings UI
       -------------------------- */
    async function showMenuOverlay() {
        const overlay = document.createElement('div');
        overlay.style.cssText = `
      position: fixed; top: 0; left: 0; width: 100%; height: 100%;
      background: rgba(0,0,0,0.7); color: #fff;
      display: flex; align-items: center; justify-content: center;
      z-index: 10000; font-size: 14px;
    `;
        const box = document.createElement('div');
        box.style.cssText = `
      background: #111; padding: 20px; border: 1px solid #0f0;
      border-radius: 6px; text-align: center; min-width: 340px;
    `;
        box.innerHTML = `<div style="margin-bottom:12px; font-size:20px; color:#0f0;">Settings</div>`;

        const statusLine = document.createElement('div');
        statusLine.style.cssText = 'color:#0f0; font-size:12px; margin-bottom:8px;';
        statusLine.textContent = ensureStatusBadge().textContent;
        registerStatusSink(statusLine);
        box.appendChild(statusLine);

        const wordCountDiv = document.createElement('div');
        wordCountDiv.style.cssText = 'color:#0f0; font-size:12px; margin-bottom:10px;';
        wordCountDiv.textContent = 'Loading dictionary stats...';
        box.appendChild(wordCountDiv);

        (async () => {
            let stats = [];
            for (let len = MIN_LENGTH; len <= MAX_LENGTH; len++) {
                const chunk = await idbGet(`len_${len}`); stats.push(`${len}: ${chunk ? chunk.length : 0}`);
            }
            const remoteCount = await idbGet('sb_remote_count');
            const delta = await idbGet('sb_pending_delta');
            wordCountDiv.textContent = `Stored words per length → ${stats.join(' | ')}  |  Remote cracked words: ${remoteCount ?? 'n/a'}${delta ? ` ( +${delta} pending )` : ''}`;
        })();

        const badgeRow = document.createElement('div');
        badgeRow.style.cssText = 'margin:8px 0; font-size:12px; color:#0f0; display:flex; align-items:center; justify-content:center; gap:8px;';
        const badgeLabel = document.createElement('label');
        badgeLabel.style.cssText = 'cursor:pointer; display:flex; align-items:center; gap:6px;';
        const badgeChk = document.createElement('input');
        badgeChk.type = 'checkbox';
        badgeChk.checked = getBoolPref(STATUS_PREF_KEY, true);
        badgeChk.onchange = () => {
            const show = badgeChk.checked;
            setBoolPref(STATUS_PREF_KEY, show);
            ensureStatusBadge().style.display = show ? 'block' : 'none';
        };
        const badgeText = document.createElement('span'); badgeText.textContent = 'Show status badge';
        badgeLabel.appendChild(badgeChk); badgeLabel.appendChild(badgeText);
        badgeRow.appendChild(badgeLabel);
        box.appendChild(badgeRow);

        const baseRow = document.createElement('div');
        baseRow.style.cssText = 'margin:8px 0; font-size:12px; color:#0f0; display:flex; align-items:center; justify-content:center; gap:8px;';
        const baseLabel = document.createElement('label'); baseLabel.textContent = 'Base wordlist:';
        const baseSelect = document.createElement('select');
        baseSelect.style.cssText = 'background:#000; color:#0f0; border:1px solid #0f0; padding:3px;';
        const opt1 = document.createElement('option'); opt1.value = '1M'; opt1.textContent = '1M';
        const opt2 = document.createElement('option'); opt2.value = '10M'; opt2.textContent = '10M (long first load)';
        baseSelect.appendChild(opt1); baseSelect.appendChild(opt2);
        baseSelect.value = getBaseWordlistPref();
        baseSelect.onchange = () => setBaseWordlistPref(baseSelect.value);
        baseRow.appendChild(baseLabel); baseRow.appendChild(baseSelect);
        box.appendChild(baseRow);

        const syncBtn = document.createElement('button');
        syncBtn.textContent = 'Sync now (manual)';
        syncBtn.style.cssText = 'margin:4px; padding:4px 8px; background:#0a0; color:#fff; cursor:pointer;';
        syncBtn.onclick = async () => {
            syncBtn.disabled = true;
            const added = await checkRemoteAndMaybeDownload(true);
            const remoteCount = await idbGet('sb_remote_count');
            const delta = await idbGet('sb_pending_delta');
            setStatus(added ? `Ready (+${added}, remote: ${remoteCount})` : `Ready (remote ${remoteCount}${delta ? `, +${delta} pending` : ''})`);
            let stats = [];
            for (let len = MIN_LENGTH; len <= MAX_LENGTH; len++) {
                const chunk = await idbGet(`len_${len}`); stats.push(`${len}: ${chunk ? chunk.length : 0}`);
            }
            wordCountDiv.textContent = `Stored words per length → ${stats.join(' | ')}  |  Remote cracked words: ${remoteCount ?? 'n/a'}`;
            syncBtn.disabled = false;
        };
        box.appendChild(syncBtn);

        const btnCache = document.createElement('button');
        btnCache.textContent = 'Clear Wordlist Cache';
        btnCache.style.cssText = 'margin:4px; padding:4px 8px; background:#a00; color:#fff; cursor:pointer;';
        btnCache.onclick = async () => { await clearLocalDictCache(); location.reload(); };

        const redlBtn = document.createElement('button');
        redlBtn.textContent = 'Re-download base wordlist (uses selection)';
        redlBtn.style.cssText = 'margin:4px; padding:4px 8px; background:#1f2937; color:#fff; cursor:pointer;';
        redlBtn.onclick = async () => {
            try {
                redlBtn.disabled = true;
                setStatus('Re-downloading base wordlist…');
                await clearLocalDictCache();
                const choice = getBaseWordlistPref();
                await downloadBaseWordlist(choice, ({ phase, pct, processed }) => {
                    if (phase === '1M-index') setStatus(`Indexing 1M… processed ${processed}`);
                    if (phase === '10M-stream') setStatus(`Streaming 10M… ${pct ?? ''}% (processed ~${(processed || 0).toLocaleString()})`);
                    if (phase === '10M-commit') setStatus(`Merging 10M… ${pct ?? ''}%`);
                });
                let stats = [];
                for (let len = MIN_LENGTH; len <= MAX_LENGTH; len++) {
                    const chunk = await idbGet(`len_${len}`); stats.push(`${len}: ${chunk ? chunk.length : 0}`);
                }
                const remoteCount = await idbGet('sb_remote_count');
                wordCountDiv.textContent = `Stored words per length → ${stats.join(' | ')}  |  Remote cracked words: ${remoteCount ?? 'n/a'}`;
                setStatus('Dictionary cached — reloading…');
                setTimeout(() => location.reload(), 120);
            } catch (e) {
                crackLog('Re-download failed', e); setStatus('Re-download failed');
            } finally { redlBtn.disabled = false; }
        };

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'Close';
        cancelBtn.style.cssText = 'margin:4px; padding:4px 8px; background:#222; color:#fff; cursor:pointer;';
        cancelBtn.onclick = () => {
            unregisterStatusSink(statusLine);
            if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
        };

        box.appendChild(btnCache);
        box.appendChild(redlBtn);
        box.appendChild(cancelBtn);

        const line = document.createElement('hr');
        line.style.cssText = 'border:none; border-top:1px solid #0f0; margin:10px 0;';
        box.appendChild(line);

        const pwrdByMsg = document.createElement('div');
        pwrdByMsg.style.cssText = 'color:#0f0; font-size:12px; margin-bottom:10px;';
        pwrdByMsg.textContent = 'Powered by Supabase / IndexedDB - Made with Love ❤ by SirAua [3785905] (and friends)';
        box.appendChild(pwrdByMsg);

        const psMsg = document.createElement('div');
        psMsg.style.cssText = 'color:#0f0; font-size:9px; margin-bottom:10px;';
        psMsg.textContent = 'Ps: Clear the cache after updates';
        box.appendChild(psMsg);

        overlay.appendChild(box);
        document.body.appendChild(overlay);
    }

    function injectMenuButton() {
        if (!location.href.endsWith('cracking')) return;
        if (document.getElementById('__crack_menu_btn')) return;
        const appHeader = document.querySelector('[class^="appHeaderDelimiter"]');
        if (!appHeader) return;
        const btn = document.createElement('button');
        btn.id = '__crack_menu_btn';
        btn.textContent = 'Bruteforce characters to show suggestions! (Click this to open a menu)';
        btn.style.cssText = 'background:#000; color:#0f0; font-size:10px; text-align:left; z-index:9999; cursor:pointer;';
        btn.onclick = showMenuOverlay;
        appHeader.appendChild(btn);
        ensureStatusBadge();
    }

    /* --------------------------
       Init
       -------------------------- */
    (async function init() {
        ensureStatusBadge();
        try { if (sessionStorage.getItem('__crack_base_reload_done') === '1') sessionStorage.removeItem('__crack_base_reload_done'); } catch { }
        setStatus('Initializing…');
        loadDict();
        scanCrimePage();
        setInterval(scanCrimePage, UPDATE_INTERVAL);
        setInterval(injectMenuButton, UPDATE_INTERVAL);
    })();
})();