Add Mintegral playable checker

This commit is contained in:
2026-06-01 21:59:52 +08:00
parent 1c445e2771
commit 9e919756fd
6 changed files with 1872 additions and 0 deletions

155
TODO.md Normal file
View File

@@ -0,0 +1,155 @@
# Mintegral Checker — TODO
## Task Overview
Build a local VS Code webview equivalent of the Mintegral PlayTurbo checker
(`https://www.playturbo.com/review`). Validates a Mintegral playable ad ZIP against
PlayTurbo requirements. Two check categories:
- **Static** — parse `index.html` + ZIP structure without running the ad.
- **Runtime** — load the ad in a sandboxed `srcdoc` iframe, inject a monitoring stub,
and detect whether lifecycle callbacks (`gameReady`, `gameEnd`, `gameStart`,
`gameClose`, `install`) actually fire.
---
## Finished
- [x] Pure-Node ZIP parser (EOCD → Central Directory → Local File Headers, zlib DEFLATE)
- [x] All 9 static checks:
- HTML Requirements (charset, viewport, single-file)
- CTA Method (`window.install` reference in scripts)
- Game End / Game Ready / Game Start / Game Close (regex on scripts)
- File Handling (no external URLs, base64 assets)
- File Spec (ZIP ≤ 5 MB, single `index.html`)
- Storage (no `localStorage.setItem` / `sessionStorage.setItem`)
- [x] Device-frame simulator (phone mockup around `srcdoc` iframe)
- [x] Event log panel showing runtime events as they fire
- [x] Runtime timer with 60s timeout and per-check waiting badges
- [x] `__ping__` handshake → "Simulator connected" in event log (confirms stub runs)
- [x] Dual notification channels: polling `__hplMtgEvents` + `parent.__hplMtgNotify`
- [x] `MW_INIT` interceptor (wraps `emitCheckData`, forces `_hasReview`/`_isPreview`)
- [x] `window.postMessage` override (catches `previewer:gameReady` self-posts)
- [x] `mkAccess``Object.defineProperty` accessor with undefined getter so AD's
`typeof window.gameReady === 'undefined'` guard passes, setter wraps assignment
- [x] Capture-phase `load` safety net — wraps any SDK function declarations that
overwrote accessors, fires before `body.onload`
- [x] Mute button UI (shows on ZIP load, hidden on reset, toggles Mute/Unmute label)
- [x] Diagnostic lifecycle logging (`post-parse`, `DOMContentLoaded`, `load`,
`load+500ms`, callback assignment/call traces)
- [x] AudioContext construction proxy for mute/unmute of closure-owned engine audio
---
## Bugs
### 1. Callbacks never fire (critical, diagnostics added)
**Symptom:** Event log shows "Simulator connected" (`__ping__` fires) but no lifecycle
events appear. The same ZIP passes all checks on the real PlayTurbo site.
**Root cause candidates (narrowed down but not confirmed):**
A. **`body.onload` vs capture `load` ordering on `window`** — When the event target is
`window` itself, capture and bubble listeners both run at the "at target" phase in
registration order. Our stub is in `<head>` and registers first, so it should fire
before the body's `onload`. But this assumes no edge-case behaviour in VS Code's
Chromium sandbox / srcdoc context.
B. **SDK function declaration closes over a local reference** — If the ad does:
```js
var gameReady = function() { ... };
window.gameReady = gameReady;
```
and `body.onload="gameReady()"` resolves to the local var (via closure), then
reassigning `window.gameReady` in our load-time wrapper has no effect.
C. **CSP in VS Code webview blocks stub execution silently** — The webview panel has
an implicit CSP. The stub is injected into `srcdoc`; it might run in a restricted
context that prevents property interception or `parent` access for later events (even
though `__ping__` works because it runs synchronously at parse time).
D. **Ad calls `gameReady()` before load** — If the ad boots via DOMContentLoaded and
calls `gameReady()` there (rather than body.onload), our load-event safety net fires
too late. The `mkAccess` accessor should still catch it, but only if the ad assigns
`window.gameReady` first.
**What's been added:**
- Injected diagnostic logging for lifecycle function `typeof` at post-parse,
DOMContentLoaded, load capture before/after wrapping, and 500 ms after load.
- Logs callback assignment/wrapping and wrapper call paths to the event log.
- Logs dynamic `document.createElement('script')` calls to help trace script injection.
**What's still needed if callbacks still do not fire:**
- Check if the ad bundles `preview-util.js` inside `index.html` or references it
externally (would fail file-handling check anyway).
- Compare the diagnostic timeline against the real PlayTurbo execution path.
---
### 2. Mute button does nothing (fixed in monitor stub)
**Symptom:** Clicking Mute/Unmute changes the label but has no audible effect.
**Root cause:** The current approach searches `Object.keys(cw)` for an `AudioContext`
instance. AudioContexts are almost always created in closures (inside Phaser or the ad
IIFE) and are never assigned as window properties — `Object.keys` will never find them.
`cw.Phaser.game.sound` also likely fails because Phaser stores the game instance
internally, not at `window.Phaser.game`.
**Fix applied:**
Intercepts `AudioContext`/`webkitAudioContext` construction in the monitoring stub,
stores captured contexts/gain nodes on `window.__hplAudioContexts` and
`window.__hplMasterGains`, and routes `ctx.destination` through a master gain when
possible. The mute button now sets captured master gain values first, then falls back
to suspending/resuming captured contexts and exposed Phaser/global game sound objects.
---
## Improvements / Nice-to-Have
- [ ] **Inject SDK alongside ad** — The real PlayTurbo checker loads `preview-util.js`
next to the ad. Bundling (or downloading once) the 265 KB SDK and injecting it into
the srcdoc would let `MW_INIT`/`emitCheckData` fire natively, which is the most
reliable callback path and matches what the real checker does.
- [ ] **Diagnostic mode** — A "Debug" toggle that logs `typeof` of each lifecycle
function at key moments (post-parse, DCL, load, 500ms after load) to the event log,
making root-cause analysis faster.
- [ ] **Replay button** — Reload only the iframe (not the full ZIP re-parse) to retest
without re-picking the file.
- [ ] **Portrait / landscape toggle** — Flip the device frame aspect ratio.
- [ ] **Multiple HTML entry points** — Some ZIPs have `playable.html` instead of
`index.html`; add a fallback search.
- [ ] **Orientation lock detection** — Check for `screen.orientation.lock()` calls
(Mintegral requires the ad to handle both orientations).
- [ ] **Version bump and changelog entry** — Once callbacks are working, bump to 0.1.8
and add a changelog entry for the Mintegral Checker tool.
---
## Known Issues / Constraints
- VS Code `WebviewPanel` runs in a sandboxed Chromium process. `srcdoc` iframes are
same-origin with the webview, so `parent` access works — but the webview's implicit
CSP may still block certain APIs inside nested iframes.
- The `Object.defineProperty` accessor approach (`mkAccess`) is the only way to let the
AD's `typeof window.X === 'undefined'` guard pass while still intercepting the
assignment. Any early concrete assignment (even `undefined`) breaks the guard in some
runtimes.
- SDK `function` declarations at brace-depth 0 are `[[DefineOwnProperty]]` calls at
parse time; they overwrite our configurable accessors unconditionally. The load-time
safety net is the only recovery path for that scenario.
- The `applyStaticResults` function sets `runtimeStatus = null` for lifecycle checks
that already failed statically. `handleRuntimeEvent` only transitions
`runtimeStatus === 'waiting'` → `'pass'`; it silently ignores `null`. This means a
static-fail check can't be runtime-rescued via the direct lifecycle path (though the
`handleSdkCheck` path does handle `null`). Intentional, but worth documenting.

