// ==UserScript== // @name GenGen-RMJ-DET // @namespace http://tampermonkey.net/ // @version 4.5.6 // @author GenGen 队 // @match https://www.luogu.com.cn/record/cf/* // @match https://www.luogu.com.cn/record/at/* // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect codeforces.com // @connect www.luogu.com.cn // @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 handle = _gmGet('cf-handle', '') || _gmGet('rmj-cf-handle', '') || ''; const code = _gmGet(`{${cid},${sid}}`, ''); document.title = 'GenGen RMJ 加载中'; 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 }; 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' }; /* ── 网络请求 ── */ 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 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; } /* ── 数据构建(只保留最后一个点,带详细数据,分数全 null) ── */ function buildCases(sub, time, memKB) { 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; // 这个 ID 要加1 status = (sub.verdict === 'TESTING') ? 1 : lgCode; } return [{ id: `${idNum}`, status: 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' }); /* ── 页面世界钩子(恢复 SharedWorker/WS 劫持) ── */ 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' }); } } }); })(); `; /* ── HTML 构建 ── */ function buildHtml(tpl, sub) { tpl = tpl.replace(/]*>/i, m => m + '