// ==UserScript== // @name GenGen-RMJ-DET // @namespace http://tampermonkey.net/ // @version 4.6.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 TC = 'Luogu\\DataClass\\Record\\JudgeResult\\TestCaseJudgeResult'; const VERB_DESC = { 'OK': 'ok accepted', 'WRONG_ANSWER': 'wrong answer', 'TIME_LIMIT_EXCEEDED': 'time limit exceeded', 'MEMORY_LIMIT_EXCEEDED': 'memory limit exceeded', 'OUTPUT_LIMIT_EXCEEDED': 'output limit exceeded', 'RUNTIME_ERROR': 'runtime error', 'COMPILATION_ERROR': 'compilation error', 'TESTING': 'judging', 'WAITING': 'waiting' }; 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')) }); }); const fetchTemplate = () => new Promise((ok, no) => { _gmReq({ method:'GET', url:'https://www.luogu.com.cn/record/290510067', timeout:15000, onload: r => r.status === 200 ? ok(r.responseText) : 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 => ({ id: c.id, status: CF2LG[c.status] ?? 0, description: VERB_DESC[c.status] || String(c.status).toLowerCase().replace(/_/g, ' '), score: null, signal: 0, exitCode: 0, subtaskID: 0, __CLASS_NAME: TC, time: c.time ?? null, memory: c.memKB ?? null })); } 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, status; if (isAC) { idNum = passed > 0 ? passed : 1; status = 12; } else { idNum = passed + 1; status = (sub.verdict === 'TESTING') ? 1 : lgCode; } return [{ id: `${idNum}`, status, description: VERB_DESC[sub.verdict] || String(sub.verdict).toLowerCase().replace(/_/g, ' '), score: null, signal: 0, exitCode: 0, subtaskID: 0, __CLASS_NAME: TC, time: time ?? null, memory: memKB ?? null }]; } function buildRecordSubset(sub, recId) { const lgCode = CF2LG[sub.verdict] ?? 0; const isCE = sub.verdict === 'COMPILATION_ERROR'; const isFinal = FINAL_CF.has(sub.verdict); const time = isFinal ? (sub.timeConsumedMillis ?? 0) : 0; const memKB = isFinal ? Math.round((sub.memoryConsumedBytes ?? 0) / 1024) : 0; const cases = buildCases(sub, time, memKB); const tcMap = {}; cases.forEach(c => { tcMap[c.id] = c; }); return JSON.parse(JSON.stringify({ detail: { __CLASS_NAME:'Luogu\\DataClass\\Record\\RecordDetail', compileResult:{ __CLASS_NAME:'Luogu\\DataClass\\Record\\JudgeResult\\CompileResult', message:null, opt2:false, success:!isCE }, judgeResult:{ __CLASS_NAME:'Luogu\\DataClass\\Record\\JudgeResult\\JudgeResult', finishedCaseCount:cases.length, memory:memKB, score: null, status:lgCode, subtasks:[{ __CLASS_NAME:'Luogu\\DataClass\\Record\\JudgeResult\\SubtaskJudgeResult', id:0, judger:'', memory:memKB, score: null, status:lgCode, testCases:tcMap, time }], time }, version:400 }, enableO2:true, id: Number(recId), memory:memKB, status:lgCode, time, score: null, })); } const buildStatusPush = (sub, id) => ({ _channel:'record.track', _channel_param:String(id), _ws_type:'server_broadcast', record:buildRecordSubset(sub, id), type:'status_push' }); const PAGE_HOOK = `(function(){var PORT=null,latestPush=null;class FakePort extends EventTarget{constructor(){super();this._om=null;}get onmessage(){return this._om;}set onmessage(fn){this._om=fn;if(latestPush&&typeof this._om==='function'){setTimeout(()=>{try{this._om({type:'message',data:JSON.stringify(latestPush)+'\\n'});}catch(e){}},500);}}start(){}close(){}postMessage(msg){let p=null;try{if(typeof msg==='string')p=JSON.parse(msg);else if(msg&&msg.type==='join_channel')p=msg;else if(msg&&msg.func==='send'&&Array.isArray(msg.parameters)){for(let item of msg.parameters){if(typeof item==='string'){p=JSON.parse(item);break;}}}}catch(e){}if(p&&latestPush){var jr={_channel:'record.track',_channel_param:p.channel_param||latestPush._channel_param,_ws_type:'join_result',client_number:1,result:'success',type:'result',welcome_message:{detail:latestPush.record.detail,memory:null,record:latestPush.record,score:latestPush.record.score||null,status:latestPush.record.status,time:null,type:'status_push'}};setTimeout(()=>this._emit({type:'message',data:JSON.stringify(jr)+'\\n'}),200);}}_emit(data){var ev=new MessageEvent('message',{data:data});this.dispatchEvent(ev);if(typeof this._om==='function')try{this._om(ev);}catch(err){}}}function FakeSW(){this.port=PORT=new FakePort();}if(window.SharedWorker){var OSW=window.SharedWorker;window.SharedWorker=new Proxy(OSW,{construct:function(){return new FakeSW();}});}window.addEventListener('message',function(e){if(e.data&&e.data.__rmj_ws_push__){latestPush=e.data.payload;if(PORT&&typeof PORT._om==='function'){PORT._emit({type:'message',data:JSON.stringify(latestPush)+'\\n'});}}});})();`; function buildHtml(tpl, sub) { tpl = tpl.replace(/]*>/i, m => m + '