PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/games/registry.php +124 -47 0.9.81.1.10 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — Games registry.
3 + * OpenStation — Games registry.
4 4 *
5 5 * Server-side registration API + payload builder for desktop games.
6 6 * A game's discovery metadata (title, icon, description, score
7 7 * columns) is declared here in PHP so the Games window and the
@@ -8,9 +8,9 @@
8 8 * scoreboard tabs paint at shell boot without downloading any game
9 9 * code; the game's JS bundle — declared via the `script` handle —
10 10 * is loaded lazily on first launch and publishes the full def
11 11 * (including the `render` callback) on
12 - * `window.desktopModeGames[ <id> ]`.
12 + * `window.openStationGames[ <id> ]`.
13 13 *
14 14 * This deliberate laziness is the one way the games registry differs
15 15 * from the wallpaper registry it is otherwise modeled on: wallpaper
16 16 * scripts are enqueued eagerly because the active wallpaper must
@@ -15,9 +15,9 @@
15 15 * from the wallpaper registry it is otherwise modeled on: wallpaper
16 16 * scripts are enqueued eagerly because the active wallpaper must
17 17 * paint at boot; game code is only needed when someone plays.
18 18 *
19 - * @package WPDesktopMode
19 + * @package OpenStation
20 20 */
21 21
22 22 defined( 'ABSPATH' ) || exit;
23 23
@@ -26,13 +26,13 @@
26 26 *
27 27 * Example:
28 28 *
29 29 * ```php
30 - * desktop_mode_register_game( 'inkfall', array(
30 + * openstation_register_game( 'inkfall', array(
31 31 * 'title' => __( 'Inkfall', 'desktop-mode' ),
32 32 * 'description' => __( 'Type the falling words.', 'desktop-mode' ),
33 33 * 'icon_svg' => '<svg …>…</svg>',
34 - * 'script' => 'desktop-mode-game-inkfall',
34 + * 'script' => 'os-game-inkfall',
35 35 * 'score_columns' => array(
36 36 * array( 'key' => 'score', 'label' => __( 'Score', 'desktop-mode' ), 'type' => 'number' ),
37 37 * array( 'key' => 'time', 'label' => __( 'Time', 'desktop-mode' ), 'type' => 'time' ),
38 38 * ),
@@ -40,11 +40,11 @@
40 40 * ) );
41 41 * ```
42 42 *
43 43 * ```js
44 - * // Inside desktop-mode-game-inkfall.js
45 - * window.desktopModeGames = window.desktopModeGames || {};
46 - * window.desktopModeGames.inkfall = {
44 + * // Inside os-game-inkfall.js
45 + * window.openStationGames = window.openStationGames || {};
46 + * window.openStationGames.inkfall = {
47 47 * id: 'inkfall',
48 48 * title: 'Inkfall',
49 49 * icon: 'data:image/svg+xml;base64,…',
50 50 * scoreColumns: [ … ],
@@ -52,9 +52,9 @@
52 52 * };
53 53 * ```
54 54 *
55 55 * @param string $id Game id (slug). Must match the
56 - * `window.desktopModeGames[<id>]` key the game's
56 + * `window.openStationGames[<id>]` key the game's
57 57 * JS publishes.
58 58 * @param array $args {
59 59 * @type string $title Launcher label. Required.
60 60 * @type string $description Plain-text description shown on the
@@ -75,16 +75,29 @@
75 75 * underneath (`wordsUrl` — see
76 76 * includes/games/config.php); the
77 77 * game's keys win on collision.
78 78 * @type string[] $capabilities Gate: ALL caps must match.
79 + * @type array $window The game window's size, as any
80 + * subset of `{ width, height,
81 + * minWidth, minHeight }` in pixels.
82 + * Declare it here as well as in the
83 + * JS def: a game's bundle is fetched
84 + * on first play, so the shell opens
85 + * the window — and paints its loading
86 + * spinner — before it has seen the
87 + * def. Without this the first window
88 + * of a session opens at the framework
89 + * default. The def still wins once it
90 + * arrives, so declaring only in JS
91 + * keeps working.
79 92 * }
80 93 * @return true|WP_Error `true` on success; `WP_Error` otherwise.
81 94 */
82 -function desktop_mode_register_game( $id, $args = array() ) {
95 +function openstation_register_game( $id, $args = array() ) {
83 96 $id = sanitize_key( (string) $id );
84 97 if ( '' === $id ) {
85 - return desktop_mode_registration_error(
86 - 'desktop_mode_missing_id',
98 + return openstation_registration_error(
99 + 'openstation_missing_id',
87 100 __( 'Game id is required and must be a valid slug.', 'desktop-mode' )
88 101 );
89 102 }
90 103
@@ -96,10 +109,11 @@
96 109 'script' => '',
97 110 'score_columns' => array(),
98 111 'config' => array(),
99 112 'capabilities' => array(),
113 + 'window' => array(),
100 114 );
101 - $args = wp_parse_args( $args, $defaults );
115 + $args = wp_parse_args( $args, $defaults );
102 116
103 117 $svg = trim( (string) $args['icon_svg'] );
104 118 if ( '' !== $svg ) {
105 119 // Same defence-in-depth as desktop icons: the data URI is
@@ -105,17 +119,17 @@
105 119 // Same defence-in-depth as desktop icons: the data URI is
106 120 // consumed via `<img src=…>` (which sandboxes SVG scripts),
107 121 // but reject script tags outright anyway.
108 122 if ( false !== stripos( $svg, '<script' ) ) {
109 - return desktop_mode_registration_error(
110 - 'desktop_mode_invalid_icon_svg',
123 + return openstation_registration_error(
124 + 'openstation_invalid_icon_svg',
111 125 __( 'Game `icon_svg` must not contain a <script> tag.', 'desktop-mode' ),
112 126 array( 'id' => $id )
113 127 );
114 128 }
115 129 if ( 0 !== stripos( ltrim( $svg ), '<svg' ) ) {
116 - return desktop_mode_registration_error(
117 - 'desktop_mode_invalid_icon_svg',
130 + return openstation_registration_error(
131 + 'openstation_invalid_icon_svg',
118 132 __( 'Game `icon_svg` must start with a <svg> root element.', 'desktop-mode' ),
119 133 array( 'id' => $id )
120 134 );
121 135 }
@@ -123,30 +137,33 @@
123 137 }
124 138
125 139 foreach ( (array) $args['capabilities'] as $cap ) {
126 140 if ( ! current_user_can( (string) $cap ) ) {
127 - return desktop_mode_registration_error(
128 - 'desktop_mode_capability_denied',
141 + return openstation_registration_error(
142 + 'openstation_capability_denied',
129 143 sprintf(
130 144 /* translators: %s: capability slug. */
131 145 __( 'Current user lacks the %s capability required to register this game.', 'desktop-mode' ),
132 146 (string) $cap
133 147 ),
134 - array( 'capability' => (string) $cap, 'id' => $id )
148 + array(
149 + 'capability' => (string) $cap,
150 + 'id' => $id,
151 + )
135 152 );
136 153 }
137 154 }
138 155
139 156 if ( '' === (string) $args['title'] ) {
140 - return desktop_mode_registration_error(
141 - 'desktop_mode_missing_title',
157 + return openstation_registration_error(
158 + 'openstation_missing_title',
142 159 __( 'Game registration requires a non-empty `title`.', 'desktop-mode' ),
143 160 array( 'id' => $id )
144 161 );
145 162 }
146 163 if ( '' === (string) $args['script'] ) {
147 - return desktop_mode_registration_error(
148 - 'desktop_mode_missing_script',
164 + return openstation_registration_error(
165 + 'openstation_missing_script',
149 166 __( 'Game registration requires a `script` handle that publishes the game def.', 'desktop-mode' ),
150 167 array( 'id' => $id )
151 168 );
152 169 }
@@ -154,30 +171,79 @@
154 171 $entry = array(
155 172 'id' => $id,
156 173 'title' => (string) $args['title'],
157 174 'description' => sanitize_textarea_field( (string) $args['description'] ),
158 - 'icon' => desktop_mode_sanitize_dock_icon( (string) $args['icon'] ),
175 + 'icon' => openstation_sanitize_dock_icon( (string) $args['icon'] ),
159 176 'script' => (string) $args['script'],
160 - 'score_columns' => desktop_mode_games_sanitize_score_columns( $args['score_columns'] ),
177 + 'score_columns' => openstation_games_sanitize_score_columns( $args['score_columns'] ),
161 178 'config' => is_array( $args['config'] ) ? $args['config'] : array(),
179 + 'window' => openstation_games_sanitize_window( $args['window'] ),
162 180 );
163 - desktop_mode_games_registry( $id, $entry );
181 + openstation_games_registry( $id, $entry );
164 182
165 183 /**
166 184 * Fires after a desktop game is successfully registered.
167 185 *
168 - * Does NOT fire when `desktop_mode_register_game()` returns a
186 + * Does NOT fire when `openstation_register_game()` returns a
169 187 * `WP_Error`.
170 188 *
171 189 * @param string $id The game id.
172 190 * @param array $entry The stored registry entry.
173 191 */
174 - do_action( 'desktop_mode_game_registered', $id, $entry );
192 + do_action( 'openstation_game_registered', $id, $entry );
175 193
176 194 return true;
177 195 }
178 196
179 197 /**
198 + * Normalize the `window` declaration — the game window's size, known
199 + * before its bundle is.
200 + *
201 + * **Why this is registered server-side at all**, when the JS def
202 + * already carries a `window` block: a game's bundle is heavyweight
203 + * (the game, its engine, sometimes a dictionary asset), so it is
204 + * fetched on first play rather than at boot. The shell therefore has
205 + * to open the window — and paint its loading spinner — *before* it has
206 + * ever seen the def. Without a size here that first window opens at
207 + * the framework default and would have to jump to the real size once
208 + * the def landed. With it, the size is right from the first frame.
209 + *
210 + * The JS def still wins once it arrives, so a game that declares only
211 + * in JS keeps working exactly as before; it simply gets the default
212 + * size on the first open of a session.
213 + *
214 + * Values are clamped rather than rejected: a nonsensical size is a
215 + * plugin bug that should not stop the game opening, and an unopenable
216 + * window is a worse answer than an oddly-sized one.
217 + *
218 + * @internal
219 + *
220 + * @param mixed $window Raw caller input.
221 + * @return array Sanitized subset of `{ width, height, minWidth, minHeight }`.
222 + */
223 +function openstation_games_sanitize_window( $window ) {
224 + if ( ! is_array( $window ) ) {
225 + return array();
226 + }
227 + $out = array();
228 + foreach ( array( 'width', 'height', 'minWidth', 'minHeight' ) as $key ) {
229 + if ( ! isset( $window[ $key ] ) || ! is_numeric( $window[ $key ] ) ) {
230 + continue;
231 + }
232 + $value = (int) $window[ $key ];
233 + if ( $value <= 0 ) {
234 + continue;
235 + }
236 + // The ceiling is generous on purpose — it exists to catch a
237 + // typo'd pixel value, not to have an opinion about how big a
238 + // game may be. The window manager clamps to the viewport anyway.
239 + $out[ $key ] = min( $value, 10000 );
240 + }
241 +
242 + return $out;
243 +}
244 +
245 +/**
180 246 * Normalize the `score_columns` declaration: drop rows without a
181 247 * valid key, default labels to the key, and clamp `type` to the
182 248 * supported set.
183 249 *
@@ -185,9 +251,9 @@
185 251 *
186 252 * @param mixed $columns Raw caller input.
187 253 * @return array[] Sanitized `{ key, label, type }` rows.
188 254 */
189 -function desktop_mode_games_sanitize_score_columns( $columns ) {
255 +function openstation_games_sanitize_score_columns( $columns ) {
190 256 if ( ! is_array( $columns ) ) {
191 257 return array();
192 258 }
193 259 $out = array();
@@ -214,14 +280,14 @@
214 280 }
215 281
216 282 /**
217 283 * Internal module-level registry for games registered via
218 - * {@see desktop_mode_register_game()}. Same static-store pattern as
284 + * {@see openstation_register_game()}. Same static-store pattern as
219 285 * the widget + wallpaper + native-window registries.
220 286 *
221 287 * @internal
222 288 */
223 -function desktop_mode_games_registry( $id = '', $entry = null ) {
289 +function openstation_games_registry( $id = '', $entry = null ) {
224 290 static $store = array();
225 291
226 292 if ( '' === (string) $id ) {
227 293 return $store;
@@ -242,36 +308,36 @@
242 308 *
243 309 * @param string $id Game id.
244 310 * @return bool Whether an entry was removed.
245 311 */
246 -function desktop_mode_unregister_game( $id ) {
312 +function openstation_unregister_game( $id ) {
247 313 $id = sanitize_key( (string) $id );
248 - if ( '' === $id || null === desktop_mode_games_registry( $id ) ) {
314 + if ( '' === $id || null === openstation_games_registry( $id ) ) {
249 315 return false;
250 316 }
251 - desktop_mode_games_registry( $id, '__unset__' );
317 + openstation_games_registry( $id, '__unset__' );
252 318 return true;
253 319 }
254 320
255 321 /**
256 - * The registered game entries with the `desktop_mode_games` filter
322 + * The registered game entries with the `openstation_games` filter
257 323 * applied. This is the read path everything else (payload, REST
258 324 * validation) goes through, so filter-registered games validate.
259 325 *
260 326 * @return array[] Entries keyed by game id.
261 327 */
262 -function desktop_mode_games_get_registered() {
263 - $registry = desktop_mode_games_registry();
328 +function openstation_games_get_registered() {
329 + $registry = openstation_games_registry();
264 330
265 331 /**
266 332 * Filters the server-declared game list. Mirrors the JS-side
267 - * `desktop-mode.games` filter so plugins can add, hide, or
333 + * `os.games` filter so plugins can add, hide, or
268 334 * override entries at boot without round-tripping through the
269 335 * JS registry.
270 336 *
271 337 * @param array[] $registry The registered game entries, keyed by id.
272 338 */
273 - $registry = apply_filters( 'desktop_mode_games', $registry );
339 + $registry = apply_filters( 'openstation_games', $registry );
274 340
275 341 return is_array( $registry ) ? $registry : array();
276 342 }
277 343
@@ -281,14 +347,14 @@
281 347 *
282 348 * @param string $id Game id.
283 349 * @return bool
284 350 */
285 -function desktop_mode_games_is_registered( $id ) {
351 +function openstation_games_is_registered( $id ) {
286 352 $id = sanitize_key( (string) $id );
287 353 if ( '' === $id ) {
288 354 return false;
289 355 }
290 - $registry = desktop_mode_games_get_registered();
356 + $registry = openstation_games_get_registered();
291 357 if ( isset( $registry[ $id ] ) ) {
292 358 return true;
293 359 }
294 360 // Filter authors may return a plain list instead of an id-keyed
@@ -307,15 +373,15 @@
307 373 * announced via the JS global its (lazily loaded) script sets up.
308 374 *
309 375 * @return array[]
310 376 */
311 -function desktop_mode_build_desktop_games_payload() {
377 +function openstation_build_desktop_games_payload() {
312 378 // The module doesn't load when the framework is disabled, so this
313 379 // only guards a mid-request flip (the admin just saved the toggle).
314 - if ( ! desktop_mode_games_enabled() ) {
380 + if ( ! openstation_games_enabled() ) {
315 381 return array();
316 382 }
317 - $registry = desktop_mode_games_get_registered();
383 + $registry = openstation_games_get_registered();
318 384 if ( empty( $registry ) ) {
319 385 return array();
320 386 }
321 387 $out = array();
@@ -323,9 +389,9 @@
323 389 if ( ! is_array( $entry ) || empty( $entry['id'] ) ) {
324 390 continue;
325 391 }
326 392 $handle = isset( $entry['script'] ) ? (string) $entry['script'] : '';
327 - $payload = desktop_mode_resolve_script_payload( $handle );
393 + $payload = openstation_resolve_script_payload( $handle );
328 394 $out[] = array(
329 395 'id' => (string) $entry['id'],
330 396 'title' => isset( $entry['title'] ) ? (string) $entry['title'] : '',
331 397 'description' => isset( $entry['description'] ) ? (string) $entry['description'] : '',
@@ -342,11 +408,19 @@
342 408 $entry['score_columns']
343 409 )
344 410 : array(),
345 411 'config' => array_merge(
346 - desktop_mode_games_framework_config(),
412 + openstation_games_framework_config(),
347 413 isset( $entry['config'] ) && is_array( $entry['config'] ) ? $entry['config'] : array()
348 414 ),
415 + // The window's size, known before its bundle is — so the
416 + // shell can open the window (and start its loading spinner)
417 + // on the click rather than after the download. Omitted
418 + // entirely when the game declared none, which reads as
419 + // "use the framework defaults" on the JS side.
420 + 'window' => isset( $entry['window'] ) && is_array( $entry['window'] )
421 + ? $entry['window']
422 + : array(),
349 423 'scriptUrl' => $payload['url'],
350 424 'scriptHandle' => $handle,
351 425 'scriptBefore' => $payload['before'],
352 426 'scriptAfter' => $payload['after'],
@@ -351,8 +425,11 @@
351 425 'scriptBefore' => $payload['before'],
352 426 'scriptAfter' => $payload['after'],
353 427 'scriptL10n' => $payload['l10n'],
354 428 'scriptTranslations' => $payload['translations'],
429 + // The handle's dependency closure, replayed before the bundle
430 + // on its lazy load — see `openstation_resolve_script_dependencies()`.
431 + 'scriptDeps' => openstation_resolve_script_dependencies( $handle ),
355 432 );
356 433 }
357 434 return $out;
358 435 }