// ==UserScript== // @name GenGen-RMJ-DET // @namespace http://tampermonkey.net/ // @version 5.1.2 // @author GenGen 队 // @match https://www.luogu.com.cn/record/cf/* // @match https://www.luogu.com.cn/record/at/* // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect codeforces.com // @connect www.luogu.com.cn // @connect atcoder.jp // @run-at document-start // @description GenGen RMJ 提交记录详情 // @license MIT // ==/UserScript== (function() { const _gmGet = GM_getValue; const _gmReq = GM_xmlhttpRequest; const VALID = [ /^https:\/\/www\.luogu\.com\.cn\/record\/cf\/[^/]+\/[^/?#]+/, /^https:\/\/www\.luogu\.com\.cn\/record\/at\/[^/]+\/[^/?#]+/, ]; if (!VALID.some(r => r.test(location.href))) return; const m = location.pathname.match(/\/record\/(cf|at)\/([^/]+)\/([^/?#]+)/); if (!m) return; const [, plat, cid, sid] = m; const isCF = plat === 'cf'; const isAT = plat === 'at'; const handle = _gmGet('cf-handle', '') || _gmGet('rmj-cf-handle', '') || ''; const code = _gmGet(`{${cid},${sid}}`, ''); const titleTimer = setInterval(() => { document.title = 'GenGen RMJ 加载中'; }, 100); const mask = document.createElement('div'); mask.id = 'rmj-mask'; mask.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgb(239,239,239);z-index:2147483647;'; (document.documentElement || document.body || document).appendChild(mask); const CF2LG = { OK:12, WRONG_ANSWER:6, TIME_LIMIT_EXCEEDED:5, MEMORY_LIMIT_EXCEEDED:4, OUTPUT_LIMIT_EXCEEDED:3, RUNTIME_ERROR:7, COMPILATION_ERROR:2, TESTING:1, WAITING:0 }; const FINAL_CF = new Set(['OK','WRONG_ANSWER','COMPILATION_ERROR','RUNTIME_ERROR', 'TIME_LIMIT_EXCEEDED','MEMORY_LIMIT_EXCEEDED','OUTPUT_LIMIT_EXCEEDED']); const gmJson = (url) => new Promise((ok, no) => { _gmReq({ method:'GET', url, timeout:15000, anonymous:true, onload: r => { try { ok(JSON.parse(r.responseText)); } catch (e) { no(e); } }, onerror: () => no(new Error('net')), ontimeout: () => no(new Error('timeout')) }); }); const gmText = (url) => new Promise((ok, no) => { _gmReq({ method:'GET', url, timeout:15000, anonymous:true, onload: r => r.status === 200 ? ok(r.responseText) : no(new Error('status ' + r.status)), onerror: () => no(new Error('net')), ontimeout: () => no(new Error('timeout')) }); }); let cachedTpl = null; const fetchTemplate = async () => { if (cachedTpl) return cachedTpl; return new Promise((ok, no) => { _gmReq({ method:'GET', url:'https://www.luogu.com.cn/record/290510067', timeout:15000, onload: r => { if (r.status === 200) { cachedTpl = r.responseText; ok(cachedTpl); } else { no(new Error('tpl')); } }, onerror: () => no(new Error('tpl net')), ontimeout: () => no(new Error('tpl timeout')) }); }); }; async function findCF() { let url = `https://codeforces.com/api/contest.status?contestId=${encodeURIComponent(cid)}&from=1&count=100`; if (handle) url += `&handle=${encodeURIComponent(handle)}`; const d = await gmJson(url); if (d.status !== 'OK') throw new Error('CF ' + d.status); return (d.result || []).find(x => String(x.id) === sid) || null; } function parseATStatus(text) { if (!text) return 'TESTING'; text = text.trim(); if (text === 'AC') return 'OK'; if (text === 'WA') return 'WRONG_ANSWER'; if (text === 'TLE' || text.includes('TLE')) return 'TIME_LIMIT_EXCEEDED'; if (text === 'MLE') return 'MEMORY_LIMIT_EXCEEDED'; if (text === 'OLE') return 'OUTPUT_LIMIT_EXCEEDED'; if (text === 'RE') return 'RUNTIME_ERROR'; if (text === 'CE') return 'COMPILATION_ERROR'; if (text === 'WJ') return 'WAITING'; if (text.includes('WA')) return 'WRONG_ANSWER'; return 'TESTING'; } async function findAT() { const url = `https://atcoder.jp/contests/${cid}/submissions/${sid}`; const html = await gmText(url); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const metaTable = doc.querySelector('table.table-bordered'); const metaRows = metaTable ? metaTable.querySelectorAll('tr') : []; const meta = {}; metaRows.forEach(row => { const th = row.querySelector('th'); const td = row.querySelector('td'); if (th && td) meta[th.textContent.trim()] = td.textContent.trim(); }); const codeEl = doc.querySelector('#submission-code'); const sourceCode = codeEl ? codeEl.textContent : ''; const tables = doc.querySelectorAll('table.table-bordered'); const cases = []; let passedCount = 0; let caseTable = null; for (const t of tables) { const header = t.querySelector('thead th, tr th'); if (header && (header.textContent.includes('Case Name') || header.textContent.includes('Status'))) { caseTable = t; break; } } if (caseTable) { caseTable.querySelectorAll('tbody tr').forEach((row, index) => { const tds = row.querySelectorAll('td'); if (tds.length >= 4) { const statusText = tds[1].textContent.trim(); const timeMatch = tds[2].textContent.trim().match(/(\d+)/); const memMatch = tds[3].textContent.trim().match(/(\d+)/); const verdict = parseATStatus(statusText); if (verdict === 'OK') passedCount++; cases.push({ id: `${index + 1}`, status: verdict, time: timeMatch ? parseInt(timeMatch[1]) : null, memKB: memMatch ? parseInt(memMatch[1]) : null }); } }); } let overallVerdict = parseATStatus(meta['Status'] || ''); if (overallVerdict === 'OK' && passedCount < cases.length && cases.length > 0) overallVerdict = 'TESTING'; const timeMatch = (meta['Exec Time'] || '').match(/(\d+)/); const memMatch = (meta['Memory'] || '').match(/(\d+)/); const taskText = meta['Task'] || ''; let taskIndex = 'a'; const taskLink = metaTable ? metaTable.querySelector('a[href*="/tasks/"]') : null; if (taskLink) { const linkMatch = (taskLink.getAttribute('href') || '').match(/\/tasks\/[^_]+_(.+)$/i); if (linkMatch) taskIndex = linkMatch[1].toLowerCase(); } else { const textMatch = taskText.match(/^([A-Za-z0-9]+)/); if (textMatch) taskIndex = textMatch[1].toLowerCase(); } let submitTime = 0; const timeStr = meta['Submission Time'] || ''; if (timeStr) { const tm = timeStr.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})([+-]\d{2})(\d{2})$/); submitTime = tm ? Math.floor(new Date(`${tm[1]}T${tm[2]}${tm[3]}:${tm[4]}`).getTime() / 1000) : (Math.floor(new Date(timeStr).getTime() / 1000) || 0); } return { isAT: true, verdict: overallVerdict, passedTestCount: passedCount, timeConsumedMillis: timeMatch ? parseInt(timeMatch[1]) : 0, memoryConsumedBytes: memMatch ? parseInt(memMatch[1]) * 1024 : 0, problem: { name: taskText, index: taskIndex }, sourceCode, cases, creationTimeSeconds: submitTime }; } function buildCases(sub, time, memKB) { if (sub.isAT) { return sub.cases.map((c, index) => ({ id: index, status: CF2LG[c.status] ?? 0, time: c.time ?? 0, memory: c.memKB ?? 0, score: 1, signal: 0, exitCode: 0, description: "", subtaskID: 0 })); } const lgCode = CF2LG[sub.verdict] ?? 0; const isAC = sub.verdict === 'OK'; const isCE = sub.verdict === 'COMPILATION_ERROR'; const passed = sub.passedTestCount ?? 0; if (isCE) return []; let idNum = isAC ? (passed > 0 ? passed - 1 : 0) : passed; let status = isAC ? 12 : lgCode; if (sub.verdict === 'TESTING') status = 1; return [{ id: idNum, status, time: time ?? 0, memory: memKB ?? 0, score: 100, signal: 0, exitCode: 0, description: "", subtaskID: 0 }]; } function buildHtml(tpl, sub) { const FE_RE = /`); } async function renderPage(sub) { const tpl = await fetchTemplate().catch(() => null); if (!tpl) return; const parser = new DOMParser(); const doc = parser.parseFromString(buildHtml(tpl, sub), 'text/html'); document.replaceChild(doc.documentElement, document.documentElement); document.querySelectorAll('script').forEach(oldScript => { const newScript = document.createElement('script'); Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value)); newScript.textContent = oldScript.textContent; oldScript.parentNode.replaceChild(newScript, oldScript); }); } (async () => { if (window.__RMJ_MAIN_RAN__) return; window.__RMJ_MAIN_RAN__ = true; const updateCache = (sub) => { if (!sub) return; if (isCF) { const CACHE_KEY_CF = 'cf-submissions-cache'; let cache = []; try { cache = JSON.parse(GM_getValue(CACHE_KEY_CF, '[]')); if (!Array.isArray(cache)) cache = []; } catch { cache = []; } const item = { id: String(sid), taskDisplay: `CF${cid}${sub.problem.index} - ${sub.problem.name}`, status: sub.verdict || 'Waiting', timestamp: (sub.creationTimeSeconds || 0) * 1000, submitUrl: `https://www.luogu.com.cn/record/cf/${cid}/${sid}`, luoguUrl: `https://www.luogu.com.cn/problem/CF${cid}${sub.problem.index}`, passedTestCount: sub.passedTestCount }; const idx = cache.findIndex(c => c.id === String(sid)); if (idx !== -1) cache[idx] = { ...cache[idx], ...item }; else cache.unshift(item); cache.sort((a, b) => Number(b.id) - Number(a.id)); GM_setValue(CACHE_KEY_CF, JSON.stringify(cache)); GM_setValue('cf-last-update', Date.now()); } else if (isAT) { const CACHE_KEY_AT = 'at-submissions-cache'; let cache = []; try { cache = JSON.parse(GM_getValue(CACHE_KEY_AT, '[]')); if (!Array.isArray(cache)) cache = []; } catch { cache = []; } const AT_VERDICT_MAP = { 'OK': 'AC', 'WRONG_ANSWER': 'WA', 'TIME_LIMIT_EXCEEDED': 'TLE', 'MEMORY_LIMIT_EXCEEDED': 'MLE', 'OUTPUT_LIMIT_EXCEEDED': 'OLE', 'RUNTIME_ERROR': 'RE', 'COMPILATION_ERROR': 'CE', 'WAITING': 'WJ', 'TESTING': 'WJ' }; const atStatus = AT_VERDICT_MAP[sub.verdict] || 'WJ'; const FINAL_AT = new Set(['AC', 'WA', 'CE', 'RE', 'TLE', 'MLE', 'OLE', 'IE']); const taskKey = `${cid}_${sub.problem.index}`; const item = { id: String(sid), taskKey: taskKey, taskDisplay: `${taskKey} - ${sub.problem.index.toUpperCase()} - ${sub.problem.name}`, status: atStatus, submitUrl: `https://atcoder.jp/contests/${cid}/submissions/${sid}`, luoguUrl: `https://www.luogu.com.cn/problem/AT_${taskKey}`, timestamp: (sub.creationTimeSeconds || 0) * 1000, isFinal: FINAL_AT.has(atStatus) }; const idx = cache.findIndex(c => c.id === String(sid)); if (idx !== -1) cache[idx] = { ...cache[idx], ...item }; else cache.unshift(item); cache.sort((a, b) => Number(b.id) - Number(a.id)); GM_setValue(CACHE_KEY_AT, JSON.stringify(cache)); GM_setValue('at-last-update', Date.now()); } }; const cfDomainPath = Number(cid) > 100000 ? 'gym' : 'contest'; const origUrl = isCF ? `https://codeforces.com/${cfDomainPath}/${cid}/submission/${sid}` : `https://atcoder.jp/contests/${cid}/submissions/${sid}`; let sub = null; try { sub = isCF ? await findCF() : (isAT ? await findAT() : null); } catch (e) { if (isAT) { clearInterval(titleTimer); location.replace(origUrl); return; } } if (isCF && sub === null) { clearInterval(titleTimer); location.replace(origUrl); return; } if (sub) updateCache(sub); clearInterval(titleTimer); let initialSub = sub; if (FINAL_CF.has(sub.verdict)) { initialSub = sub.isAT ? { ...sub, verdict: 'TESTING', cases: sub.cases.map(c => ({...c, status: 'TESTING'})) } : { ...sub, verdict: 'TESTING', passedTestCount: Math.max(0, (sub.passedTestCount ?? 1) - 1) }; } await renderPage(initialSub); if (FINAL_CF.has(sub.verdict)) { setTimeout(() => renderPage(sub), 600); } let currentSub = sub; let atWaitPollingId = null; setInterval(() => { const m = document.getElementById('rmj-mask'); if (m && (document.querySelector('#app') || document.querySelector('.lfe-body'))) m.remove(); const hasCode = (isAT && currentSub && currentSub.sourceCode) || code; if (!hasCode) { const vals = document.querySelectorAll('.value.lfe-caption'); if (vals.length >= 2 && vals[1].textContent !== '-') vals[1].textContent = '-'; const ce = document.querySelector('pre code'); if (ce) { const pre = ce.closest('pre'); if (pre) pre.outerHTML = `