File diff suppressed because one or more lines are too long

View File

@@ -43,6 +43,10 @@
{ {
"command": "hplToolbox.openPlayworksConverter", "command": "hplToolbox.openPlayworksConverter",
"title": "HPL Toolbox: Open Playworks Converter" "title": "HPL Toolbox: Open Playworks Converter"
},
{
"command": "hplToolbox.openMintegralChecker",
"title": "HPL Toolbox: Open Mintegral Checker"
} }
], ],
"viewsContainers": { "viewsContainers": {

View File

@@ -6,6 +6,7 @@ import { openBase64Scanner } from './tools/base64Scanner';
import { openMraidChecker } from './tools/mraidChecker'; import { openMraidChecker } from './tools/mraidChecker';
import { openSendToMobile } from './tools/sendToMobile'; import { openSendToMobile } from './tools/sendToMobile';
import { openPlayworksConverter } from './tools/playworksConverter'; import { openPlayworksConverter } from './tools/playworksConverter';
import { openMintegralChecker } from './tools/mintegralChecker';
import { openChangelog } from './changelogView'; import { openChangelog } from './changelogView';
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
@@ -16,6 +17,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('hplToolbox.openMraidChecker', () => openMraidChecker(context)), vscode.commands.registerCommand('hplToolbox.openMraidChecker', () => openMraidChecker(context)),
vscode.commands.registerCommand('hplToolbox.openSendToMobile', () => openSendToMobile(context)), vscode.commands.registerCommand('hplToolbox.openSendToMobile', () => openSendToMobile(context)),
vscode.commands.registerCommand('hplToolbox.openPlayworksConverter', () => openPlayworksConverter(context)), vscode.commands.registerCommand('hplToolbox.openPlayworksConverter', () => openPlayworksConverter(context)),
vscode.commands.registerCommand('hplToolbox.openMintegralChecker', () => openMintegralChecker(context)),
vscode.commands.registerCommand('hplToolbox.openChangelog', () => openChangelog(context)), vscode.commands.registerCommand('hplToolbox.openChangelog', () => openChangelog(context)),
vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider(context)) vscode.window.registerWebviewViewProvider('hplToolbox.launcher', new LauncherViewProvider(context))
); );

File diff suppressed because it is too large Load Diff

View File

@@ -46,5 +46,13 @@
"command": "hplToolbox.openPlayworksConverter", "command": "hplToolbox.openPlayworksConverter",
"path": "/playworks", "path": "/playworks",
"beta": true "beta": true
},
{
"id": "mintegral",
"title": "Mintegral Checker",
"description": "Validate a Mintegral playable ZIP against PlayTurbo requirements",
"command": "hplToolbox.openMintegralChecker",
"path": "/mintegral",
"beta": true
} }
] ]