mintegral checker

This commit is contained in:
2026-06-01 23:18:06 +08:00
parent 9e919756fd
commit 2524bacb5d
20 changed files with 111 additions and 579 deletions

View File

@@ -577,7 +577,7 @@ async function loadZip(panel: vscode.WebviewPanel, zipPath: string): Promise<voi
const html = index.data.toString('utf8');
const instrumented = injectMonitoringStubs(html);
const checks = runStaticChecks(html, entries, buf.length);
const externalUrl = await openExternalRuntime({
const runtimeUrl = await openEmbeddedRuntime({
fileName: path.basename(zipPath),
zipSize: buf.length,
html: instrumented,
@@ -591,19 +591,17 @@ async function loadZip(panel: vscode.WebviewPanel, zipPath: string): Promise<voi
zipSize: buf.length,
html: '',
checks,
externalUrl,
runtimeUrl,
});
} catch (e: any) {
panel.webview.postMessage({ type: 'error', message: e.message || String(e) });
}
}
async function openExternalRuntime(payload: RuntimePayload): Promise<string> {
async function openEmbeddedRuntime(payload: RuntimePayload): Promise<string> {
runtimePayload = payload;
const port = await ensureRuntimeServer();
const url = `http://127.0.0.1:${port}/`;
await vscode.env.openExternal(vscode.Uri.parse(url));
return url;
return `http://127.0.0.1:${port}/`;
}
function ensureRuntimeServer(): Promise<number> {
@@ -671,49 +669,61 @@ function getExternalRuntimeHtml(payload: RuntimePayload): string {
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Mintegral Runtime Checker</title>
<style>
:root{color-scheme:dark;--gap-xs:4px;--gap-sm:8px;--gap-md:12px;--gap-lg:16px;--radius:4px;--border:var(--vscode-panel-border,rgba(128,128,128,.35));--panel:var(--vscode-editorWidget-background,var(--vscode-sideBar-background));--panel-soft:var(--vscode-textBlockQuote-background,rgba(128,128,128,.08));--fg:var(--vscode-foreground,#d4d4d4);--muted:var(--vscode-descriptionForeground,#9d9d9d);--ok:var(--vscode-testing-iconPassed,#3fb950);--err:var(--vscode-errorForeground,#f85149);--warn:var(--vscode-editorWarning-foreground,#d29922)}
*{box-sizing:border-box}
body{margin:0;background:#171717;color:#e8e8e8;font-family:Segoe UI,Arial,sans-serif;font-size:13px}
main{display:grid;grid-template-columns:minmax(360px,520px) minmax(340px,1fr);gap:16px;padding:16px;align-items:start}
.panel{border:1px solid #3a3a3a;background:#202020;border-radius:6px;overflow:hidden}
.head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border-bottom:1px solid #3a3a3a;background:#252525}
h1{font-size:16px;margin:0}.sub{color:#a9a9a9;font-size:12px}.body{padding:12px}
ol{list-style:none;margin:0;padding:0}.check{display:flex;gap:10px;padding:10px 12px;border-bottom:1px solid #333}.check:last-child{border-bottom:0}
.num{flex:0 0 24px;height:24px;border:1px solid #484848;border-radius:50%;display:grid;place-items:center;color:#bbb;font-size:11px}
.label{font-weight:650}.detail{margin-top:3px;color:#aaa;font-size:12px;overflow-wrap:anywhere}
.badge{display:inline-block;margin-left:6px;padding:1px 6px;border:1px solid #555;border-radius:999px;font-size:11px;color:#ddd}
.pass{color:#5bd46f}.fail{color:#ff6b6b}.warn{color:#f1c65b}.waiting{color:#a9a9a9}
.device{width:min(380px,calc(100vw - 32px));margin:auto;background:#0b0b0b;border-radius:28px;padding:14px 10px;box-shadow:0 10px 36px #0008}
.screen{position:relative;aspect-ratio:9/16;background:#000;border-radius:12px;overflow:hidden}
html,body{width:100%;height:100%;overflow:hidden}
body{margin:0;background:var(--vscode-editor-background,#1e1e1e);color:var(--fg);font-family:var(--vscode-font-family,Segoe UI,Arial,sans-serif);font-size:var(--vscode-font-size,13px)}
main{height:100vh;display:grid;grid-template-columns:minmax(360px,1fr) minmax(320px,420px);grid-template-rows:minmax(0,1fr);gap:var(--gap-md);padding:var(--gap-md);align-items:stretch;overflow:hidden}
.panel{border:1px solid var(--border);background:var(--panel-soft);border-radius:var(--radius);overflow:hidden}
.head{display:flex;align-items:center;justify-content:space-between;gap:var(--gap-md);padding:10px 12px;border-bottom:1px solid var(--border);background:var(--panel)}
h1{margin:0;color:var(--fg);font-size:12px;font-weight:700;letter-spacing:.45px;text-transform:uppercase}.sub{color:var(--muted);font-size:12px;overflow-wrap:anywhere}.body{padding:var(--gap-md)}
.head-meta{display:flex;align-items:center;justify-content:flex-end;gap:6px;min-width:0;white-space:nowrap}.head-meta .sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.head-meta .sub+.sub::before{content:"|";margin-right:6px;color:var(--border)}.title-actions{display:flex;align-items:center;gap:var(--gap-xs);flex:0 0 auto}
ol{list-style:none;margin:0;padding:0;overflow:auto}.check{display:flex;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border)}.check:last-child{border-bottom:0}
.num{flex:0 0 22px;height:22px;border:1px solid var(--border);border-radius:50%;display:grid;place-items:center;color:var(--muted);background:var(--panel);font-size:10px;font-weight:700}
.label{font-size:12px;font-weight:600}.detail{margin-top:3px;color:var(--muted);font-size:11px;line-height:1.35;overflow-wrap:anywhere}
.badge{display:inline-flex;align-items:center;min-height:18px;margin-left:6px;padding:1px 6px;border:1px solid var(--border);border-radius:999px;font-size:11px;line-height:1.2;color:var(--muted)}
.badge.pass{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:rgba(63,185,80,.12)}.badge.fail{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:var(--vscode-inputValidation-errorBackground,rgba(248,81,73,.12))}.badge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:var(--vscode-inputValidation-warningBackground,rgba(210,153,34,.12))}.badge.waiting{color:var(--muted)}
.runtime-side{display:grid;grid-template-rows:minmax(0,1fr);gap:var(--gap-md);min-height:0}
.playable-panel{min-height:0;display:flex;flex-direction:column}
.playable-body{min-height:0;flex:1;display:flex;flex-direction:column;padding:10px}
.preview-stage{min-height:0;flex:1;display:grid;place-items:center;overflow:hidden;container-type:size}
.device{width:min(100cqw, calc(100cqh * .5625));max-width:100%;aspect-ratio:9/16;margin:auto}
@supports not (width: 100cqw){.device{height:min(100%,calc((100vw - 500px) * 1.777));max-height:calc(100vh - 106px)}}
.screen{position:relative;width:100%;height:100%;background:#000;border-radius:4px;overflow:hidden}
iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#000}
.toolbar{display:flex;gap:8px;align-items:center;justify-content:center;margin-top:10px;flex-wrap:wrap}
button{min-height:30px;padding:5px 12px;border:1px solid #555;border-radius:4px;background:#333;color:#eee;cursor:pointer}
button:hover{background:#3c3c3c}
.logs{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
ul.log{list-style:none;margin:0;padding:0;max-height:260px;overflow:auto;font-family:Consolas,monospace;font-size:12px}
ul.log li{display:flex;gap:8px;padding:4px 8px;border-bottom:1px solid #303030}
.time{color:#888;flex:0 0 auto}.msg{white-space:pre-wrap;overflow-wrap:anywhere;min-width:0}.level{font-weight:700;flex:0 0 auto}
@media(max-width:900px){main{grid-template-columns:1fr}.logs{grid-template-columns:1fr}}
button{min-height:28px;padding:5px 12px;border:1px solid transparent;border-radius:var(--radius);background:var(--vscode-button-background,#0e639c);color:var(--vscode-button-foreground,#fff);font:inherit;cursor:pointer}button:hover{background:var(--vscode-button-hoverBackground,#1177bb)}
.icon-btn{width:28px;height:28px;min-height:28px;padding:0;display:grid;place-items:center;font-size:15px;line-height:1}.icon-btn[aria-pressed="true"]{color:var(--ok)}
.icon-btn,.tab{background:var(--vscode-button-secondaryBackground,#3a3d41);color:var(--vscode-button-secondaryForeground,var(--fg));border-color:var(--border)}.icon-btn:hover,.tab:hover{background:var(--vscode-button-secondaryHoverBackground,#45494e)}
.tab-panel{min-height:0;display:flex;flex-direction:column}
.tabs{display:flex;gap:var(--gap-xs);padding:8px 8px 0;background:var(--panel);border-bottom:1px solid var(--border)}
.tab{min-height:28px;padding:4px 10px;border-bottom:0;border-radius:var(--radius) var(--radius) 0 0}
.tab.active{background:var(--panel-soft);color:var(--fg);border-bottom-color:var(--panel-soft)}
.tab-content{min-height:0;flex:1;display:none;overflow:auto}.tab-content.active{display:block}
ul.log{list-style:none;margin:0;padding:0;overflow:auto;font-family:var(--vscode-editor-font-family,Consolas,monospace);font-size:12px;min-height:0;height:100%}
ul.log li{display:flex;gap:8px;padding:4px 8px;border-bottom:1px solid var(--border)}.time{color:var(--muted);flex:0 0 auto}.msg{white-space:pre-wrap;overflow-wrap:anywhere;min-width:0}.level{font-weight:700;flex:0 0 auto}.level.pass,.pass{color:var(--ok)}.level.fail,.fail{color:var(--err)}.level.warn,.warn{color:var(--warn)}
@media(max-width:900px){html,body{overflow:auto}main{height:auto;min-height:100vh;grid-template-columns:1fr;overflow:visible}.playable-panel{height:min(72vh,640px);min-height:360px}.tab-panel{min-height:360px}}
@media(max-height:700px) and (max-width:900px){.playable-panel{height:calc(100vh - 24px);min-height:300px}}
</style>
</head>
<body>
<main>
<section class="panel">
<div class="head"><div><h1>Mintegral Runtime Checker</h1><div class="sub" id="fileInfo"></div></div><div class="sub" id="timer">60s</div></div>
<ol id="checks"></ol>
</section>
<section>
<section class="panel">
<div class="head"><h1>Playable</h1><div class="sub" id="bridge">Starting...</div></div>
<div class="body">
<div class="device"><div class="screen"><iframe id="adFrame" src="/ad?preview=true&loading=0&review=1"></iframe></div></div>
<div class="toolbar"><button id="muteBtn">Mute</button><button id="reloadBtn">Reload iframe</button></div>
<section class="runtime-side">
<section class="panel playable-panel">
<div class="head"><h1>Playable</h1><div class="title-actions"><button id="muteBtn" class="icon-btn" title="Mute" aria-label="Mute" aria-pressed="false">&#128263;</button><button id="reloadBtn" class="icon-btn" title="Reload" aria-label="Reload">&#8635;</button></div></div>
<div class="body playable-body">
<div class="preview-stage"><div class="device"><div class="screen"><iframe id="adFrame" src="/ad?preview=true&loading=0&review=1"></iframe></div></div></div>
</div>
</section>
<div class="logs">
<section class="panel"><div class="head"><h1>Runtime Events</h1></div><ul class="log" id="eventLog"></ul></section>
<section class="panel"><div class="head"><h1>Playable Console</h1></div><ul class="log" id="consoleLog"></ul></section>
</section>
<section class="panel tab-panel">
<div class="head"><h1>Checks</h1><div class="head-meta"><div class="sub" id="bridge">Starting...</div><div class="sub" id="timer">60s</div></div></div>
<div class="tabs">
<button class="tab active" data-tab="checksTab">Checklist</button>
<button class="tab" data-tab="eventsTab">Events</button>
</div>
<ol class="tab-content active" id="checksTab"></ol>
<ul class="log tab-content" id="eventsTab"></ul>
</section>
</main>
<script>
@@ -737,23 +747,27 @@ CHECKS.forEach(c=>state.checks[c.id]={status:'pending',detail:'',runtimeStatus:c
BOOT.checks.forEach(r=>{const ch=state.checks[r.id];if(ch){ch.status=r.status;ch.detail=r.detail;}});
state.checks['code-exception'].status='pass';state.checks['code-exception'].runtimeStatus='monitoring';
['cta-method','game-end','game-ready','game-start','game-close'].forEach(id=>{if(state.checks[id].status==='fail')state.checks[id].runtimeStatus=null;});
const checksEl=document.getElementById('checks'),eventLog=document.getElementById('eventLog'),consoleLog=document.getElementById('consoleLog'),adFrame=document.getElementById('adFrame'),muteBtn=document.getElementById('muteBtn');
document.getElementById('fileInfo').textContent=BOOT.fileName+' ('+Math.round(BOOT.zipSize/1024)+' KB)';
const checksEl=document.getElementById('checksTab'),eventLog=document.getElementById('eventsTab'),adFrame=document.getElementById('adFrame'),muteBtn=document.getElementById('muteBtn');
document.querySelectorAll('.tab').forEach(btn=>btn.addEventListener('click',()=>{document.querySelectorAll('.tab').forEach(b=>b.classList.remove('active'));document.querySelectorAll('.tab-content').forEach(p=>p.classList.remove('active'));btn.classList.add('active');document.getElementById(btn.dataset.tab).classList.add('active');}));
function resetChecks(){if(state.timer)clearInterval(state.timer);state.seconds=0;state.connected=false;state.hasSentNoRetry=false;CHECKS.forEach(c=>state.checks[c.id]={status:'pending',detail:'',runtimeStatus:c.runtimeEvent?'waiting':null,runtimeDetail:''});BOOT.checks.forEach(r=>{const ch=state.checks[r.id];if(ch){ch.status=r.status;ch.detail=r.detail;}});state.checks['code-exception'].status='pass';state.checks['code-exception'].runtimeStatus='monitoring';['cta-method','game-end','game-ready','game-start','game-close'].forEach(id=>{if(state.checks[id].status==='fail')state.checks[id].runtimeStatus=null;});eventLog.innerHTML='';document.getElementById('bridge').textContent='Starting...';document.getElementById('timer').textContent=state.timeout+'s';render();state.timer=startRuntimeTimer();}
function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function addLog(list,name,cls,msg){const d=new Date(),li=document.createElement('li');li.innerHTML='<span class="time">'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0')+'</span><span class="level '+(cls||'')+'">'+esc(name)+'</span><span class="msg">'+esc(msg||'')+'</span>';list.appendChild(li);list.scrollTop=list.scrollHeight;}
function render(){checksEl.innerHTML=CHECKS.map(c=>{const ch=state.checks[c.id];let s=ch.status,d=ch.detail;if(ch.runtimeStatus==='waiting'&&ch.status==='pass'){s='waiting';d=ch.detail+' - awaiting runtime confirmation';}else if(ch.runtimeStatus==='monitoring'){s='waiting';d='Monitoring for exceptions...';}else if(ch.runtimeStatus==='pass'){s='pass';d=(ch.detail||'')+(ch.runtimeDetail?' - '+ch.runtimeDetail:'');}else if(ch.runtimeStatus==='fail'){s='fail';d=ch.runtimeDetail||ch.detail;}else if(ch.runtimeStatus==='timeout'&&ch.status==='pass'){s='warn';d=ch.detail+' - not fired within '+state.timeout+'s';}else if(ch.runtimeStatus==='clean'){s='pass';d='No exceptions detected';}return '<li class="check"><span class="num">'+c.num+'</span><span><div class="label">'+esc(c.label)+' <span class="badge '+s+'">'+s.toUpperCase()+'</span></div><div class="detail">'+esc(d||'')+'</div></span></li>';}).join('');}
function sdkCheck(type,value,msg){const id=SDK_TYPE_TO_CHECK[type],ch=state.checks[id];if(!ch)return;const pass=value===1||value===true;if(type==='code'){ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail=msg||'JS error detected';addLog(eventLog,'code error','fail',ch.runtimeDetail);}else if(type==='outer_chain'){ch.runtimeStatus=pass?'pass':'fail';ch.runtimeDetail=pass?'No external resources at runtime':'External resources detected at runtime';addLog(eventLog,type,pass?'pass':'fail',ch.runtimeDetail);}else if(type==='base64'){if(!pass){ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail='Non-JS/HTML assets must be base64';addLog(eventLog,type,'warn',ch.runtimeDetail);}}else{if(pass&&(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null)){ch.runtimeStatus='pass';ch.runtimeDetail='Confirmed by SDK at '+state.seconds+'s';addLog(eventLog,type,'pass','');}else if(!pass&&(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null)){ch.runtimeStatus='fail';ch.runtimeDetail=type+' not defined';addLog(eventLog,type,'warn',ch.runtimeDetail);}}render();done();}
function inferAll(text){const s=String(text||''),out=[];if(/(^|[^a-z])gameReady([^a-z]|$)|previewer:gameReady|game_ready|\\bDISPLAYED\\b|\\bLOADED\\b/i.test(s))out.push('gameReady');if(/(^|[^a-z])gameStart([^a-z]|$)|previewer:gameStart|game_start|\\bCHALLENGE_STARTED\\b/i.test(s))out.push('gameStart');if(/(^|[^a-z])gameEnd([^a-z]|$)|previewer:gameEnd|game_end|\\bCHALLENGE_SOLVED\\b|\\bENDCARD_SHOWN\\b/i.test(s))out.push('gameEnd');if(/(^|[^a-z])gameClose([^a-z]|$)|previewer:gameClose|game_close/i.test(s))out.push('gameClose');if(/(^|[^a-z])install([^a-z]|$)|previewer:install|\\bCTA_CLICKED\\b/i.test(s))out.push('install');return out;}
function handle(msg){const name=msg.name,extra=msg.extra||{};if(name==='__ping__'){state.connected=true;document.getElementById('bridge').textContent='Monitor connected';addLog(eventLog,'Monitor connected','pass','');return;}if(name==='sdkCheck'){sdkCheck(extra.type,extra.value,extra.msg||'');return;}if(name==='console'){addLog(consoleLog,extra.level||'log',extra.level||'',extra.text||'');inferAll(extra.text||'').forEach(ev=>handle({name:ev,extra:{source:'console'}}));return;}if(name==='diagnostic'){addLog(eventLog,extra.label||'diagnostic','warn',extra.detail||'');return;}if(name==='audioContext'){addLog(eventLog,'Audio captured','pass',extra.routed?'master gain ready':'context only');return;}if(name==='exception'){const ch=state.checks['code-exception'];ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail=extra.message||'Unhandled exception';addLog(eventLog,'exception','fail',ch.runtimeDetail);render();done();return;}const id=EVENT_TO_CHECK[name];if(id){const ch=state.checks[id];if(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null){ch.runtimeStatus='pass';ch.runtimeDetail=(extra.source==='console'?'Seen in console at ':'Fired at ')+state.seconds+'s';addLog(eventLog,name,'pass',extra.source==='console'?'from console':'');render();done();}}}
function handle(msg){const name=msg.name,extra=msg.extra||{};if(name==='__ping__'){state.connected=true;document.getElementById('bridge').textContent='Monitor connected';addLog(eventLog,'Monitor connected','pass','');return;}if(name==='sdkCheck'){sdkCheck(extra.type,extra.value,extra.msg||'');return;}if(name==='console'){inferAll(extra.text||'').forEach(ev=>handle({name:ev,extra:{source:'console'}}));return;}if(name==='diagnostic'){addLog(eventLog,extra.label||'diagnostic','warn',extra.detail||'');return;}if(name==='audioContext'){addLog(eventLog,'Audio captured','pass',extra.routed?'master gain ready':'context only');return;}if(name==='exception'){const ch=state.checks['code-exception'];ch.status='fail';ch.runtimeStatus='fail';ch.runtimeDetail=extra.message||'Unhandled exception';addLog(eventLog,'exception','fail',ch.runtimeDetail);render();done();return;}const id=EVENT_TO_CHECK[name];if(id){const ch=state.checks[id];if(ch.runtimeStatus==='waiting'||ch.runtimeStatus===null){ch.runtimeStatus='pass';ch.runtimeDetail=(extra.source==='console'?'Seen in console at ':'Fired at ')+state.seconds+'s';addLog(eventLog,name,'pass',extra.source==='console'?'from console':'');render();done();}}}
window.__hplMtgNotify=(name,extra)=>handle({name,extra:extra||{}});
window.addEventListener('message',e=>{const m=e.data;if(!m)return;if(m.type==='lifecycle')handle(m);else if(typeof m.type==='string'&&m.type.startsWith('previewer:')){const map={'previewer:gameReady':'gameReady','previewer:gameStart':'gameStart','previewer:install':'install','previewer:gameEnd':'gameEnd','previewer:gameClose':'gameClose'};if(map[m.type])handle({name:map[m.type],extra:{}});if(m.type==='previewer:review'&&m.data&&Array.isArray(m.data.reviewResult)){if(!state.hasSentNoRetry){state.hasSentNoRetry=true;try{adFrame.contentWindow.postMessage({type:'previewer:review',data:{reviewResult:{no_game_retry:true}}},'*');}catch(e){}}m.data.reviewResult.forEach(r=>sdkCheck(r.type,r.value,r.msg||''));}}});
function applyMute(){try{const cw=adFrame.contentWindow;if(!cw)return;cw.__hplMuted=state.muted;if(typeof cw.__hplApplyMutedState==='function')cw.__hplApplyMutedState();(cw.__hplMasterGains||[]).forEach(g=>{try{if(g&&g.gain)g.gain.value=state.muted?0:1;}catch(e){}});(cw.__hplMediaElements||[]).forEach(el=>{try{el.muted=state.muted;el.volume=state.muted?0:1;}catch(e){}});}catch(e){}}
muteBtn.onclick=()=>{state.muted=!state.muted;muteBtn.textContent=state.muted?'Unmute':'Mute';applyMute();addLog(eventLog,state.muted?'Muted':'Unmuted','pass','');};
document.getElementById('reloadBtn').onclick=()=>{state.connected=false;document.getElementById('bridge').textContent='Reloading...';adFrame.src='/ad?preview=true&loading=0&review=1&t='+Date.now();};
function renderMuteButton(){muteBtn.innerHTML=state.muted?'&#128264;':'&#128263;';muteBtn.title=state.muted?'Unmute':'Mute';muteBtn.setAttribute('aria-label',state.muted?'Unmute':'Mute');muteBtn.setAttribute('aria-pressed',state.muted?'true':'false');}
muteBtn.onclick=()=>{state.muted=!state.muted;renderMuteButton();applyMute();addLog(eventLog,state.muted?'Muted':'Unmuted','pass','');};
document.getElementById('reloadBtn').onclick=()=>{resetChecks();document.getElementById('bridge').textContent='Reloading...';adFrame.src='/ad?preview=true&loading=0&review=1&t='+Date.now();};
['pointerdown','click','touchstart','keydown'].forEach(evt=>window.addEventListener(evt,()=>setTimeout(applyMute,0),true));
function markCodeClean(){const ch=state.checks['code-exception'];if(ch&&ch.runtimeStatus==='monitoring'){ch.runtimeStatus='clean';ch.runtimeDetail='No exceptions detected';addLog(eventLog,'code clean','pass',ch.runtimeDetail);render();}}
function done(){const othersDone=CHECKS.every(c=>c.id==='code-exception'||!c.runtimeEvent||state.checks[c.id].runtimeStatus!=='waiting');if(othersDone)markCodeClean();if(CHECKS.every(c=>!c.runtimeEvent||!['waiting','monitoring'].includes(state.checks[c.id].runtimeStatus))){clearInterval(state.timer);document.getElementById('timer').textContent='Runtime complete';}}
state.timer=setInterval(()=>{state.seconds++;document.getElementById('timer').textContent=Math.max(0,state.timeout-state.seconds)+'s';if(state.seconds===2&&!state.connected){document.getElementById('bridge').textContent='Monitor not connected';addLog(eventLog,'Monitor not connected','fail','Check browser DevTools console for blocked inline script/CSP errors.');}if(state.seconds>=state.codeCleanAfter)markCodeClean();done();if(state.seconds>=state.timeout){clearInterval(state.timer);CHECKS.forEach(c=>{const ch=state.checks[c.id];if(ch.runtimeStatus==='waiting')ch.runtimeStatus='timeout';if(ch.runtimeStatus==='monitoring'){ch.runtimeStatus='clean';ch.runtimeDetail='No exceptions detected';}});render();document.getElementById('timer').textContent='Runtime complete';}},1000);
function startRuntimeTimer(){return setInterval(()=>{state.seconds++;document.getElementById('timer').textContent=Math.max(0,state.timeout-state.seconds)+'s';if(state.seconds===2&&!state.connected){document.getElementById('bridge').textContent='Monitor not connected';addLog(eventLog,'Monitor not connected','fail','Check browser DevTools console for blocked inline script/CSP errors.');}if(state.seconds>=state.codeCleanAfter)markCodeClean();done();if(state.seconds>=state.timeout){clearInterval(state.timer);CHECKS.forEach(c=>{const ch=state.checks[c.id];if(ch.runtimeStatus==='waiting')ch.runtimeStatus='timeout';if(ch.runtimeStatus==='monitoring'){ch.runtimeStatus='clean';ch.runtimeDetail='No exceptions detected';}});render();document.getElementById('timer').textContent='Runtime complete';}},1000);}
state.timer=startRuntimeTimer();
renderMuteButton();
render();
</script>
</body>
@@ -907,6 +921,22 @@ ${getToolWebviewStyles()}
display: block;
}
.embedded-runtime {
display: none;
margin-top: var(--tool-gap-md);
height: calc(100vh - 170px);
min-height: 360px;
}
.embedded-runtime iframe {
width: 100%;
height: calc(100% - 39px);
min-height: 0;
border: 0;
background: #171717;
display: block;
}
.runtime-legend {
font-size: 10px;
color: var(--vscode-descriptionForeground);
@@ -982,9 +1012,7 @@ ${getToolWebviewStyles()}
flex-wrap: wrap;
}
.summary-count {
display: flex;
align-items: center;
gap: 4px;
display: inline;
font-size: 12px;
}
</style>
@@ -1005,18 +1033,26 @@ ${getToolWebviewStyles()}
<h3 class="panel-title">Input</h3>
</div>
<div class="panel-body">
<div class="control-label">Playable ZIP</div>
<div class="field-row">
<button id="pickBtn">Select ZIP</button>
<span id="fileName" class="file-name">(no file selected)</span>
<button id="pickBtn" class="secondary">Select File</button>
<button id="reloadBtn" class="secondary" style="display:none">Reload</button>
</div>
<div class="file-name" id="fileName" style="margin-top:8px;">(no file selected)</div>
<div id="statusBar" class="status-panel"></div>
</div>
</section>
<div id="outputPanel" class="is-hidden">
<div class="summary-bar" id="summaryBar" style="margin-bottom:12px;"></div>
<div class="checker-layout">
<div class="summary-bar" id="summaryBar" style="display:none"></div>
<section class="tool-panel embedded-runtime" id="embeddedRuntimePanel">
<div class="panel-header">
<h3 class="panel-title">Runtime Checker</h3>
</div>
<iframe id="runtimeFrame"></iframe>
</section>
<div class="checker-layout" id="legacyRuntimePanel">
<!-- Checklist -->
<section class="tool-panel" style="margin-bottom:0">
@@ -1063,13 +1099,6 @@ ${getToolWebviewStyles()}
</ul>
</div>
<div class="event-log" id="consoleLogWrap" style="display:none">
<div class="event-log-title">Playable Console</div>
<ul class="event-log-list console-log-list" id="consoleLogList">
<li class="event-log-empty">No console output yet</li>
</ul>
</div>
</div>
</section>
@@ -1151,12 +1180,13 @@ const statusBarEl = document.getElementById('statusBar');
const outputPanel = document.getElementById('outputPanel');
const checklistEl = document.getElementById('checklist');
const iframe = document.getElementById('sim-iframe');
const runtimeFrame = document.getElementById('runtimeFrame');
const runtimePanel = document.getElementById('embeddedRuntimePanel');
const legacyPanel = document.getElementById('legacyRuntimePanel');
const screenHolder = document.getElementById('screenPlaceholder');
const summaryBar = document.getElementById('summaryBar');
const eventLogWrap = document.getElementById('eventLogWrap');
const eventLogList = document.getElementById('eventLogList');
const consoleLogWrap = document.getElementById('consoleLogWrap');
const consoleLogList = document.getElementById('consoleLogList');
const timerLabel = document.getElementById('runtimeTimerLabel');
// ─── Rendering helpers ──────────────────────────────────────────────────────
@@ -1230,26 +1260,11 @@ function renderChecklist() {
}
function renderSummary() {
const counts = { pass: 0, fail: 0, warn: 0, waiting: 0 };
CHECKS.forEach(c => {
const ch = state.checks[c.id];
if (ch.status === 'pending') return;
const effective = (ch.runtimeStatus === 'waiting' || ch.runtimeStatus === 'monitoring')
? 'waiting'
: ch.runtimeStatus === 'pass' ? 'pass' // SDK confirmed — always counts as pass
: ch.runtimeStatus === 'fail' ? 'fail'
: ch.runtimeStatus === 'timeout' && ch.status === 'pass' ? 'warn'
: ch.runtimeStatus === 'clean' ? ch.status
: ch.status;
if (counts[effective] !== undefined) counts[effective]++;
});
const parts = [];
if (counts.pass > 0) parts.push('<span class="summary-count ok">' + counts.pass + ' Passed</span>');
if (counts.fail > 0) parts.push('<span class="summary-count err">' + counts.fail + ' Failed</span>');
if (counts.warn > 0) parts.push('<span class="summary-count warn">' + counts.warn + ' Warning(s)</span>');
if (counts.waiting>0) parts.push('<span class="summary-count" style="color:var(--vscode-descriptionForeground)">' + counts.waiting + ' Waiting</span>');
summaryBar.innerHTML = parts.join('<span style="color:var(--tool-border)">|</span>');
summaryBar.textContent = '';
if (!statusBarEl.classList.contains('is-busy') && !statusBarEl.textContent.startsWith('Error:')) {
statusBarEl.textContent = '';
statusBarEl.className = 'status-panel';
}
}
function logEvent(name, cls, extra) {
@@ -1267,21 +1282,6 @@ function logEvent(name, cls, extra) {
eventLogList.scrollTop = eventLogList.scrollHeight;
}
function logConsole(level, text) {
const d = new Date();
const time = String(d.getMinutes()).padStart(2,'0') + ':' + String(d.getSeconds()).padStart(2,'0');
const empty = consoleLogList.querySelector('.event-log-empty');
if (empty) empty.remove();
const li = document.createElement('li');
li.innerHTML =
'<span class="event-time">' + esc(time) + '</span>' +
'<span class="console-level ' + esc(level) + '">' + esc(level) + '</span>' +
'<span class="console-line">' + esc(text || '') + '</span>';
consoleLogList.appendChild(li);
consoleLogList.scrollTop = consoleLogList.scrollHeight;
}
function inferLifecycleFromConsole(text) {
const s = String(text || '');
if (/(^|[^a-z])gameReady([^a-z]|$)|previewer:gameReady|game_ready/i.test(s)) return 'gameReady';
@@ -1382,12 +1382,15 @@ function resetState() {
state.monitorConnected = false;
iframe.style.display = 'none';
iframe.src = 'about:blank';
runtimeFrame.src = 'about:blank';
runtimePanel.style.display = 'none';
legacyPanel.style.display = '';
screenHolder.style.display = 'flex';
eventLogList.innerHTML = '<li class="event-log-empty">No events yet</li>';
consoleLogList.innerHTML = '<li class="event-log-empty">No console output yet</li>';
eventLogWrap.style.display = 'none';
consoleLogWrap.style.display = 'none';
summaryBar.innerHTML = '';
statusBarEl.textContent = '';
statusBarEl.className = 'status-panel';
timerLabel.textContent = '';
muteBtn.style.display = 'none';
state.muted = false;
@@ -1473,18 +1476,23 @@ function onLoaded(msg) {
fileNameEl.textContent = msg.fileName + ' (' + (msg.zipSize / 1024).toFixed(0) + ' KB)';
reloadBtn.style.display = '';
if (msg.html) { muteBtn.style.display = ''; muteBtn.textContent = 'Mute'; state.muted = false; }
statusBarEl.textContent = msg.externalUrl
? 'Runtime checker opened in browser: ' + msg.externalUrl
: '';
statusBarEl.textContent = '';
statusBarEl.className = 'status-panel';
outputPanel.classList.remove('is-hidden');
eventLogWrap.style.display = '';
consoleLogWrap.style.display = '';
applyStaticResults(msg.checks || []);
renderChecklist();
renderSummary();
if (msg.runtimeUrl) {
legacyPanel.style.display = 'none';
runtimePanel.style.display = 'block';
runtimeFrame.src = msg.runtimeUrl;
stopTimer();
return;
}
if (msg.html) {
screenHolder.style.display = 'none';
iframe.style.display = 'block';
@@ -1619,7 +1627,6 @@ function handleRuntimeEvent(msg) {
if (name === 'console') {
const text = String(extra.text || '');
logConsole(extra.level || 'log', text);
const inferred = inferLifecycleFromConsole(text);
if (inferred) {
handleRuntimeEvent({ type: 'lifecycle', name: inferred, extra: { source: 'console' } });