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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,19 +0,0 @@
# Unified Layout
[Select Folder] [Select Files] [Clear]
(PLEC UPLOAD)
| FILENAME |ITERATION NAME| |
|-----------------|--------------|---|
|pato-to-file.html| 01_full | X |
|pato-to-file.html| 01_full | X |
|pato-to-file.html| 01_full | X |
(AppLovin Playable Preview)(Base64 Scanner)(Send To Mobile)(MRAID Checker)
| FILENAME | |
|-----------------|-------|
|pato-to-file.html| X |
|pato-to-file.html| X |
|pato-to-file.html| X |
[Scan]/[Upload]/[Start]

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 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>
</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>
</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 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' } });