# Player sound and touch controls The player toolbar offers **Mute / Unmute** and **Controls** in the normal player, creator preview and fullscreen. The platform overlay leaves authored game buttons and touch handlers available. Controls start hidden unless the creator chooses to open them on touch devices. **Controls** opens a menu with independent **Game**, **Keyboard** and **Customize** toggles; Keyboard is available when enabled by the creator. **Hide** closes that menu while active game buttons and the keyboard remain available; switch **Game** or **Keyboard** off to hide that panel. Sound interception is installed before uploaded scripts. It applies a separate mute gate to `HTMLMediaElement` and routes WebAudio connections to an `AudioContext`'s speaker destination through a platform gain node. An app changing its own `muted` property or creating a new audio context while platform mute is on stays silent. Unmute restores the app's requested mute and upstream volume/gain; it does not force an intentionally silent app to play. The platform never calls `play()` or `AudioContext.resume()` on behalf of an app, and browser autoplay still requires the app's appropriate gesture. Offline audio rendering retains its original signal. The override covers supported audio APIs in the injected runtime document. Cross-origin embedded players, other documents, speech synthesis and apps deliberately replacing intercepted browser APIs may bypass it. It is a player convenience, not a browser-level audio security boundary. The toolbar reports audio control unavailable if the browser exposes neither supported API. The media `muted` getter reports effective platform mute, as the browser does. A creator using `audio.muted = !audio.muted` while platform mute is active reads that effective state, so authors with a separate mute-toggle UI should keep their own desired state in a variable and assign it explicitly. `hackemist:audiochange` can reflect the platform state separately. The platform preserves explicit assignments and muted-attribute changes; it cannot infer a creator's intent from an ambiguous read/modify/write toggle. ## Touch input Native app taps, swipes, dragging, scrolling and authored multi-finger gestures remain available. The platform's full-size overlay and control stage use pointer-events: none, and the runtime's native pointer observer does not cancel events or alter the app's touch-action policy. Only visible platform widgets accept input over their own areas. Move, disable or hide a widget if it covers an authored control. Disabling Game or Keyboard restores those covered areas; Hide closes only the selector menu. Browsers can restrict simultaneous touches that span the control document and its embedded app. Chromium keeps a touch sequence in its starting document; the regression check reproduces the same behavior using a plain parent-page button with no platform handlers. This does not disable an app's own multitouch within its document. For combined gestures, use the app's own controls together, or map the necessary actions onto the platform pad together. See the [W3C Touch Events implementer note](https://www.w3.org/TR/touch-events/). **Game** offers held, repeating keys and simultaneous button presses. It dispatches untrusted `keydown` / `keyup` events with `key`, `code`, legacy `keyCode` / `which`, repeat and modifier state. Cancelled pointers, lost capture, switching game controls off, editing controls, opening account Saves, resizing, blur, visibility loss and closing the player release held keys. Keys are a fixed allowlist of letters, digits, arrows, Space, Enter, Backspace, Shift and Control. Escape remains the platform's existing close shortcut. Players can add an eight-direction joystick in Customize and map its Up, Down, Left and Right directions to four allowed keys. A diagonal holds both relevant directions. This works with existing directional keyboard handlers. The joystick also emits a typed `hackemist:input` event with `{type: 'joystick', controlId, axes: {x, y}, source: 'touch'}` for creators who integrate analog movement. Axes range from -1 to 1: negative x is left, positive x is right, negative y is up and positive y is down. Releasing the stick or releasing all input emits neutral `{x: 0, y: 0}`. These events do not create a hardware gamepad or bypass an engine's device checks. On a phone, touching an editable app field can focus it and open the device's normal keyboard under the browser's usual rules. The platform does not cancel those app touches or suppress the native keyboard. **Keyboard** is an optional fallback: tap the app's text field or game surface first, then switch Keyboard on. For supported editable fields, it inserts text into the last focused field, emits cancellable `beforeinput` followed by `input`, and respects disabled/read-only fields and text-length limits. When no editable field is selected, it sends untrusted keyboard events to the focused game surface so ordinary canvas/game key handlers can receive letters, digits, Space, Shift capitalization, Backspace and Enter. The virtual keyboard can be moved and resized in Customize and can stay visible alongside Game buttons. It does not expose system keyboard shortcuts or browser navigation. Mapping buttons to keys already handled by the app often works without an adapter. Standard `keydown` / `keyup` listeners receive the events, and editable fields receive supported text input. Some engines reject synthetic keyboard events (`isTrusted` remains false), poll physical device state, require native gamepads/pointer lock or need a real gesture to unlock sound. A key mapping cannot bypass those requirements. Creators can keep their own native controls and, when needed, connect the typed custom event emitted alongside platform input to their engine's existing actions: ```js window.addEventListener('hackemist:input', event => { const {key, code, pressed, repeat, source, text, inputType} = event.detail; // Map known game actions in your own engine; do not evaluate input as code. if (key === 'ArrowLeft') game.setLeftPressed(pressed); }); window.addEventListener('hackemist:audiochange', event => { // Optional: reflect the platform mute state in authored UI. game.platformMuted = event.detail.muted; }); ``` Key events carry `{key, code, pressed, repeat, source: 'touch'}`. Text notifications carry `{text, inputType, source: 'touch'}` after a supported field was edited or virtual keyboard text was sent to game handlers. Joystick events carry bounded axes for a named control; the bridge permits at most 16 active joystick identifiers and rejects nonnumeric, nonfinite or out-of-range axes. These are local integration events, not authentication signals. The runtime bridge checks the parent window, exact parent origin and launch nonce before applying platform messages. No privileged input, arbitrary selectors, uploaded JavaScript, network origins or extra runtime permissions are accepted by the platform control configuration. ## Creator and MCP configuration Studio's **Player controls** panel provides arrows, WASD, platformer, racing, fighting and custom presets. Creators can edit labels and keys, add/remove up to 16 buttons, drag the preview positions, set button size (40–96 pixels), opacity (0.25–1), virtual keyboard availability and automatic touch-device opening. MCP exposes `player_controls_get` with `projects:read` and `player_controls_update` with `projects:write`. Both require a currently authorized owner. Read first and send the current integer `base_revision` plus the complete typed `config`. Updates refresh credential scope/ownership within the mutation transaction and reject stale revisions with 409. Cookie writes also refresh the session and enforce CSRF. A successful update applies when the app is reopened; it does not change a running launch, save data, app files or runtime permission policy. ```json { "project_id": "YOUR_PROJECT_ID", "base_revision": 0, "config": { "enabled": true, "preset": "custom", "show_on_touch": false, "keyboard": true, "size": 56, "opacity": 0.75, "buttons": [ {"id":"jump","label":"Jump","key":" ","code":"Space","x":0.9,"y":0.8} ] } } ``` Coordinates are finite fractions from 0 to 1 within the available game control stage; resize/fullscreen clamps each button to remain visible. Unknown fields, script/selector fields, unsupported or mismatched key codes, duplicate identifiers, invalid Unicode, nonfinite numbers and oversized lists are rejected. Player-added buttons, joysticks, individual sizes and keyboard layouts are device preferences. They do not extend this creator/server configuration schema and are not accepted as extra fields by `player_controls_update`. ## Device preferences In Customize, use **Arrange controls** to close the mapping panel while keeping movement and resize handles active. Hide the selection menu to expose the whole play area. Reopen Controls and choose the small **Edit mappings** icon to return to labels, key mappings and sizes. Customize still toggles off on a second click. Arranging a control changes only this account's device layout and does not send gameplay input. Players can switch **Customize** on to add buttons or joysticks, remove controls from their local layout, edit each control's action label and keyboard mappings, and drag or resize each control. Buttons can be 40–120 pixels and joysticks 64–200 pixels. Labels describe the action for the player; the selected key is what the app receives, so choose keys the game already handles. Up to 16 extra controls share one limit, in addition to the creator's maximum of 16 buttons. Players can also adjust opacity, move and resize the virtual keyboard, or use **Reset to creator layout**. Game, Keyboard and Customize are independent: closing Customize or using Hide leaves enabled input panels visible. Editing or removing a creator button here changes only this player's local layout. A bounded localStorage map stores up to 20 project layouts per signed-in account (separate guest map, at most 64 KiB), scoped to the creator configuration revision. A new creator revision uses the new layout. Platform sound preference is also local to that account on the device. These preferences are separate from account/cloud game saves; unavailable localStorage leaves controls usable without persistence. Control customization does not capture game progress. JavaScript game state still needs capture and restore callbacks, or another supported save integration. ## Verification `tests/test_player_controls.py` uses a temporary SQLite database for typed validation, owner-only/private access, CSRF, stale revision, ownership changes and revoked sessions. `tests/player_controls_browser.spec.mjs` uses real browser Media/WebAudio and Pointer/Input APIs with every network request intercepted to local source fixtures. It exercises autoplay preservation, nonce/source rejection, held multi-button and joystick release, custom integration events, virtual field editing and canvas key fallback, dragged layout persistence and responsive/dark/native-control behavior without mutating a live deployment.