-
Notifications
You must be signed in to change notification settings - Fork 41
example register game
The Games window on the wallpaper (gamepad icon) is registry-driven: every registered game becomes a launcher tile, gets a tab in the unified scoreboard, and can be played in score-to-beat challenges between users. Registration is two-sided, like wallpapers and widgets: PHP declares the metadata (so the launcher and scoreboard paint at boot without downloading game code) and JS supplies the render callback (loaded lazily on first launch).
Only server-registered games can persist scores and challenges — the REST routes 404 unknown game ids.
The games framework is opt-in and off by default — an admin enables it site-wide in OpenStation Preferences → Features → Extended options (or via the openstation_games_enabled filter). While it's off, none of the games module loads — openstation_register_game() is undefined, exactly as if OpenStation weren't active. The function_exists() guard in the recipe below covers both cases; saved scores and play time survive a disable/re-enable round trip untouched.
my-plugin.php
<?php
/** Plugin Name: My Game */
defined( 'ABSPATH' ) || exit;
add_action( 'init', function () {
if ( ! function_exists( 'openstation_register_game' ) ) {
return; // OpenStation not active.
}
// The handle is NOT enqueued — the shell fetches it lazily the
// first time someone presses Play.
wp_register_script(
'my-plugin-tap-game',
plugins_url( 'tap-game.js', __FILE__ ),
array( 'openstation' ),
'1.0.0',
true
);
openstation_register_game( 'my-plugin-tap', array(
'title' => __( 'Tap', 'my-plugin' ),
'description' => __( 'Tap the dot before it fades.', 'my-plugin' ),
'icon' => 'dashicons-marker',
'script' => 'my-plugin-tap-game',
'score_columns' => array(
array( 'key' => 'score', 'label' => __( 'Score', 'my-plugin' ), 'type' => 'number' ),
array( 'key' => 'taps', 'label' => __( 'Taps', 'my-plugin' ), 'type' => 'number' ),
array( 'key' => 'time', 'label' => __( 'Time', 'my-plugin' ), 'type' => 'time' ),
),
// Arbitrary blob handed to the game's launch context.
'config' => array(
'roundsUrl' => plugins_url( 'rounds.json', __FILE__ ),
),
) );
}, 20 );tap-game.js
// Publish the full def on the games global — the analogue of
// window.openStationWallpapers for wallpapers. The framework merges
// this with the server metadata (server wins for title/icon/etc.).
window.openStationGames = window.openStationGames || {};
window.openStationGames[ 'my-plugin-tap' ] = {
id: 'my-plugin-tap',
title: 'Tap',
icon: 'dashicons-marker',
scoreColumns: [ { key: 'score', label: 'Score', type: 'number' } ],
window: { width: 640, height: 480, minWidth: 400, minHeight: 320 },
render( ctx ) {
// ctx.container — the native window body (yours until teardown)
// ctx.config — the PHP-registered blob ({ roundsUrl })
// ctx.challenge — set when this run is an accepted challenge:
// { id, scoreToBeat, scoreMeta, challengerName }
const button = document.createElement( 'button' );
button.type = 'button';
button.textContent = 'Tap!';
let score = 0;
button.addEventListener( 'click', () => {
score += 10;
if ( score >= 100 ) {
// Persist the run: leaderboard in free play, the
// challenge-completion endpoint in challenge mode.
// Keys in `meta` line up with `score_columns`.
void ctx.submitScore( {
score,
meta: { taps: score / 10, time: 12 },
} );
ctx.close();
}
} );
ctx.container.appendChild( button );
// Teardown runs on EVERY close path — stop loops here.
return () => button.remove();
},
};That's the whole integration. For free you get:
- a launcher tile in the Games window (live — activating your plugin mid-session adds the tile without a reload; deactivating removes it);
- a detail panel when your game is selected — description, Play, and Challenge (the latter throws down the player's best score), plus a scoreboard with your declared columns, a Player column (name + avatar + presence dot), and a Date column;
- score persistence (
POST /desktop-mode/v1/games/{game}/scores, always credited to the submitting session); - challenges: any player can pick one of their scores and challenge another user; recipients get a notification + Accept & Play toast, and your game sees
ctx.challengeduring the run; - wallpaper suspension while your game's window is open (the framework holds
wp.os.wallpaper.suspend( 'game:<windowId>' )and releases it on every close path); - play-time tracking: the framework measures how long each player keeps your game's window open (the clock pauses while it's minimized) and accumulates per-user lifetime totals plus daily buckets — shown Steam-style on your game's detail panel ("Play time (last two weeks)" / "Play time (total)"), readable via
wp.os.games.getPlaytime()/openstation_games_get_playtime( $user_id, $game )/openstation_games_get_playtime_daily( $user_id, $game ).
// Anti-cheat / plausibility gate — return a WP_Error to reject a save.
add_filter( 'openstation_game_score_pre_save', function ( $pre, $game, $user_id, $score ) {
if ( 'my-plugin-tap' === $game && $score > 100000 ) {
return new WP_Error( 'implausible', 'No.' );
}
return $pre;
}, 10, 4 );
// Block challenges (do-not-disturb, roles…).
add_filter( 'openstation_games_can_challenge', function ( $allowed, $challenger_id, $recipient_id ) {
return get_user_meta( $recipient_id, 'dnd', true ) ? false : $allowed;
}, 10, 3 );
// React to finished runs.
add_action( 'openstation_game_score_saved', function ( $id, $game, $user_id, $score ) {
// e.g. award a badge at 10k.
}, 10, 4 );
// React to accumulated play time — e.g. a dedication badge at 10 hours.
add_action( 'openstation_game_playtime_recorded', function ( $game, $user_id, $seconds, $total ) {
if ( 'my-plugin-tap' === $game && $total >= 10 * HOUR_IN_SECONDS ) {
// …
}
}, 10, 4 );The built-in Inkfall typing game (src/games/inkfall/, registered in includes/games/inkfall.php) is the full-fat reference: PixiJS rendering, the framework dictionary via the injected config.wordsUrl (every server-registered game receives it — see openstation_games_words_url in the hooks reference), challenge-mode HUD, and pure, unit-tested gameplay modules. The second built-in, Alphabet Soup (src/games/alphabet-soup/), shows the seeded-daily-puzzle pattern (same grid worldwide from a dd-mm-yyyy date seed), a Time Attack countdown mode, and the game-over share-card image (src/games/share-card.ts).
This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config