Cracking helper with community word pool, probability-ranked suggestions, encrypted-slot awareness, and safe positional estimates
// ==UserScript==
// @name cRaCked
// @namespace modul.torn.cracking
// @version 1.3.1
// @description Cracking helper with community word pool, probability-ranked suggestions, encrypted-slot awareness, and safe positional estimates
// @author SirAua [3785905] & MoDuL [4022159]
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @match https://www.torn.com/page.php?sid=crimes*
// @grant GM_xmlhttpRequest
// @connect gitlab.com
// @connect pp-api.sokin.xyz
// @run-at document-idle
// @license mit
// ==/UserScript==
(function () {
'use strict';
function isCrackingPage() {
return location.pathname === '/page.php'
&& new URLSearchParams(location.search).get('sid') === 'crimes'
&& location.hash === '#/cracking';
}
if (window.CRACK_SCRIPT_BOOTSTRAPPED) return;
window.CRACK_SCRIPT_BOOTSTRAPPED = true;
/* --------------------------
Config
-------------------------- */
const debug = false;
const UPDATE_INTERVAL = 800;
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 DOWNLOAD_MIN_DELTA = 20;
const COMMUNITY_POOL_ORIGIN = 'https://pp-api.sokin.xyz/torn-crack-pool';
const COMMUNITY_SYNC_ENABLED = true;
const CF_WORKER_ORIGIN = COMMUNITY_SYNC_ENABLED ? COMMUNITY_POOL_ORIGIN.replace(/\/+$/, '') : '';
const CF_ADD_WORD_URL = COMMUNITY_SYNC_ENABLED ? `${CF_WORKER_ORIGIN}/submit` : '';
const CF_STORAGE_BASE = COMMUNITY_SYNC_ENABLED ? `${CF_WORKER_ORIGIN}/words` : '';
const METADATA_URL = COMMUNITY_SYNC_ENABLED ? `${CF_STORAGE_BASE}/metadata.json` : '';
/* --------------------------
Rate-limiting / batching
-------------------------- */
const SYNC_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
const OUTBOX_FLUSH_INTERVAL_MS = 5 * 1000;
const OUTBOX_POST_INTERVAL_MS = 2000;
const OUTBOX_BATCH_SIZE = 5;
const DB_NAME = 'crack';
const STORE_NAME = 'dictionary';
const STATUS_PREF_KEY = 'crack_show_badge';
const EXCL_STORAGE_PREFIX = 'crack_excl_';
// theme + UI preferences
const THEME_PREF_KEY = 'crack_theme'; // 'dark' | 'light'
const PREF_SUG_FONT_PX = 'crack_sug_font_px';
const PREF_SUG_TEXT_COLOR = 'crack_sug_text_color';
const PREF_SUG_BG_COLOR = 'crack_sug_bg_color';
const PREF_UI_TEXT_COLOR = 'crack_ui_text_color';
const PREF_UI_BG_COLOR = 'crack_ui_bg_color';
const PREF_UI_BORDER_COLOR = 'crack_ui_border_color';
const PREF_UI_BOX_BG_COLOR = 'crack_ui_box_color';
const PREF_SHOW_SUG_ON_COMPLETE = 'crack_show_sug_on_complete';
const PREF_SHOW_ADVANCED_OPT_COLOR = 'crack_show_advanced_opt_color';
const PREF_MAX_SUGGESTIONS = 'crack_max_suggestions_size';
const DEFAULT_MAX_SUGGESTIONS = 8;
const MIN_MAX_SUGGESTIONS = 1;
const MAX_MAX_SUGGESTIONS = 20;
const THEME_PRESETS = {
dark: {
uiBg: '#000',
uiText: '#0f0',
uiBorder: '#0f0',
overlayBg: 'rgba(0,0,0,0.5)',
boxBg: '#111',
sugBg: '#000',
sugText: '#0f0',
},
light: {
uiBg: '#fff',
uiText: '#03396c',
uiBorder: '#39ace7',
overlayBg: 'rgba(0,0,0,0.5)',
boxBg: '#fff',
sugBg: '#fff',
sugText: '#03396c',
},
};
/* --------------------------
State
-------------------------- */
let dict = [];
let dictRevision = 0;
let dictLoaded = false;
let dictLoading = false;
let remoteWords = new Set();
let statusEl = null;
const prevRowStates = new Map();
const panelUpdateTimers = new Map();
const LAST_INPUT = { key: null, time: 0 };
let outboxFlushTimer = null;
let lastOutboxPost = 0;
let wordObservationCounts = null;
let contextObservationCounts = null;
let observationWriteChain = Promise.resolve();
let scanInterval = null;
let menuInterval = null;
let runtimeStarted = false;
let autoSyncTimer = null;
let autoSyncInFlight = false;
/* --------------------------
Utils
-------------------------- */
function crackLog(...args) { if (debug) console.log('[Crack]', ...args); }
function isPdaUserAgentCheck() {
return window.navigator.userAgent.toLowerCase().includes("com.manuito.tornpda");
}
function isCompactCrackingView() {
return isPdaUserAgentCheck() || window.innerWidth <= 700;
}
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 getStrPref(key, def = '') {
const v = localStorage.getItem(key);
return v === null ? def : String(v);
}
function setStrPref(key, val) {
localStorage.setItem(key, String(val));
}
function getIntPref(key, def = 0) {
const v = localStorage.getItem(key);
const n = v === null ? NaN : parseInt(v, 10);
return Number.isFinite(n) ? n : def;
}
function setIntPref(key, val) {
const n = Number(val);
localStorage.setItem(key, String(Number.isFinite(n) ? Math.trunc(n) : 0));
}
function clampInt(n, min, max, def) {
n = Number(n);
if (!Number.isFinite(n)) n = def;
return Math.max(min, Math.min(max, Math.trunc(n)));
}
function getClampedIntPref(key, def, min, max) {
return clampInt(getIntPref(key, def), min, max, def);
}
function splitMiddle(str) {
const mid = Math.ceil(str.length / 2);
return str.slice(0, mid) + "\n" + str.slice(mid);
}
function normalizeThemeName(v) {
return v === 'light' ? 'light' : 'dark';
}
function getThemePreset() {
const name = normalizeThemeName(getStrPref(THEME_PREF_KEY, 'dark'));
return { name, ...THEME_PRESETS[name] };
}
function getTheme() {
const preset = getThemePreset();
return {
name: preset.name,
uiBg: getStrPref(PREF_UI_BG_COLOR, preset.uiBg),
uiText: getStrPref(PREF_UI_TEXT_COLOR, preset.uiText),
uiBorder: getStrPref(PREF_UI_BORDER_COLOR, preset.uiBorder),
overlayBg: preset.overlayBg,
boxBg: getStrPref(PREF_UI_BOX_BG_COLOR, preset.boxBg),
sugBg: getStrPref(PREF_SUG_BG_COLOR, preset.sugBg),
sugText: getStrPref(PREF_SUG_TEXT_COLOR, preset.sugText),
sugFontPx: getIntPref(PREF_SUG_FONT_PX, 10),
};
}
function removeLegacyRigPreferences() {
const keys = [
'crack_rig_skill_level',
'crack_rig_inv_HPCPU',
'crack_rig_inv_ECPU',
'crack_rig_inv_CPU',
'crack_rig_inv_FAN',
'crack_rig_inv_WB',
'crack_rig_inv_HS',
'crack_rig_inv_PSU'
];
for (const key of keys) localStorage.removeItem(key);
}
function applyPreset(themeName) {
const name = normalizeThemeName(themeName);
const p = THEME_PRESETS[name];
setStrPref(THEME_PREF_KEY, name);
setStrPref(PREF_UI_BG_COLOR, p.uiBg);
setStrPref(PREF_UI_TEXT_COLOR, p.uiText);
setStrPref(PREF_UI_BORDER_COLOR, p.uiBorder);
setStrPref(PREF_SUG_BG_COLOR, p.sugBg);
setStrPref(PREF_SUG_TEXT_COLOR, p.sugText);
setStrPref(PREF_UI_BOX_BG_COLOR, p.boxBg);
if (localStorage.getItem(PREF_SUG_FONT_PX) === null) setIntPref(PREF_SUG_FONT_PX, 10);
}
function applyStatusBadgeTheme(el) {
const t = getTheme();
if (!el) return;
el.style.background = t.uiBg;
el.style.color = t.uiText;
el.style.border = `1px solid ${t.uiBorder}`;
}
function applyMenuButtonTheme(btn) {
const t = getTheme();
if (!btn) return;
btn.style.background = t.uiBg;
btn.style.color = t.uiText;
btn.style.border = `1px solid ${t.uiBorder}`;
btn.style.borderRadius = '4px';
}
function appplyMenuInputTheme(inpt) {
const t = getTheme();
if (!inpt) return;
inpt.style.background = t.uiBg;
inpt.style.color = t.uiText;
inpt.style.border = `1px solid ${t.uiBorder}`;
inpt.style.borderRadius = '4px';
}
function styleSugSpan(sp) {
const t = getTheme();
const compact = isCompactCrackingView();
sp.style.padding = '2px 4px';
sp.style.margin = '0 2px';
sp.style.display = 'inline-block';
sp.style.borderRadius = '3px';
sp.style.fontSize = `${compact ? Math.min(t.sugFontPx, 9) : t.sugFontPx}px`;
sp.style.color = t.sugText;
sp.style.maxWidth = compact ? '100%' : '';
sp.style.overflowWrap = compact ? 'anywhere' : '';
sp.style.whiteSpace = compact ? 'normal' : '';
}
function styleMessageChip(sp) {
styleSugSpan(sp);
const state = sp?.dataset?.msgState || '';
if (state === 'no-match') sp.style.color = '#ff5555';
else if (state === 'complete') sp.style.color = '#82c91e';
else sp.style.color = '#ffd75a';
sp.style.background = 'transparent';
sp.style.border = 'none';
sp.style.fontWeight = state === 'complete' ? 'bold' : 'normal';
}
function applyPanelTheme(panel) {
const t = getTheme();
if (!panel) return;
const compact = isCompactCrackingView();
panel.style.background = t.sugBg;
panel.style.color = t.sugText;
panel.style.fontSize = `${compact ? Math.min(t.sugFontPx, 9) : t.sugFontPx}px`;
panel.style.textAlign = 'center';
panel.style.position = 'absolute';
panel.style.zIndex = '9999';
const listDiv = panel.querySelector(':scope > div');
if (!listDiv) return;
listDiv.style.gap = compact ? '1px' : '2px';
listDiv.style.lineHeight = compact ? '1.05' : '1.2';
for (const child of Array.from(listDiv.children)) {
const kind = child.dataset ? child.dataset.kind : '';
if (kind === 'msg') {
styleMessageChip(child);
} else if (kind === 'sug' || kind === 'prob-best' || kind === 'prob-position') {
styleSugSpan(child);
}
if (kind === 'prob-best') {
child.style.fontWeight = 'bold';
child.style.border = `1px solid ${t.uiBorder}`;
}
}
}
function positionPanelBesideWord(row, panel) {
if (!row || !panel) return;
const firstSlot = row.querySelector('[class^="charSlot"]:not([class*="charSlotDummy"])');
if (!firstSlot) return;
const rowRect = row.getBoundingClientRect();
const slotRect = firstSlot.getBoundingClientRect();
if (!rowRect.width || !rowRect.height || !slotRect.width) return;
if (getComputedStyle(row).position === 'static') row.style.position = 'relative';
const compact = isCompactCrackingView();
const listDiv = panel.querySelector(':scope > div');
const slotTop = Math.floor(slotRect.top - rowRect.top);
const availableWidth = Math.floor(slotRect.left - rowRect.left - 8);
const rowWidth = Math.max(80, Math.floor(rowRect.width - 8));
const safeLeftWidth = Math.max(0, availableWidth - 4);
panel.dataset.layout = compact ? 'compact' : 'desktop';
panel.style.boxSizing = 'border-box';
panel.style.overflowX = 'hidden';
panel.style.overflowY = 'auto';
panel.style.whiteSpace = 'normal';
if (compact) {
const topStripHeight = Math.max(0, slotTop - 6);
if (safeLeftWidth >= 52) {
panel.style.left = '4px';
panel.style.right = 'auto';
panel.style.top = '4px';
panel.style.bottom = 'auto';
panel.style.width = `${safeLeftWidth}px`;
panel.style.maxWidth = `${safeLeftWidth}px`;
panel.style.maxHeight = `${topStripHeight >= 24 ? topStripHeight : Math.max(28, Math.floor(rowRect.height - 8))}px`;
if (listDiv) {
listDiv.style.flexDirection = 'column';
listDiv.style.alignItems = 'stretch';
listDiv.style.flexWrap = 'nowrap';
}
return;
}
// TornPDA can have no safe left gutter. In that case keep the panel above
// the first password slot instead of crossing over the slot row.
panel.style.left = '4px';
panel.style.right = 'auto';
panel.style.top = '2px';
panel.style.bottom = 'auto';
panel.style.width = `${rowWidth}px`;
panel.style.maxWidth = `${rowWidth}px`;
panel.style.maxHeight = `${Math.max(24, Math.min(42, topStripHeight || 32))}px`;
if (listDiv) {
listDiv.style.flexDirection = 'row';
listDiv.style.alignItems = 'center';
listDiv.style.flexWrap = 'wrap';
}
return;
}
// Keep the probability panel entirely to the left of Torn's first character slot.
// This preserves the actual cracking word and the brute-force controls.
const usableWidth = Math.max(120, availableWidth - 4);
const usableHeight = Math.max(30, Math.floor(rowRect.height - 8));
panel.style.left = '4px';
panel.style.right = 'auto';
panel.style.top = '4px';
panel.style.bottom = 'auto';
panel.style.width = `${Math.min(usableWidth, Math.max(120, availableWidth))}px`;
panel.style.maxWidth = `${Math.max(120, availableWidth)}px`;
panel.style.maxHeight = `${usableHeight}px`;
if (listDiv) {
listDiv.style.flexDirection = 'row';
listDiv.style.alignItems = 'center';
listDiv.style.flexWrap = 'wrap';
}
}
function applyThemeEverywhere() {
applyStatusBadgeTheme(statusEl);
const btn = document.getElementById('__crack_menu_btn');
if (btn) applyMenuButtonTheme(btn);
for (const panel of document.querySelectorAll('.__crackhelp_panel')) {
applyPanelTheme(panel);
}
}
function addHr(parent, theme0, count = 1, margin = "12px 0") {
for (let i = 0; i < count; i++) {
const hr = document.createElement("hr");
hr.style.cssText = `border:none; border-top:1px solid ${theme0.uiBorder}; margin:${margin};`;
parent.appendChild(hr);
}
}
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;
padding:6px 8px; font-size:11px; font-family:monospace; opacity:0.9;
border-radius:6px;
`;
statusEl.textContent = 'Dictionary: Idle';
document.body.appendChild(statusEl);
applyStatusBadgeTheme(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 {
const safeOpts = Object.assign({}, opts);
if (!('responseType' in safeOpts) || !safeOpts.responseType) safeOpts.responseType = 'text';
safeOpts.headers = Object.assign({ Accept: 'application/json, text/plain, */*; q=0.1' }, safeOpts.headers || {});
GM_xmlhttpRequest({ ...safeOpts, 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;
}
function isGzipPath(pathOrUrl) {
try {
const s = String(pathOrUrl || '');
const clean = s.split('?')[0];
return /\.gz$/i.test(clean);
} catch (_) {
return false;
}
}
async function gunzipArrayBufferToText(arrayBuffer) {
if (!arrayBuffer) return '';
if (typeof DecompressionStream !== 'function') {
throw new Error('Your browser does not support DecompressionStream(gzip). Upload an uncompressed snapshot/diff (plain .txt / .ndjson) or use a modern browser.');
}
const ds = new DecompressionStream('gzip');
const stream = new Blob([arrayBuffer]).stream().pipeThrough(ds);
return await new Response(stream).text();
}
async function responseToText(res, pathOrUrl) {
if (isGzipPath(pathOrUrl)) {
return await gunzipArrayBufferToText(res.response);
}
return res.responseText || '';
}
function metadataURL(force = false) {
if (!COMMUNITY_SYNC_ENABLED || !METADATA_URL) return '';
const ts = force ? Date.now() : Math.floor(Date.now() / 60000);
return `${METADATA_URL}?cb=${ts}`;
}
function formatShortDuration(ms) {
if (ms <= 0) return 'now';
const s = Math.floor(ms / 1000);
const d = Math.floor(s / 86400);
const h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
if (d > 0) return `${d}d ${h}h ${m}m`;
if (h > 0) return `${h}h ${m}m ${sec}s`;
if (m > 0) return `${m}m ${sec}s`;
return `${sec}s`;
}
function cleanupView() {
const btn = document.getElementById('__crack_menu_btn');
if (btn && btn.parentNode) btn.parentNode.removeChild(btn);
const panels = document.querySelectorAll('.__crackhelp_panel');
if (panels) panels.forEach(p => p.remove());
for (const timer of panelUpdateTimers.values()) clearTimeout(timer);
panelUpdateTimers.clear();
prevRowStates.clear();
if (statusEl && statusEl.parentNode) {
statusEl.parentNode.removeChild(statusEl);
statusEl = null;
}
}
/* --------------------------
IndexedDB
-------------------------- */
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);
/* --------------------------
Dictionary load
-------------------------- */
async function commitBucketsToIDB(buckets) {
let changed = false;
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]));
if (merged.length !== existing.length) changed = true;
await idbSet(`len_${L}`, merged);
dict[L] = merged;
}
if (changed) dictRevision++;
}
async function fetchAndIndex(url, onProgress) {
setStatus('Downloading base wordlist …');
let res;
try {
res = await gmRequest({ method: 'GET', url, timeout: 90000, responseType: 'text' });
} catch (e) {
throw e;
}
if (res.status < 200 || res.status >= 300 || !res.responseText) {
const err = new Error(`Bad response from base wordlist: ${res.status}`);
err.status = res.status;
throw err;
}
setStatus('Indexing…');
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 };
}
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…');
const MAX_TRIES = 4;
const DELAYS = [0, 3000, 10000, 30000];
let ok = false, lastErr = null;
for (let attempt = 0; attempt < MAX_TRIES; attempt++) {
try {
await fetchAndIndex(WORDLIST_URL, ({ phase, processed }) => {
if (phase === '1M-index') setStatus(`Indexing 1M… processed ${processed}`);
});
ok = true;
break;
} catch (e) {
lastErr = e;
const wait = DELAYS[Math.min(attempt, DELAYS.length - 1)];
crackLog(`Base download failed (try ${attempt + 1}/${MAX_TRIES})`, e);
setStatus(`Download failed (try ${attempt + 1}/${MAX_TRIES}) — retrying in ${Math.ceil(wait / 1000)}s…`);
if (wait) await new Promise(r => setTimeout(r, wait));
}
}
if (!ok) {
crackLog('Giving up on base download for now.', lastErr);
dictLoading = false;
dictLoaded = false;
setTimeout(() => { loadDict().catch(() => {}); }, 60000);
setStatus('Failed to fetch base wordlist (will retry)...');
return;
}
if (needReloadAfterBaseLoad()) {
setStatus('Dictionary cached — reloading…');
setTimeout(() => location.reload(), 120);
return;
}
} else {
crackLog('Dictionary loaded from IndexedDB');
}
dictLoaded = true;
dictLoading = false;
setStatus(COMMUNITY_SYNC_ENABLED ? 'Ready' : 'Ready (local mode)');
}
async function fetchRemoteMeta(force = false) {
if (!COMMUNITY_SYNC_ENABLED) {
await idbSet('cf_pending_delta', 0);
return { count: 0, etag: '', snapshot_path: null, diff_path: null, generated_at: null, disabled: true };
}
try {
const lastSync = Number(await idbGet('cf_last_sync_ts')) || 0;
const now = Date.now();
if (!force && (now - lastSync) < SYNC_MIN_INTERVAL_MS) {
crackLog('Skipping fetchRemoteMeta (recent sync)');
const cachedMeta = await idbGet('cf_metadata') || {};
return {
count: cachedMeta.count || Number(await idbGet('cf_remote_count')) || 0,
etag: '',
snapshot_path: cachedMeta.snapshot_path || null,
diff_path: cachedMeta.diff_path || null,
generated_at: cachedMeta.generated_at || null
};
}
const metaUrl = metadataURL(force);
crackLog('Fetching metadata.json ->', metaUrl);
const metaRes = await gmRequest({ method: 'GET', url: metaUrl, timeout: 10000, responseType: 'text' });
if (metaRes.status !== 200) {
crackLog('metadata.json not available; using cached meta only', metaRes.status);
const cachedMeta = await idbGet('cf_metadata') || {};
return {
count: cachedMeta.count || Number(await idbGet('cf_remote_count')) || 0,
etag: '',
snapshot_path: cachedMeta.snapshot_path || null,
diff_path: cachedMeta.diff_path || null,
generated_at: cachedMeta.generated_at || null
};
}
const meta = JSON.parse(metaRes.responseText || '{}');
const toSave = {
count: meta.count || 0,
etag: '',
snapshot_path: meta.snapshot_path || meta.latest_path || null,
diff_path: meta.diff_path || null,
generated_at: meta.generated_at || null
};
await idbSet('cf_metadata', toSave);
await idbSet('cf_remote_count', toSave.count);
await idbSet('cf_last_sync_ts', Date.now());
return {
count: toSave.count,
etag: '',
snapshot_path: toSave.snapshot_path,
diff_path: toSave.diff_path,
generated_at: toSave.generated_at
};
} catch (e) {
crackLog('fetchRemoteMeta failed:', e);
return {
count: Number(await idbGet('cf_remote_count')) || 0,
etag: '',
snapshot_path: null,
diff_path: null,
generated_at: null
};
}
}
async function downloadCommunityWordlist(meta, ifNoneMatchEtag) {
if (!COMMUNITY_SYNC_ENABLED) return 0;
try {
if (!meta || !meta.snapshot_path) {
crackLog('No snapshot_path in metadata.');
return 0;
}
const snapshotUrl = `${CF_STORAGE_BASE}/${meta.snapshot_path}`;
crackLog('Fetching snapshot ->', snapshotUrl);
const headers = {};
if (ifNoneMatchEtag) headers['If-None-Match'] = ifNoneMatchEtag;
const isGz = isGzipPath(meta.snapshot_path);
const res = await gmRequest({
method: 'GET',
url: snapshotUrl,
headers,
timeout: 45000,
responseType: isGz ? 'arraybuffer' : 'text'
});
const remoteEtag = getHeader(res.responseHeaders, 'ETag') || '';
if (remoteEtag) await idbSet('cf_remote_etag', remoteEtag);
if (res.status === 304) {
crackLog('Snapshot unchanged (304)');
await idbSet('cf_last_downloaded_count', meta.count || 0);
await idbSet('cf_last_sync_ts', Date.now());
return 0;
}
if (res.status !== 200) {
crackLog('Snapshot fetch failed, status:', res.status);
return 0;
}
const text = await responseToText(res, meta.snapshot_path);
setStatus('Indexing snapshot…');
const lines = text.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) await new Promise(r => setTimeout(r, 0));
}
await commitBucketsToIDB(buckets);
setStatus('Snapshot indexed');
await idbSet('cf_remote_count', meta.count || 0);
await idbSet('cf_last_downloaded_count', meta.count || 0);
await idbSet('cf_last_sync_ts', Date.now());
await idbSet('cf_metadata', {
snapshot_path: meta.snapshot_path,
diff_path: meta.diff_path || null,
count: meta.count || 0,
generated_at: meta.generated_at || null,
etag: remoteEtag || ''
});
return 1;
} catch (e) {
crackLog('downloadCommunityWordlist failed:', e);
return 0;
}
}
async function checkRemoteAndMaybeDownload(force = false) {
if (!COMMUNITY_SYNC_ENABLED) {
await idbSet('cf_pending_delta', 0);
setStatus('Ready (local mode)');
return 0;
}
const meta = await fetchRemoteMeta(force);
const lastDownloaded = (await idbGet('cf_last_downloaded_count')) || 0;
const remoteCount = meta.count || Number(await idbGet('cf_remote_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('cf_pending_delta', delta);
return 0;
}
setStatus(force ? 'Manual sync…' : `Syncing (+${delta})…`);
const etag = (await idbGet('cf_remote_etag')) || '';
const added = await downloadCommunityWordlist(meta, etag);
await idbSet('cf_pending_delta', 0);
return added;
}
async function msUntilEligibleSync() {
if (!COMMUNITY_SYNC_ENABLED) return Number.POSITIVE_INFINITY;
const last = Number(await idbGet('cf_last_sync_ts')) || 0;
const remain = last + SYNC_MIN_INTERVAL_MS - Date.now();
return Math.max(0, remain);
}
function startAutoSyncHeartbeat() {
if (!COMMUNITY_SYNC_ENABLED) return;
if (autoSyncTimer) return;
autoSyncTimer = setInterval(async () => {
if (autoSyncInFlight) return;
if (!runtimeStarted) return;
if (!isCrackingPage()) return;
try {
const remain = await msUntilEligibleSync();
if (remain > 0) return;
autoSyncInFlight = true;
setStatus('Auto-syncing community words…');
const added = await checkRemoteAndMaybeDownload(false);
const remoteCount = await idbGet('cf_remote_count');
const delta = await idbGet('cf_pending_delta');
if (added && added > 0) {
setStatus(`Ready (+${added}, remote: ${remoteCount})`);
} else {
setStatus(`Ready (remote ${remoteCount}${delta ? `, +${delta} pending` : ''})`);
}
} catch (e) {
crackLog('Auto-sync failed', e);
setStatus('Ready');
} finally {
autoSyncInFlight = false;
}
}, 1000);
}
/* --------------------------
Outbox
-------------------------- */
async function enqueueOutbox(word) {
if (!COMMUNITY_SYNC_ENABLED) return;
if (!word) return;
const w = word.toUpperCase();
let out = await idbGet('cf_outbox') || [];
if (!out.includes(w)) {
out.push(w);
await idbSet('cf_outbox', out);
crackLog('Enqueued word to outbox:', w);
ensureOutboxFlushScheduled();
}
}
function ensureOutboxFlushScheduled() {
if (outboxFlushTimer) return;
outboxFlushTimer = setTimeout(flushOutbox, OUTBOX_FLUSH_INTERVAL_MS);
}
async function flushOutbox() {
outboxFlushTimer = null;
if (!COMMUNITY_SYNC_ENABLED) {
await idbSet('cf_outbox', []);
return;
}
let out = await idbGet('cf_outbox') || [];
if (!out || out.length === 0) return;
while (out.length > 0) {
const batch = out.splice(0, OUTBOX_BATCH_SIZE);
const now = Date.now();
const sinceLast = now - lastOutboxPost;
if (sinceLast < OUTBOX_POST_INTERVAL_MS) {
await new Promise(r => setTimeout(r, OUTBOX_POST_INTERVAL_MS - sinceLast));
}
try {
await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: CF_ADD_WORD_URL,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ words: batch }),
onload: (res) => (res.status >= 200 && res.status < 300) ? resolve(res) : reject(res),
onerror: reject,
ontimeout: reject,
timeout: 15000
});
});
crackLog('Flushed outbox batch:', batch.length);
for (const w of batch) {
remoteWords.add(w);
await addWordToLocalCache(w);
}
} catch (e) {
crackLog('Batch POST failed, falling back to single POSTs', e);
for (const w of batch) {
try {
await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: CF_ADD_WORD_URL,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ word: w }),
onload: (r) => (r.status >= 200 && r.status < 300) ? resolve(r) : reject(r),
onerror: reject,
ontimeout: reject,
timeout: 10000
});
});
crackLog('Flushed outbox (single):', w);
remoteWords.add(w);
await addWordToLocalCache(w);
await new Promise(r => setTimeout(r, OUTBOX_POST_INTERVAL_MS));
} catch (ee) {
crackLog('Single POST failed for', w, ee);
out.unshift(w);
break;
}
}
}
lastOutboxPost = Date.now();
await idbSet('cf_outbox', out);
}
}
/* --------------------------
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);
}
}
function formatProbability(probability) {
const pct = Math.max(0, Number(probability) || 0) * 100;
if (pct >= 10) return `${pct.toFixed(0)}%`;
if (pct >= 1) return `${pct.toFixed(1)}%`;
if (pct >= 0.1) return `${pct.toFixed(2)}%`;
return '<0.1%';
}
function normalizeContextPart(value) {
return String(value || '')
.replace(/\s+/g, ' ')
.trim()
.toUpperCase();
}
function getRowContext(crimeOption) {
const wrapper = crimeOption.querySelector('[class^="typeAndService"]');
const spans = wrapper ? Array.from(wrapper.querySelectorAll('span')) : [];
const titleEl = spans[0] || crimeOption.querySelector('span[class^="type"]');
const serviceEl = spans[1] || crimeOption.querySelector('span[class^="service"]');
const title = String(titleEl?.textContent || '').replace(/\s+/g, ' ').trim();
const service = String(serviceEl?.textContent || '').replace(/\s+/g, ' ').trim();
const titleKey = normalizeContextPart(title);
const serviceKey = normalizeContextPart(service);
return {
title,
service,
key: titleKey || serviceKey ? `${titleKey}||${serviceKey}` : ''
};
}
function getGuessesLeft(crimeOption) {
const section = crimeOption.querySelector('[class^="guessesSection"]');
if (!section) return null;
const label = String(section.getAttribute('aria-label') || '').trim();
if (/^No guesses\b/i.test(label)) return 0;
const match = label.match(/^(\d+)\s+guess(?:es)?\b/i);
if (match) return Number(match[1]);
return null;
}
function readSlotState(slot, index) {
const discoveredEl = slot.querySelector('[class^="discoveredChar"]');
let char = String(discoveredEl?.textContent || '').trim().toUpperCase();
if (!/^[A-Z0-9._]$/.test(char)) char = '*';
const className = typeof slot.className === 'string'
? slot.className
: String(slot.getAttribute('class') || '');
const aria = String(slot.getAttribute('aria-label') || '');
const layerMatch = aria.match(/\b(\d+)\s+encryption\b/i);
const encryptionLayers = layerMatch
? Math.max(0, Number(layerMatch[1]) || 0)
: (className.includes('hasEncryption') ? 1 : 0);
const encrypted = encryptionLayers > 0 || className.includes('hasEncryption');
const hidden = char === '*';
const hasInput = !!slot.querySelector('input[type="text"]');
return {
index,
char,
hidden,
encrypted,
encryptionLayers,
open: hidden && !encrypted,
hasInput,
guessable: hidden && !encrypted && hasInput
};
}
function buildRowActionState(crimeOption, slotStates, guessesLeft, context) {
const openPositions = slotStates.filter(slot => slot.open).map(slot => slot.index);
const actionablePositions = guessesLeft === 0
? []
: Number.isFinite(guessesLeft)
? openPositions
: slotStates.filter(slot => slot.guessable).map(slot => slot.index);
const signature = [
Number.isFinite(guessesLeft) ? guessesLeft : '?',
context.key,
slotStates.map(slot => `${slot.char}:${slot.encryptionLayers}:${slot.hasInput ? 1 : 0}`).join('|')
].join('::');
return {
guessesLeft,
openPositions,
actionablePositions,
unknownCount: slotStates.filter(slot => slot.hidden).length,
contextKey: context.key,
title: context.title,
service: context.service,
signature
};
}
async function getContextObservationCounts() {
if (contextObservationCounts && typeof contextObservationCounts === 'object') {
return contextObservationCounts;
}
const stored = await idbGet('context_word_observations');
contextObservationCounts = stored && typeof stored === 'object' && !Array.isArray(stored)
? stored
: {};
return contextObservationCounts;
}
async function getWordObservationCounts() {
if (wordObservationCounts && typeof wordObservationCounts === 'object') {
return wordObservationCounts;
}
const stored = await idbGet('word_observations');
wordObservationCounts = stored && typeof stored === 'object' && !Array.isArray(stored)
? stored
: {};
return wordObservationCounts;
}
function recordWordObservation(word, contextKey = '') {
const normalized = String(word || '').trim().toUpperCase();
const normalizedContext = String(contextKey || '').trim();
if (!normalized) return Promise.resolve(0);
observationWriteChain = observationWriteChain.then(async () => {
const counts = await getWordObservationCounts();
const next = Math.max(0, Number(counts[normalized]) || 0) + 1;
counts[normalized] = next;
await idbSet('word_observations', counts);
if (normalizedContext) {
const contextual = await getContextObservationCounts();
if (!contextual[normalizedContext] || typeof contextual[normalizedContext] !== 'object') {
contextual[normalizedContext] = {};
}
contextual[normalizedContext][normalized] =
Math.max(0, Number(contextual[normalizedContext][normalized]) || 0) + 1;
await idbSet('context_word_observations', contextual);
}
return next;
}).catch((err) => {
crackLog('Failed to record word observation:', normalized, err);
return 0;
});
return observationWriteChain;
}
async function analyzePattern(pattern, rowKey, actionablePositions = [], contextKey = '') {
const len = pattern.length;
const empty = { suggestions: [], positions: [], bestGuess: null, matchCount: 0, totalWeight: 0 };
if (len < MIN_LENGTH || len > MAX_LENGTH) return empty;
if (!dict[len]) {
const chunk = await idbGet(`len_${len}`);
if (!chunk) return empty;
dict[len] = chunk;
}
const maxSug = getClampedIntPref(
PREF_MAX_SUGGESTIONS,
DEFAULT_MAX_SUGGESTIONS,
MIN_MAX_SUGGESTIONS,
MAX_MAX_SUGGESTIONS
);
const exSets = loadExclusions(rowKey, len);
const exclusions = exSets.map(set => Array.from(set || []));
const observations = await getWordObservationCounts();
const allContextObservations = await getContextObservationCounts();
const contextObservations = contextKey && allContextObservations[contextKey]
? allContextObservations[contextKey]
: {};
const workerUrl = URL.createObjectURL(new Blob([`
function patternMatches(word, pattern) {
if (!word || word.length !== pattern.length) return false;
for (let i = 0; i < pattern.length; i++) {
if (pattern[i] !== '*' && pattern[i] !== word[i]) return false;
}
return true;
}
function addTopCandidate(top, candidate, max) {
let lo = 0;
let hi = top.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
const cur = top[mid];
if (cur.weight > candidate.weight ||
(cur.weight === candidate.weight && cur.rank < candidate.rank)) {
lo = mid + 1;
} else {
hi = mid;
}
}
top.splice(lo, 0, candidate);
if (top.length > max) top.pop();
}
self.onmessage = function(e) {
const {
dictChunk,
pattern,
max,
exclusions,
observations,
contextObservations,
actionablePositions
} = e.data;
const patternText = String(pattern || '');
const unknownPositions = Array.from(new Set(actionablePositions || []))
.filter(position => Number.isInteger(position) && position >= 0 &&
position < patternText.length && patternText[position] === '*');
const letterWeights = unknownPositions.map(() => Object.create(null));
const top = [];
let totalWeight = 0;
let matchCount = 0;
for (let rank = 0; rank < dictChunk.length; rank++) {
const word = dictChunk[rank];
if (!patternMatches(word, patternText)) continue;
let excluded = false;
for (let i = 0; i < word.length; i++) {
const blocked = exclusions[i];
if (blocked && blocked.includes(word[i])) {
excluded = true;
break;
}
}
if (excluded) continue;
// The source dictionary is popularity-ranked but has no raw frequency counts.
// Global Torn observations improve the prior, while same target/service
// observations receive a stronger contextual boost.
const observed = Math.max(0, Number(observations[word]) || 0);
const contextObserved = Math.max(0, Number(contextObservations[word]) || 0);
const rankWeight = 1 / Math.sqrt(rank + 25);
const observationBoost = 1 +
(3 * Math.log2(1 + observed)) +
(8 * Math.log2(1 + contextObserved));
const weight = rankWeight * observationBoost;
totalWeight += weight;
matchCount++;
for (let p = 0; p < unknownPositions.length; p++) {
const ch = word[unknownPositions[p]];
letterWeights[p][ch] = (letterWeights[p][ch] || 0) + weight;
}
addTopCandidate(top, { word, weight, rank, observed, contextObserved }, max);
}
const suggestions = top.map(item => ({
word: item.word,
probability: totalWeight > 0 ? item.weight / totalWeight : 0,
rank: item.rank + 1,
observed: item.observed,
contextObserved: item.contextObserved
}));
const positions = unknownPositions.map((position, idx) => {
const letters = Object.entries(letterWeights[idx])
.map(([letter, weight]) => ({
letter,
probability: totalWeight > 0 ? weight / totalWeight : 0
}))
.sort((a, b) => b.probability - a.probability || a.letter.localeCompare(b.letter));
return { position, letters: letters.slice(0, 5) };
});
let bestGuess = null;
for (const pos of positions) {
const first = pos.letters[0];
if (!first) continue;
if (!bestGuess || first.probability > bestGuess.probability) {
bestGuess = {
position: pos.position,
letter: first.letter,
probability: first.probability
};
}
}
self.postMessage({ suggestions, positions, bestGuess, matchCount, totalWeight });
};
`], { type: 'application/javascript' }));
const worker = new Worker(workerUrl);
return await new Promise((resolve) => {
let done = false;
const finish = (value) => {
if (done) return;
done = true;
try { worker.terminate(); } catch (_) {}
try { URL.revokeObjectURL(workerUrl); } catch (_) {}
resolve(value || empty);
};
worker.onmessage = (e) => finish(e.data);
worker.onerror = () => finish(empty);
worker.postMessage({
dictChunk: dict[len],
pattern: pattern.toUpperCase(),
max: maxSug,
exclusions,
observations,
contextObservations,
actionablePositions
});
});
}
function prependPanelToRow(row, pat, rowKey, rowState) {
let panel = row.querySelector('.__crackhelp_panel');
const state = rowState || {
guessesLeft: null,
openPositions: [],
actionablePositions: [],
unknownCount: 0,
contextKey: '',
title: '',
service: '',
signature: ''
};
if (!panel) {
panel = document.createElement('div');
panel.className = '__crackhelp_panel';
panel.dataset.rowkey = rowKey;
panel.dataset.pattern = pat;
panel.dataset.dictRevision = String(dictRevision);
panel.dataset.stateSignature = state.signature || '';
panel._rowState = state;
panel._seq = 0;
panel.style.cssText = 'text-align:center; position:absolute; z-index:9999; white-space:normal;';
panel.style.border = `1px solid ${getTheme().uiBorder}`;
panel.style.borderRadius = '4px';
const listDiv = document.createElement('div');
listDiv.style.cssText = 'padding:2px; display:flex; align-items:center; justify-content:flex-start; align-content:flex-start; flex-wrap:wrap; gap:2px; line-height:1.2;';
panel.appendChild(listDiv);
function addChip(text, kind, title = '', messageState = '') {
const sp = document.createElement('span');
sp.dataset.kind = kind;
if (messageState) sp.dataset.msgState = messageState;
sp.textContent = text;
sp.title = title;
if (kind === 'msg') styleMessageChip(sp);
else styleSugSpan(sp);
if (kind === 'prob-best') {
sp.style.fontWeight = 'bold';
sp.style.border = `1px solid ${getTheme().uiBorder}`;
} else if (kind === 'prob-position') {
sp.style.opacity = '0.95';
}
listDiv.appendChild(sp);
return sp;
}
panel.updateSuggestions = async function () {
const curPat = panel.dataset.pattern || '';
const curRowKey = panel.dataset.rowkey;
const currentState = panel._rowState || state;
const guessesLeft = currentState.guessesLeft;
const openPositions = currentState.openPositions || [];
const actionablePositions = currentState.actionablePositions || [];
const showOnComplete = getBoolPref(PREF_SHOW_SUG_ON_COMPLETE, true);
if (!showOnComplete && curPat && !curPat.includes('*')) {
if (listDiv.childNodes.length) listDiv.innerHTML = '';
return;
}
applyPanelTheme(panel);
if (!dictLoaded && dictLoading) {
listDiv.innerHTML = '';
addChip('(loading dictionary…)', 'msg', '', 'loading');
return;
}
const seq = ++panel._seq;
const result = await analyzePattern(
curPat,
curRowKey,
actionablePositions,
currentState.contextKey || ''
);
if (seq !== panel._seq) return;
listDiv.innerHTML = '';
const isComplete = curPat && !curPat.includes('*');
if (isComplete) {
addChip('PASSWORD FOUND', 'msg', 'The complete password is visible.', 'complete');
} else if (guessesLeft === 0) {
addChip(
'NO GUESSES LEFT — BRUTE FORCE ONLY',
'msg',
`${openPositions.length} unencrypted hidden slot${openPositions.length === 1 ? '' : 's'} remain, but Torn reports no guesses left.`,
'no-guesses'
);
} else if (currentState.unknownCount > 0 && openPositions.length === 0) {
addChip(
'NO OPEN SLOTS — BRUTE FORCE REQUIRED',
'msg',
'Every remaining hidden slot is still encrypted.',
'blocked'
);
}
if (!result || result.matchCount === 0) {
addChip(
dictLoaded ? '(no matches)' : '(loading dictionary…)',
'msg',
'',
dictLoaded ? 'no-match' : 'loading'
);
return;
}
if (result.bestGuess && guessesLeft !== 0) {
const best = result.bestGuess;
addChip(
`★ TRY ${best.position + 1}:${best.letter} ${formatProbability(best.probability)}`,
'prob-best',
`Highest estimated success chance among currently unencrypted, guessable slots and ${result.matchCount.toLocaleString()} matching words.`
);
}
const positionSummaries = [];
const positionDetails = [];
for (const pos of result.positions) {
if (!pos.letters || !pos.letters.length) continue;
const top = pos.letters[0];
positionSummaries.push(`${pos.position + 1}:${top.letter} ${formatProbability(top.probability)}`);
positionDetails.push(
`Position ${pos.position + 1}: ` + pos.letters
.map(x => `${x.letter} ${formatProbability(x.probability)}`)
.join(' · ')
);
}
if (positionSummaries.length && guessesLeft !== 0) {
addChip(
`LETTERS ${positionSummaries.join(' · ')}`,
'prob-position',
positionDetails.join('\n')
);
}
if (result.suggestions.length) {
const sep = document.createElement('span');
sep.textContent = '|';
sep.style.opacity = '0.65';
listDiv.appendChild(sep);
}
for (const item of result.suggestions) {
const observedText = item.observed
? ` · observed globally ${item.observed} time${item.observed === 1 ? '' : 's'}`
: '';
const contextText = item.contextObserved
? ` · observed for this target/service ${item.contextObserved} time${item.contextObserved === 1 ? '' : 's'}`
: '';
addChip(
`${item.word} ${formatProbability(item.probability)}`,
'sug',
`Estimated share of current matches · source rank ${item.rank.toLocaleString()}${observedText}${contextText}`
);
}
};
row.prepend(panel);
applyPanelTheme(panel);
positionPanelBesideWord(row, panel);
schedulePanelUpdate(panel);
} else {
const patternChanged = panel.dataset.pattern !== pat;
const dictionaryChanged = panel.dataset.dictRevision !== String(dictRevision);
const stateChanged = panel.dataset.stateSignature !== (state.signature || '');
panel.dataset.pattern = pat;
panel.dataset.dictRevision = String(dictRevision);
panel.dataset.stateSignature = state.signature || '';
panel._rowState = state;
applyPanelTheme(panel);
positionPanelBesideWord(row, panel);
if (patternChanged || dictionaryChanged || stateChanged) 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);
dictRevision++;
crackLog('Added to local cache:', word);
}
}
function getRowKey(crimeOption) {
if (!crimeOption.dataset.crackKey) {
crimeOption.dataset.crackKey = String(Date.now()) + '-' + Math.floor(Math.random() * 100000);
}
return crimeOption.dataset.crackKey;
}
function resetRowTracking(crimeOption) {
const oldKey = crimeOption.dataset.crackKey;
if (oldKey) {
prevRowStates.delete(oldKey);
const panel = crimeOption.querySelector('.__crackhelp_panel');
if (panel && panel.parentNode) {
const key = panel.dataset.rowkey;
if (panelUpdateTimers.has(key)) {
clearTimeout(panelUpdateTimers.get(key));
panelUpdateTimers.delete(key);
}
panel.parentNode.removeChild(panel);
}
}
delete crimeOption.dataset.crackCompletedWord;
crimeOption.dataset.crackKey = String(Date.now()) + '-' + Math.floor(Math.random() * 100000);
return crimeOption.dataset.crackKey;
}
function attachSlotSensors(crimeOption) {
if (crimeOption.dataset.crackDelegated === '1') return;
crimeOption.dataset.crackDelegated = '1';
const slotSelector = '[class^="charSlot"]:not([class*="charSlotDummy"])';
const rememberGuess = (event) => {
const target = event.target;
if (!target || !target.matches || !target.matches('input[type="text"]')) return;
const slot = target.closest(slotSelector);
if (!slot || !crimeOption.contains(slot)) return;
const slots = Array.from(crimeOption.querySelectorAll(slotSelector));
const index = slots.indexOf(slot);
if (index < 0) return;
const raw = event.type === 'keydown' ? event.key : target.value;
const letter = String(raw || '').slice(-1).toUpperCase();
if (!/^[A-Z0-9._]$/.test(letter)) return;
const rowKey = getRowKey(crimeOption);
const existing = prevRowStates.get(rowKey) || {
chars: slots.map(item => readSlotState(item, 0).char),
guessesLeft: getGuessesLeft(crimeOption)
};
existing.pendingGuess = {
i: index,
letter,
time: performance.now(),
guessesBefore: Number.isFinite(existing.guessesLeft)
? existing.guessesLeft
: getGuessesLeft(crimeOption)
};
prevRowStates.set(rowKey, existing);
captureKey(letter);
};
crimeOption.addEventListener('keydown', rememberGuess, true);
crimeOption.addEventListener('input', rememberGuess, true);
}
function scanCrimePage() {
if (!runtimeStarted) return;
if (!isCrackingPage()) {
cleanupView();
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 rowKey = getRowKey(crimeOption);
attachSlotSensors(crimeOption);
const charSlots = Array.from(
crimeOption.querySelectorAll('[class^="charSlot"]:not([class*="charSlotDummy"])')
);
if (!charSlots.length) continue;
const slotStates = charSlots.map((slot, index) => readSlotState(slot, index));
const curChars = slotStates.map(slot => slot.char);
const patText = curChars.join('');
const guessesLeft = getGuessesLeft(crimeOption);
const context = getRowContext(crimeOption);
let actionState = buildRowActionState(crimeOption, slotStates, guessesLeft, context);
const now = performance.now();
const len = curChars.length;
let prev = prevRowStates.get(rowKey) || {
chars: Array(len).fill('*'),
guessesLeft,
contextKey: context.key,
pendingGuess: null
};
// Torn's virtual list recycles row elements. A changed length, changed target/service,
// or a completed row becoming fresh again means this DOM node now represents a new job.
const lengthChanged = prev.chars && prev.chars.length !== len;
const contextChanged = !!(prev.contextKey && context.key && prev.contextKey !== context.key);
const prevWasComplete = prev.chars && prev.chars.length === len && prev.chars.every(ch => ch !== '*');
const nowIsFreshHidden = curChars.length === len && curChars.every(ch => ch === '*');
if (lengthChanged || contextChanged || (prevWasComplete && nowIsFreshHidden)) {
rowKey = resetRowTracking(crimeOption);
prev = {
chars: Array(len).fill('*'),
guessesLeft,
contextKey: context.key,
pendingGuess: null
};
actionState = buildRowActionState(crimeOption, slotStates, guessesLeft, context);
}
let pendingGuess = prev.pendingGuess || null;
if (pendingGuess && pendingGuess.i >= 0 && pendingGuess.i < len) {
const age = now - pendingGuess.time;
const currentChar = curChars[pendingGuess.i];
const guessesDropped = Number.isFinite(pendingGuess.guessesBefore) &&
Number.isFinite(guessesLeft) &&
guessesLeft < pendingGuess.guessesBefore;
const wrongFlash = String(charSlots[pendingGuess.i].className || '')
.includes('incorrectGuessFlash');
if (currentChar !== '*') {
// The server returned a character, therefore the guess was correct.
pendingGuess = null;
} else if (guessesDropped || (age >= 100 && wrongFlash)) {
// The slot stayed hidden and Torn consumed a guess: record a confirmed miss.
addExclusion(rowKey, pendingGuess.i, pendingGuess.letter, len);
pendingGuess = null;
} else if (age > 4000) {
// Avoid carrying a stale pending guess after a failed/cancelled request.
pendingGuess = null;
}
}
prevRowStates.set(rowKey, {
chars: curChars,
guessesLeft,
contextKey: context.key,
pendingGuess,
slotSignature: actionState.signature,
time: now
});
if (!patText.includes('*')) {
const newWord = patText.toUpperCase();
if (!/^[A-Z0-9_.]+$/.test(newWord)) {
crackLog('Revealed word contains invalid chars. skipping:', newWord);
} else if (crimeOption.dataset.crackCompletedWord !== newWord) {
// Count one observation per completed Torn row, not once per 800 ms scan.
crimeOption.dataset.crackCompletedWord = newWord;
(async () => {
await recordWordObservation(newWord, context.key);
const localHas = await isWordInLocalDict(newWord);
const supHas = remoteWords.has(newWord);
if (!localHas && !supHas) {
await addWordToLocalCache(newWord);
await enqueueOutbox(newWord);
} else if (supHas && !localHas) {
await addWordToLocalCache(newWord);
}
const panel = crimeOption.querySelector('.__crackhelp_panel');
if (panel && panel.updateSuggestions) schedulePanelUpdate(panel);
})();
}
}
const showOnComplete = getBoolPref(PREF_SHOW_SUG_ON_COMPLETE, true);
const isComplete = patText && !patText.includes('*');
if (isComplete && !showOnComplete) {
const existing = crimeOption.querySelector('.__crackhelp_panel');
if (existing) {
const key = existing.dataset.rowkey;
if (panelUpdateTimers.has(key)) {
clearTimeout(panelUpdateTimers.get(key));
panelUpdateTimers.delete(key);
}
existing.remove();
}
} else if (!/^[*]+$/.test(patText)) {
prependPanelToRow(crimeOption, patText, rowKey, actionState);
}
}
}
/* --------------------------
Settings UI
-------------------------- */
async function showMenuOverlay() {
const theme0 = getTheme();
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed; inset: 0;
width: 100%; height: 100%;
background: ${theme0.overlayBg};
color: ${theme0.uiText};
font-size: 14px;
padding: 12px;
box-sizing: border-box;
overflow: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483647;
overflow: hidden;
overscroll-behavior: contain;
touch-action: none;
`;
const box = document.createElement('div');
box.style.cssText = `
background: ${theme0.boxBg};
padding: 18px;
border: 1px solid ${theme0.uiBorder};
border-radius: 10px;
text-align: left;
width: min(520px, 92vw);
max-width: 520px;
box-sizing: border-box;
max-height: calc(100vh - 24px);
overflow: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
`;
const isMobile = isPdaUserAgentCheck();
if (isMobile) {
overlay.style.fontSize = '13px';
overlay.style.padding = '10px';
overlay.style.alignItems = 'flex-start';
box.style.padding = '12px';
box.style.width = '100%';
box.style.maxWidth = '100%';
box.style.marginTop = '10px';
box.style.marginBottom = '10px';
}
const title = document.createElement('div');
title.textContent = 'Settings (BETA)';
title.style.cssText = `margin-bottom:12px; font-size:20px; color:${theme0.uiText}; text-align:center;`;
if (isMobile) title.style.fontSize = '18px';
box.appendChild(title);
addHr(box, theme0, 2);
const statusLine = document.createElement('div');
statusLine.style.cssText = `color:${theme0.uiText}; font-size:12px; margin-bottom:8px; text-align:center;`;
if (isMobile) statusLine.style.fontSize = '11px';
statusLine.textContent = ensureStatusBadge().textContent;
registerStatusSink(statusLine);
box.appendChild(statusLine);
const nextSyncDiv = document.createElement('div');
nextSyncDiv.style.cssText = `color:${theme0.uiText}; font-size:12px; margin-bottom:8px; text-align:center;`;
if (isMobile) nextSyncDiv.style.fontSize = '11px';
nextSyncDiv.textContent = 'Calculating next sync time…';
box.appendChild(nextSyncDiv);
const wordCountDiv = document.createElement('div');
wordCountDiv.style.cssText = `color:${theme0.uiText}; font-size:12px; margin-bottom:10px; text-align:center;`;
if (isMobile) wordCountDiv.style.fontSize = '11px';
wordCountDiv.style.whiteSpace = "pre-line";
wordCountDiv.textContent = 'Loading dictionary stats...';
box.appendChild(wordCountDiv);
addHr(box, theme0, 1);
function addRow(labelText, controlEl, parent = box) {
const row = document.createElement('div');
row.style.cssText = 'display:flex; align-items:center; justify-content:space-between; gap:12px; margin:6px 0; flex-wrap:wrap;';
const lab = document.createElement('div');
lab.textContent = labelText;
lab.style.cssText = 'font-size:12px; opacity:0.95; min-width:0; flex: 1 1 auto;';
if (isMobile) {
row.style.flexDirection = 'column';
row.style.alignItems = 'stretch';
row.style.gap = '6px';
row.style.justifyContent = 'flex-start';
lab.style.textAlign = 'left';
lab.style.fontSize = '11px';
}
row.appendChild(lab);
if (controlEl) {
const tag = (controlEl.tagName || '').toLowerCase();
const type = (controlEl.type || '').toLowerCase();
const isCheckLike = tag === 'input' && (type === 'checkbox' || type === 'radio');
if (isMobile && !isCheckLike) {
controlEl.style.width = '100%';
controlEl.style.maxWidth = '100%';
controlEl.style.boxSizing = 'border-box';
}
row.appendChild(controlEl);
}
parent.appendChild(row);
return { row, lab, controlEl };
}
const themeSel = document.createElement('select');
themeSel.style.cssText = 'padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(themeSel);
themeSel.innerHTML = `<option value="dark">Black (default)</option><option value="light">White</option>`;
themeSel.value = normalizeThemeName(getStrPref(THEME_PREF_KEY, 'dark'));
addRow('Theme preset', themeSel);
const maxSug = document.createElement('input');
maxSug.type = 'number';
maxSug.min = '1';
maxSug.max = String(MAX_MAX_SUGGESTIONS);
maxSug.step = '1';
maxSug.value = String(getClampedIntPref(PREF_MAX_SUGGESTIONS, DEFAULT_MAX_SUGGESTIONS, MIN_MAX_SUGGESTIONS, MAX_MAX_SUGGESTIONS));
maxSug.style.cssText = 'width:90px; padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(maxSug);
addRow('Max. Suggestions', maxSug);
const advColorWrap = document.createElement('div');
advColorWrap.style.cssText = 'display:flex; flex-direction:column; gap:8px; margin-top:6px;';
box.appendChild(advColorWrap);
function setAdvancedColorsVisible(on) {
advColorWrap.style.display = on ? '' : 'none';
}
const boxBg = document.createElement('input');
boxBg.type = 'text';
boxBg.value = getStrPref(PREF_UI_BOX_BG_COLOR, getThemePreset().boxBg);
boxBg.placeholder = '#000';
boxBg.style.cssText = 'width:140px; padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(boxBg);
addRow('Settings Main color', boxBg, advColorWrap);
const fontSize = document.createElement('input');
fontSize.type = 'number';
fontSize.min = '5';
fontSize.max = '20';
fontSize.step = '1';
fontSize.value = String(getIntPref(PREF_SUG_FONT_PX, 10));
fontSize.style.cssText = 'width:90px; padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(fontSize);
addRow('Suggestion font size (px)', fontSize, advColorWrap);
const sugText = document.createElement('input');
sugText.type = 'text';
sugText.value = getStrPref(PREF_SUG_TEXT_COLOR, getThemePreset().sugText);
sugText.placeholder = '#0f0';
sugText.style.cssText = 'width:140px; padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(sugText);
addRow('Suggestion text color', sugText, advColorWrap);
const sugBg = document.createElement('input');
sugBg.type = 'text';
sugBg.value = getStrPref(PREF_SUG_BG_COLOR, getThemePreset().sugBg);
sugBg.placeholder = '#000 or transparent';
sugBg.style.cssText = 'width:140px; padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(sugBg);
addRow('Suggestion panel background', sugBg, advColorWrap);
const uiText = document.createElement('input');
uiText.type = 'text';
uiText.value = getStrPref(PREF_UI_TEXT_COLOR, getThemePreset().uiText);
uiText.placeholder = '#0f0';
uiText.style.cssText = 'width:140px; padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(uiText);
addRow('Overall UI text color', uiText, advColorWrap);
const uiBg = document.createElement('input');
uiBg.type = 'text';
uiBg.value = getStrPref(PREF_UI_BG_COLOR, getThemePreset().uiBg);
uiBg.placeholder = '#000';
uiBg.style.cssText = 'width:140px; padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(uiBg);
addRow('Overall UI background', uiBg, advColorWrap);
const uiBorder = document.createElement('input');
uiBorder.type = 'text';
uiBorder.value = getStrPref(PREF_UI_BORDER_COLOR, getThemePreset().uiBorder);
uiBorder.placeholder = '#0f0';
uiBorder.style.cssText = 'width:140px; padding:4px 6px; font-size:12px;';
appplyMenuInputTheme(uiBorder);
addRow('Overall UI border color', uiBorder, advColorWrap);
const showAdvancedOptRow = document.createElement('label');
showAdvancedOptRow.style.cssText = 'cursor:pointer; display:flex; align-items:center; gap:8px; font-size:12px; margin:10px 0 6px;';
const showAdvancedOptChk = document.createElement('input');
showAdvancedOptChk.type = 'checkbox';
showAdvancedOptChk.checked = getBoolPref(PREF_SHOW_ADVANCED_OPT_COLOR, true);
const showAdvancedOptText = document.createElement('span');
showAdvancedOptText.textContent = 'Show advanced color options';
showAdvancedOptRow.appendChild(showAdvancedOptChk);
showAdvancedOptRow.appendChild(showAdvancedOptText);
if (isMobile) {
showAdvancedOptRow.style.width = '100%';
showAdvancedOptRow.style.fontSize = '11px';
}
box.appendChild(showAdvancedOptRow);
setAdvancedColorsVisible(showAdvancedOptChk.checked);
showAdvancedOptChk.addEventListener('change', () => {
setBoolPref(PREF_SHOW_ADVANCED_OPT_COLOR, showAdvancedOptChk.checked);
setAdvancedColorsVisible(showAdvancedOptChk.checked);
});
const showCompleteRow = document.createElement('label');
showCompleteRow.style.cssText = 'cursor:pointer; display:flex; align-items:center; gap:8px; font-size:12px;';
const showCompleteChk = document.createElement('input');
showCompleteChk.type = 'checkbox';
showCompleteChk.checked = getBoolPref(PREF_SHOW_SUG_ON_COMPLETE, true);
const showCompleteText = document.createElement('span');
showCompleteText.textContent = 'Show suggestions when crack is complete';
showCompleteRow.appendChild(showCompleteChk);
showCompleteRow.appendChild(showCompleteText);
box.appendChild(showCompleteRow);
const badgeRow = document.createElement('label');
badgeRow.style.cssText = 'cursor:pointer; display:flex; align-items:center; gap:8px; font-size:12px; margin-top:8px;';
const badgeChk = document.createElement('input');
badgeChk.type = 'checkbox';
badgeChk.checked = getBoolPref(STATUS_PREF_KEY, true);
const badgeText = document.createElement('span');
badgeText.textContent = 'Show status badge';
badgeRow.appendChild(badgeChk);
badgeRow.appendChild(badgeText);
box.appendChild(badgeRow);
addHr(box, theme0, 1);
const btnRow = document.createElement('div');
btnRow.style.cssText = 'display:flex; gap:8px; justify-content:center; margin-top:12px; flex-wrap:wrap;';
if (isMobile) {
btnRow.style.flexDirection = 'column';
btnRow.style.alignItems = 'stretch';
btnRow.style.justifyContent = 'flex-start';
}
const btnReset = document.createElement('button');
btnReset.textContent = 'Reset to theme preset';
btnReset.style.cssText = 'padding:6px 10px; cursor:pointer; border-radius:6px; font-size:12px;';
if (isMobile) btnReset.style.width = '100%';
applyMenuButtonTheme(btnReset);
const btnSync = document.createElement('button');
btnSync.textContent = COMMUNITY_SYNC_ENABLED ? 'Sync Community Words Now' : 'Community Sync Unavailable';
btnSync.style.cssText = 'padding:6px 10px; cursor:pointer; border-radius:6px; font-size:12px;';
if (isMobile) btnSync.style.width = '100%';
applyMenuButtonTheme(btnSync);
const btnCache = document.createElement('button');
btnCache.textContent = 'Clear Wordlist Cache';
btnCache.style.cssText = 'padding:6px 10px; background:#a00; color:#fff; cursor:pointer; border-radius:6px; font-size:12px; border:none;';
if (isMobile) btnCache.style.width = '100%';
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Close';
cancelBtn.style.cssText = 'padding:6px 10px; cursor:pointer; border-radius:6px; font-size:12px;';
if (isMobile) cancelBtn.style.width = '100%';
applyMenuButtonTheme(cancelBtn);
btnRow.appendChild(btnReset);
if (COMMUNITY_SYNC_ENABLED) btnRow.appendChild(btnSync);
btnRow.appendChild(btnCache);
btnRow.appendChild(cancelBtn);
box.appendChild(btnRow);
addHr(box, theme0, 1);
const pwrdByMsg = document.createElement('div');
pwrdByMsg.style.cssText = `color:${theme0.uiText}; font-size:12px; text-align:center; opacity:0.95;`;
pwrdByMsg.textContent = COMMUNITY_SYNC_ENABLED
? 'VM public pool + IndexedDB + probability-ranked suggestions - Made with Love ❤ by SirAua [3785905] (and friends)'
: 'Local IndexedDB mode - set COMMUNITY_POOL_ORIGIN to enable your VM public pool';
box.appendChild(pwrdByMsg);
overlay.appendChild(box);
document.body.appendChild(overlay);
const applyOverlayTheme = () => {
const t = getTheme();
overlay.style.background = t.overlayBg;
overlay.style.color = t.uiText;
box.style.background = t.boxBg;
box.style.borderColor = t.uiBorder;
title.style.color = t.uiText;
statusLine.style.color = t.uiText;
nextSyncDiv.style.color = t.uiText;
wordCountDiv.style.color = t.uiText;
pwrdByMsg.style.color = t.uiText;
applyMenuButtonTheme(btnReset);
if (COMMUNITY_SYNC_ENABLED) applyMenuButtonTheme(btnSync);
applyMenuButtonTheme(cancelBtn);
};
const onAnyPrefChange = () => {
applyThemeEverywhere();
applyOverlayTheme();
};
themeSel.onchange = () => {
applyPreset(themeSel.value);
const preset = getThemePreset();
uiText.value = getStrPref(PREF_UI_TEXT_COLOR, preset.uiText);
uiBg.value = getStrPref(PREF_UI_BG_COLOR, preset.uiBg);
uiBorder.value = getStrPref(PREF_UI_BORDER_COLOR, preset.uiBorder);
sugText.value = getStrPref(PREF_SUG_TEXT_COLOR, preset.sugText);
sugBg.value = getStrPref(PREF_SUG_BG_COLOR, preset.sugBg);
boxBg.value = getStrPref(PREF_UI_BOX_BG_COLOR, preset.boxBg);
onAnyPrefChange();
location.reload();
};
const saveMaxSuggestions = () => {
const n = clampInt(maxSug.value, MIN_MAX_SUGGESTIONS, MAX_MAX_SUGGESTIONS, DEFAULT_MAX_SUGGESTIONS);
maxSug.value = String(n);
setIntPref(PREF_MAX_SUGGESTIONS, n);
onAnyPrefChange();
};
maxSug.onchange = saveMaxSuggestions;
maxSug.oninput = saveMaxSuggestions;
boxBg.onchange = () => { setStrPref(PREF_UI_BOX_BG_COLOR, boxBg.value); onAnyPrefChange(); };
fontSize.onchange = () => { setIntPref(PREF_SUG_FONT_PX, fontSize.value); onAnyPrefChange(); };
fontSize.oninput = () => { setIntPref(PREF_SUG_FONT_PX, fontSize.value); onAnyPrefChange(); };
sugText.onchange = () => { setStrPref(PREF_SUG_TEXT_COLOR, sugText.value.trim()); onAnyPrefChange(); };
sugBg.onchange = () => { setStrPref(PREF_SUG_BG_COLOR, sugBg.value.trim()); onAnyPrefChange(); };
uiText.onchange = () => { setStrPref(PREF_UI_TEXT_COLOR, uiText.value.trim()); onAnyPrefChange(); };
uiBg.onchange = () => { setStrPref(PREF_UI_BG_COLOR, uiBg.value.trim()); onAnyPrefChange(); };
uiBorder.onchange = () => { setStrPref(PREF_UI_BORDER_COLOR, uiBorder.value.trim()); onAnyPrefChange(); };
showCompleteChk.onchange = () => { setBoolPref(PREF_SHOW_SUG_ON_COMPLETE, showCompleteChk.checked); };
setAdvancedColorsVisible(showAdvancedOptChk.checked);
badgeChk.onchange = () => {
const show = badgeChk.checked;
setBoolPref(STATUS_PREF_KEY, show);
ensureStatusBadge().style.display = show ? 'block' : 'none';
};
btnReset.onclick = () => {
applyPreset(themeSel.value);
const preset = getThemePreset();
uiText.value = getStrPref(PREF_UI_TEXT_COLOR, preset.uiText);
uiBg.value = getStrPref(PREF_UI_BG_COLOR, preset.uiBg);
uiBorder.value = getStrPref(PREF_UI_BORDER_COLOR, preset.uiBorder);
sugText.value = getStrPref(PREF_SUG_TEXT_COLOR, preset.sugText);
sugBg.value = getStrPref(PREF_SUG_BG_COLOR, preset.sugBg);
onAnyPrefChange();
};
btnSync.onclick = async () => {
if (!COMMUNITY_SYNC_ENABLED) {
setStatus('Community sync unavailable');
return;
}
btnSync.disabled = true;
const oldText = btnSync.textContent;
try {
btnSync.textContent = 'Syncing…';
await checkRemoteAndMaybeDownload(true);
await updateNextSync();
await refreshRemoteStats();
setStatus('Ready');
} catch (e) {
crackLog('Manual sync failed', e);
setStatus('Sync failed');
} finally {
btnSync.disabled = false;
btnSync.textContent = oldText;
}
};
btnCache.onclick = async () => {
await clearLocalDictCache();
location.reload();
};
let ticker = null;
let statsTimer = null;
cancelBtn.onclick = () => {
unregisterStatusSink(statusLine);
if (ticker) clearInterval(ticker);
if (statsTimer) clearInterval(statsTimer);
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
};
let stats = [];
(async () => {
for (let len = MIN_LENGTH; len <= MAX_LENGTH; len++) {
const chunk = await idbGet(`len_${len}`);
stats.push(`${len}: ${chunk ? chunk.length : 0}`);
}
if (COMMUNITY_SYNC_ENABLED) {
const remoteCount = await idbGet('cf_remote_count');
const delta = await idbGet('cf_pending_delta');
const observations = await getWordObservationCounts();
const observationTotal = Object.values(observations).reduce((sum, n) => sum + (Number(n) || 0), 0);
wordCountDiv.textContent =
splitMiddle(`Stored per length → ${stats.join(' | ')} | Remote cracked: ${remoteCount ?? 'n/a'}${delta ? ` ( +${delta} pending )` : ''} | Local observations: ${observationTotal}`);
} else {
const observations = await getWordObservationCounts();
const observationTotal = Object.values(observations).reduce((sum, n) => sum + (Number(n) || 0), 0);
wordCountDiv.textContent = splitMiddle(`Stored per length → ${stats.join(' | ')} | Community sync: disabled | Local observations: ${observationTotal}`);
}
})();
const updateNextSync = async () => {
if (!COMMUNITY_SYNC_ENABLED) {
nextSyncDiv.textContent = 'Community sync: unavailable (worker returned 404)';
return;
}
const lastSyncTs = Number(await idbGet('cf_last_sync_ts')) || 0;
const nextAllowed = lastSyncTs + SYNC_MIN_INTERVAL_MS;
const remaining = nextAllowed - Date.now();
const eligible = remaining <= 0;
const delta = Number(await idbGet('cf_pending_delta')) || 0;
nextSyncDiv.textContent = eligible
? `Next sync: now${delta ? ` ( +${delta} pending )` : ''}`
: `Next sync in ${formatShortDuration(remaining)}${delta ? ` ( +${delta} pending )` : ''}`;
};
const refreshRemoteStats = async () => {
if (COMMUNITY_SYNC_ENABLED) {
const remoteCount = await idbGet('cf_remote_count');
const delta = await idbGet('cf_pending_delta');
const observations = await getWordObservationCounts();
const observationTotal = Object.values(observations).reduce((sum, n) => sum + (Number(n) || 0), 0);
wordCountDiv.textContent =
splitMiddle(`Stored per length → ${stats.join(' | ')} | Remote cracked: ${remoteCount ?? 'n/a'}${delta ? ` ( +${delta} pending )` : ''} | Local observations: ${observationTotal}`);
} else {
const observations = await getWordObservationCounts();
const observationTotal = Object.values(observations).reduce((sum, n) => sum + (Number(n) || 0), 0);
wordCountDiv.textContent = splitMiddle(`Stored per length → ${stats.join(' | ')} | Community sync: disabled | Local observations: ${observationTotal}`);
}
};
await updateNextSync();
ticker = setInterval(updateNextSync, 1000);
statsTimer = setInterval(refreshRemoteStats, 15000);
applyOverlayTheme();
}
function injectMenuButton() {
if (!runtimeStarted) return;
if (!isCrackingPage()) {
cleanupView();
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 probability-ranked suggestions! (Click for settings)';
btn.style.cssText = 'font-size:10px; text-align:left; z-index:9999; cursor:pointer; padding:4px 6px;';
applyMenuButtonTheme(btn);
btn.onclick = showMenuOverlay;
appHeader.appendChild(btn);
ensureStatusBadge();
}
/* --------------------------
Runtime control
-------------------------- */
function startRuntime() {
if (runtimeStarted) return;
runtimeStarted = true;
ensureStatusBadge();
removeLegacyRigPreferences();
try {
if (sessionStorage.getItem('__crack_base_reload_done') === '1') {
sessionStorage.removeItem('__crack_base_reload_done');
}
} catch {}
if (localStorage.getItem(THEME_PREF_KEY) === null) applyPreset('dark');
applyThemeEverywhere();
setStatus('Initializing…');
loadDict();
scanCrimePage();
if (!scanInterval) scanInterval = setInterval(scanCrimePage, UPDATE_INTERVAL);
if (!menuInterval) menuInterval = setInterval(injectMenuButton, UPDATE_INTERVAL);
startAutoSyncHeartbeat();
}
function stopRuntime() {
runtimeStarted = false;
cleanupView();
}
function onRouteChange() {
if (isCrackingPage()) {
startRuntime();
} else {
stopRuntime();
}
}
/* --------------------------
Init
-------------------------- */
onRouteChange();
window.addEventListener('hashchange', onRouteChange);
window.addEventListener('popstate', onRouteChange);
const pushState = history.pushState;
const replaceState = history.replaceState;
history.pushState = function (...args) {
const ret = pushState.apply(this, args);
setTimeout(onRouteChange, 0);
return ret;
};
history.replaceState = function (...args) {
const ret = replaceState.apply(this, args);
setTimeout(onRouteChange, 0);
return ret;
};
})();