FAQ and troubleshooting

The failures people hit most, by the rule id the report shows, with what it means and how to fix it. For what a rule is, see Platform rules. A good way to work is to run pnpm check, take the first fail, look it up here, fix it, run validate (seconds) until the static rules are clean, and run check again.

Starting up and the handshake #

The game works on its own but stays blank in the portal #

init never arrived. In order of likelihood:

  • The bridge was never started: bridge.start() must run when the page loads, with the handlers registered before it.

  • The page is not served through games-host, so the portal-origins meta tag is missing. The bridge logs no portal origins configured. The dev console and games-host add the tag; a plain static server does not. In your own tests pass portalOrigins.

  • The game failed to load inside the frame. Open the frame's own console (browser developer tools, pick the iframe) and look for errors: a root-absolute path (R-BLD-05), a blocked script (R-SEC-03), a request to another host.

  • The game is in an iframe but the portal's origin is not in its list. The portal shows its retry screen after 10 seconds.

R-SDK-02: hello, init, ready did not complete in 3 seconds #

hello is sent on start(); ready is sent when you call bridge.ready(). Call ready() as soon as the first frame is drawn and input works: not after every level has loaded and not after the intro animation. Load later content lazily after ready (R-BLD-02). Check the timeline in the dev console; the status line shows how long each step took.

R-SDK-06: the game switched to standalone mode inside the iframe #

Something other than the bridge decides the mode (a timeout of your own, a check of window.parent, a fallback to localStorage when init is late). Remove it. Inside an iframe the game waits for init however long it takes; the portal reloads the frame after 10 seconds without ready. Only outside an iframe is there a 2 second rule, and the bridge applies it.

R-SDK-03: postMessage with target "*" #

Many ready-made games post a score or a state to the parent window with "*". Remove those calls: scores and state go through bridge.score() and bridge.storage. Never write your own protocol code when the bridge exists (R-SDK-01). The repository lints against "*" as well.

R-SDK-04 or R-SDK-05: the game listens to window messages after init, or accepts init from anywhere #

Use the bridge: it accepts init only from window.parent at a portal origin from the meta tag, and stops listening to window messages once the port exists. A hand-written window.addEventListener("message", ...) that stays active is what fails the check.

R-SDK-10: rate_limited answers in a normal session #

The game sends too often: more than one score per second, more than five events per second, or saves more often than every 30 seconds. Space them in your game: send a score at the end of a round, not every frame; coalesce events. The starter's src/pacing.ts and createThrottle / createSpacer in game-kit are ready-made. Saves are paced by bridge.storage; call storage.save() as often as you like and the bridge sends the newest state at the allowed rate.

R-SDK-13: session is active while the player is idle #

Send session(true) when a round starts and session(false) when it ends, the game is paused or the player leaves. The portal sends the heartbeats; session only says whether the player is really playing. Reporting active while a title screen waits inflates XP.

Saves #

R-STO-01: the game wrote to browser storage in portal mode #

Progress must go only through bridge.storage. Find every localStorage, sessionStorage, IndexedDB and Cache API write and either route it through storage.save() or limit it to standalone mode (bridge.mode === "standalone"). Two copies of a save is how progress gets lost.

R-STO-03: storage keys without the game id prefix #

All games share one origin, so every key starts with <gameId>: (my-game:save, my-game:lang). The bridge's default key already does. Legacy keys from an older release of the game need the reviewer's note.

R-STO-04: the save is too large or not plain JSON #

The limit is 256 KB of UTF-8 JSON, also after long play. Derive what you can instead of storing it (levels from a seed, caches, layouts), and store numbers, strings, booleans, arrays and plain objects only: no Map, Set, Date, class instances, undefined, BigInt, NaN or cycles. Measure with measureJson in a unit test (Testing).

R-STO-05 or R-STO-07: saveVersion #

Send the manifest's saveVersion with ready and every save (the bridge does when you pass it to createBridge). When the format changes: raise it in the manifest and in createBridge, add an upgrade step, and never lower it. If the server's save has a higher version than your build, send no save and ask the player to reload; the bridge's outdated status tells you.

R-STO-06: a broken or unexpected save stops the game #

Parse defensively: accept null, a string, an array and an object missing half its fields, repair field by field, and always reach ready. The starter's parseProfile is the pattern. The runner feeds the game broken and unexpected saves and expects a playable game and no uncaught error.

R-STO-08: a conflict is dropped silently #

On conflict either ask the player which copy to keep or merge both without loss. Calling resolveConflict("local") without looking at the server's copy fails the rule unless the local state already contains it. See Conflicts.

Network, code and security #

R-NET-02: a request to another host #

Fonts from a CDN, a library from a CDN, analytics, ad networks, a leaderboard of your own, WebSockets: none are allowed; the list of allowed hosts is empty. Bundle fonts and libraries into the build, remove analytics (the portal measures play), and use the portal's scores. A dead URL left in the code is reported for a human to look at.

R-NET-03: eval, new Function or remote code #

Replace eval and new Function with ordinary code (a lookup table, a parser), setTimeout("code") with a function, and remote <script src> or import("https://...") with a bundled copy. Some minified libraries contain dead eval branches; the static scan reports those for a human, and the runtime CSP report is the proof either way.

R-NET-01: a request outside the game's own folder #

Another game's folder, the games-host root, or the portal. The game may request files under its own /<id>/<version>/ only, and the portal's API is not for games.

R-SEC-01: the game reaches into window.parent or window.top #

Remove reads of parent.document, top.location, storage of the parent, window.open and top navigation. The only allowed use of the parent is the bridge's postMessage.

R-SEC-02: alert, confirm, prompt, popups, forms, downloads #

The iframe sandbox has no modals, popups, forms or downloads, so these fail silently (confirm always returns false, and a "start over" button stops working). Build the confirmation into the game's own screens. No camera, microphone, geolocation or clipboard either.

R-SEC-03: Content-Security-Policy violations #

The runner reports every violation of the target policy. The usual causes are an inline <script> (move it into a file), an onclick="..." attribute (use addEventListener), an image or font from another host, and eval. Inline <style> and style="..." are allowed.

R-SEC-04: a service worker #

Remove the registration. PWA plugins for Vite and others add one by default: switch it off. Offline play is the portal's job, and a service worker on the shared games origin can keep an old version alive after a rollback.

R-SEC-05: a key, token or .env in the build #

Anything that looks like a secret in the output is a blocker. Remove it from the build and rotate the key: the build is public.

Build and files #

R-BLD-05: root-absolute paths #

/assets/x.js asks the games host for a file outside your folder. Use ./assets/x.js. In Vite set base: "./"; check CSS url(/...) and any hard-coded host names too.

R-BLD-02 and R-BLD-03: first load too large #

Everything loaded until ready is at most 1.5 MB gzipped; a quick game should stay under 300 KB. Look at the biggest files in the report's findings. Typical savings: lazy-load later levels, music and large art after ready; compress images (WebP, AVIF) and audio (OGG, M4A); drop an unused engine module; do not bundle a font family when two weights do.

R-BLD-06: versions disagree #

The version of manifest.json, the one in hello.gameVersion (createBridge) and the release folder must be identical, and a released folder must not change. Bump the version everywhere for every change that ships. The starter keeps it in GAME_VERSION (constants), manifest.json and package.json, and a unit test fails when the constants and the manifest differ.

R-BLD-07: a file type games-host does not know #

Files with an unknown extension are served as application/octet-stream, and nosniff stops the browser from running a script or style served like that. Use the extensions listed in Packaging and submission (rename .bin blobs that the engine loads by URL only if the engine allows it).

R-BLD-08: threads and SharedArrayBuffer #

Cross-origin isolation (COOP/COEP) is not available, so threaded engine builds (some Unity and Emscripten settings) cannot run. Use the engine's single-threaded web build.

R-MAN-01: the manifest does not parse or does not match the build #

Run the manifest through gameManifestSchema in a unit test; the error names the field. Check that id equals the folder and entry exists in the build.

Timing, manifest and player experience #

R-TIM-01: device time used in portal mode #

Anything that uses Date.now() or new Date() for earnings, cooldowns, buffs or daily counters fails when the runner moves the device clock a day ahead. In portal mode compute from init.time.serverTime and resume.serverTime plus a monotonic clock (performance.now()); offline earnings are the time between init.save.savedAt and init.time.serverTime. Standalone mode may use the device clock because it has no portal rewards.

R-TIM-02 and R-TIM-03: the day #

Take "today" from init.time.day and reset daily counters on the day message, never at local midnight. Seed daily content with dailySeed(gameId, init.time.day) so every player gets the same board.

R-MAN-03: an invalid answer in the session #

The game sent an event or property the manifest does not declare (or with the wrong type). Declare every event and property with its type, or stop sending it. The dev console log shows exactly which message got the invalid.

R-MAN-04: a score mode without maxPerSecond #

Every score mode needs a maxPerSecond derived from the game's fastest possible scoring rate, plus integer and max where they hold, and monotonic only for ever-growing totals. Prove the number with a bot (Manifest reference).

R-I18N-01: strings in only one language, or a reload on language change #

Provide every player-visible string in Turkish (default) and English, list both in languages, and switch on init.lang and on lang messages without reloading. Hide your own language button in portal mode.

R-UX-01: something moves while paused #

A game loop, a CSS animation, a setInterval or a sound kept running after pause. Stop all of it, say session(false), and on resume wait for a tap when a round is in progress. The runner compares two screenshots two seconds apart and reads the audio context state.

R-UX-04: not playable on a 360x640 phone with touch #

No hover-only controls, no page scroll or pinch zoom taking the input, buttons large enough (44 px) and everything inside the frame. Add touch-action: none on the playfield and viewport-fit=cover with env(safe-area-inset-*) padding.

R-QLT-01: an error or a failed request in the first 60 seconds #

Uncaught errors, unhandled promise rejections and failed requests all count: a missing favicon, a font file path that is wrong in production, a source map reference. Fix the path, or remove the reference.

Tools #

pnpm check exits with 2 and says there is no build #

Build first (pnpm build). validate and check of a game run vite build for you; the standalone pnpm --filter @gameportal/rules ... commands need a built folder with index.html and manifest.json.

check fails with a missing browser or a busy port #

Install the browser once with pnpm exec playwright install chromium. The runner uses two local ports (3750 and 4176 by default, 3760 and 4160 for the dev console); free them or pass --portal-port and --games-port.

A warning I disagree with #

Warnings are reported with their evidence and a reviewer may accept one for a game version, with a reason. Blockers cannot be waived for a single game. If a blocker looks wrong in general, say so: rule changes are made deliberately and versioned (PLATFORM_RULES_VERSION).

My game is an engine build (Unity WebGL, Godot, Construct) #

Start at Level 0: it needs no code change and the rules for security, network and build still apply. The bridge needs a plugin on the engine side to reach Level 1; the protocol in the SDK reference is what such a plugin implements.

İlerlemeni kaydet

E-postana bir bağlantı gönderelim ya da Google ile devam et. Misafir olarak kazandıkların hesabına taşınır.