PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 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 All 35 releases
← All changes | includes/games/registry.php +124 -64 0.9.7 → 1.1.11 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,10 +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
20 - * @since 0.9.6
19 + * @package OpenStation
21 20 */
22 21
23 22 defined( 'ABSPATH' ) || exit;
24 23
@@ -27,13 +26,13 @@
27 26 *
28 27 * Example:
29 28 *
30 29 * ```php
31 - * desktop_mode_register_game( 'inkfall', array(
30 + * openstation_register_game( 'inkfall', array(
32 31 * 'title' => __( 'Inkfall', 'desktop-mode' ),
33 32 * 'description' => __( 'Type the falling words.', 'desktop-mode' ),
34 33 * 'icon_svg' => '<svg …>…</svg>',
35 - * 'script' => 'desktop-mode-game-inkfall',
34 + * 'script' => 'os-game-inkfall',
36 35 * 'score_columns' => array(
37 36 * array( 'key' => 'score', 'label' => __( 'Score', 'desktop-mode' ), 'type' => 'number' ),
38 37 * array( 'key' => 'time', 'label' => __( 'Time', 'desktop-mode' ), 'type' => 'time' ),
39 38 * ),
@@ -41,11 +40,11 @@
41 40 * ) );
42 41 * ```
43 42 *
44 43 * ```js
45 - * // Inside desktop-mode-game-inkfall.js
46 - * window.desktopModeGames = window.desktopModeGames || {};
47 - * window.desktopModeGames.inkfall = {
44 + * // Inside os-game-inkfall.js
45 + * window.openStationGames = window.openStationGames || {};
46 + * window.openStationGames.inkfall = {
48 47 * id: 'inkfall',
49 48 * title: 'Inkfall',
50 49 * icon: 'data:image/svg+xml;base64,…',
51 50 * scoreColumns: [ … ],
@@ -52,12 +51,10 @@
52 51 * render: function ( ctx ) { return function () {}; },
53 52 * };
54 53 * ```
55 54 *
56 - * @since 0.9.6
57 - *
58 55 * @param string $id Game id (slug). Must match the
59 - * `window.desktopModeGames[<id>]` key the game's
56 + * `window.openStationGames[<id>]` key the game's
60 57 * JS publishes.
61 58 * @param array $args {
62 59 * @type string $title Launcher label. Required.
63 60 * @type string $description Plain-text description shown on the
@@ -78,16 +75,29 @@
78 75 * underneath (`wordsUrl` — see
79 76 * includes/games/config.php); the
80 77 * game's keys win on collision.
81 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.
82 92 * }
83 93 * @return true|WP_Error `true` on success; `WP_Error` otherwise.
84 94 */
85 -function desktop_mode_register_game( $id, $args = array() ) {
95 +function openstation_register_game( $id, $args = array() ) {
86 96 $id = sanitize_key( (string) $id );
87 97 if ( '' === $id ) {
88 - return desktop_mode_registration_error(
89 - 'desktop_mode_missing_id',
98 + return openstation_registration_error(
99 + 'openstation_missing_id',
90 100 __( 'Game id is required and must be a valid slug.', 'desktop-mode' )
91 101 );
92 102 }
93 103
@@ -99,10 +109,11 @@
99 109 'script' => '',
100 110 'score_columns' => array(),
101 111 'config' => array(),
102 112 'capabilities' => array(),
113 + 'window' => array(),
103 114 );
104 - $args = wp_parse_args( $args, $defaults );
115 + $args = wp_parse_args( $args, $defaults );
105 116
106 117 $svg = trim( (string) $args['icon_svg'] );
107 118 if ( '' !== $svg ) {
108 119 // Same defence-in-depth as desktop icons: the data URI is
@@ -108,17 +119,17 @@
108 119 // Same defence-in-depth as desktop icons: the data URI is
109 120 // consumed via `<img src=…>` (which sandboxes SVG scripts),
110 121 // but reject script tags outright anyway.
111 122 if ( false !== stripos( $svg, '<script' ) ) {
112 - return desktop_mode_registration_error(
113 - 'desktop_mode_invalid_icon_svg',
123 + return openstation_registration_error(
124 + 'openstation_invalid_icon_svg',
114 125 __( 'Game `icon_svg` must not contain a <script> tag.', 'desktop-mode' ),
115 126 array( 'id' => $id )
116 127 );
117 128 }
118 129 if ( 0 !== stripos( ltrim( $svg ), '<svg' ) ) {
119 - return desktop_mode_registration_error(
120 - 'desktop_mode_invalid_icon_svg',
130 + return openstation_registration_error(
131 + 'openstation_invalid_icon_svg',
121 132 __( 'Game `icon_svg` must start with a <svg> root element.', 'desktop-mode' ),
122 133 array( 'id' => $id )
123 134 );
124 135 }
@@ -126,30 +137,33 @@
126 137 }
127 138
128 139 foreach ( (array) $args['capabilities'] as $cap ) {
129 140 if ( ! current_user_can( (string) $cap ) ) {
130 - return desktop_mode_registration_error(
131 - 'desktop_mode_capability_denied',
141 + return openstation_registration_error(
142 + 'openstation_capability_denied',
132 143 sprintf(
133 144 /* translators: %s: capability slug. */
134 145 __( 'Current user lacks the %s capability required to register this game.', 'desktop-mode' ),
135 146 (string) $cap
136 147 ),
137 - array( 'capability' => (string) $cap, 'id' => $id )
148 + array(
149 + 'capability' => (string) $cap,
150 + 'id' => $id,
151 + )
138 152 );
139 153 }
140 154 }
141 155
142 156 if ( '' === (string) $args['title'] ) {
143 - return desktop_mode_registration_error(
144 - 'desktop_mode_missing_title',
157 + return openstation_registration_error(
158 + 'openstation_missing_title',
145 159 __( 'Game registration requires a non-empty `title`.', 'desktop-mode' ),
146 160 array( 'id' => $id )
147 161 );
148 162 }
149 163 if ( '' === (string) $args['script'] ) {
150 - return desktop_mode_registration_error(
151 - 'desktop_mode_missing_script',
164 + return openstation_registration_error(
165 + 'openstation_missing_script',
152 166 __( 'Game registration requires a `script` handle that publishes the game def.', 'desktop-mode' ),
153 167 array( 'id' => $id )
154 168 );
155 169 }
@@ -157,43 +171,89 @@
157 171 $entry = array(
158 172 'id' => $id,
159 173 'title' => (string) $args['title'],
160 174 'description' => sanitize_textarea_field( (string) $args['description'] ),
161 - 'icon' => desktop_mode_sanitize_dock_icon( (string) $args['icon'] ),
175 + 'icon' => openstation_sanitize_dock_icon( (string) $args['icon'] ),
162 176 'script' => (string) $args['script'],
163 - 'score_columns' => desktop_mode_games_sanitize_score_columns( $args['score_columns'] ),
177 + 'score_columns' => openstation_games_sanitize_score_columns( $args['score_columns'] ),
164 178 'config' => is_array( $args['config'] ) ? $args['config'] : array(),
179 + 'window' => openstation_games_sanitize_window( $args['window'] ),
165 180 );
166 - desktop_mode_games_registry( $id, $entry );
181 + openstation_games_registry( $id, $entry );
167 182
168 183 /**
169 184 * Fires after a desktop game is successfully registered.
170 185 *
171 - * Does NOT fire when `desktop_mode_register_game()` returns a
186 + * Does NOT fire when `openstation_register_game()` returns a
172 187 * `WP_Error`.
173 188 *
174 - * @since 0.9.6
175 - *
176 189 * @param string $id The game id.
177 190 * @param array $entry The stored registry entry.
178 191 */
179 - do_action( 'desktop_mode_game_registered', $id, $entry );
192 + do_action( 'openstation_game_registered', $id, $entry );
180 193
181 194 return true;
182 195 }
183 196
184 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 +/**
185 246 * Normalize the `score_columns` declaration: drop rows without a
186 247 * valid key, default labels to the key, and clamp `type` to the
187 248 * supported set.
188 249 *
189 - * @since 0.9.6
190 250 * @internal
191 251 *
192 252 * @param mixed $columns Raw caller input.
193 253 * @return array[] Sanitized `{ key, label, type }` rows.
194 254 */
195 -function desktop_mode_games_sanitize_score_columns( $columns ) {
255 +function openstation_games_sanitize_score_columns( $columns ) {
196 256 if ( ! is_array( $columns ) ) {
197 257 return array();
198 258 }
199 259 $out = array();
@@ -220,15 +280,14 @@
220 280 }
221 281
222 282 /**
223 283 * Internal module-level registry for games registered via
224 - * {@see desktop_mode_register_game()}. Same static-store pattern as
284 + * {@see openstation_register_game()}. Same static-store pattern as
225 285 * the widget + wallpaper + native-window registries.
226 286 *
227 - * @since 0.9.6
228 287 * @internal
229 288 */
230 -function desktop_mode_games_registry( $id = '', $entry = null ) {
289 +function openstation_games_registry( $id = '', $entry = null ) {
231 290 static $store = array();
232 291
233 292 if ( '' === (string) $id ) {
234 293 return $store;
@@ -246,45 +305,39 @@
246 305
247 306 /**
248 307 * Unregister a game. Safe to call for unknown ids.
249 308 *
250 - * @since 0.9.6
251 - *
252 309 * @param string $id Game id.
253 310 * @return bool Whether an entry was removed.
254 311 */
255 -function desktop_mode_unregister_game( $id ) {
312 +function openstation_unregister_game( $id ) {
256 313 $id = sanitize_key( (string) $id );
257 - if ( '' === $id || null === desktop_mode_games_registry( $id ) ) {
314 + if ( '' === $id || null === openstation_games_registry( $id ) ) {
258 315 return false;
259 316 }
260 - desktop_mode_games_registry( $id, '__unset__' );
317 + openstation_games_registry( $id, '__unset__' );
261 318 return true;
262 319 }
263 320
264 321 /**
265 - * The registered game entries with the `desktop_mode_games` filter
322 + * The registered game entries with the `openstation_games` filter
266 323 * applied. This is the read path everything else (payload, REST
267 324 * validation) goes through, so filter-registered games validate.
268 325 *
269 - * @since 0.9.6
270 - *
271 326 * @return array[] Entries keyed by game id.
272 327 */
273 -function desktop_mode_games_get_registered() {
274 - $registry = desktop_mode_games_registry();
328 +function openstation_games_get_registered() {
329 + $registry = openstation_games_registry();
275 330
276 331 /**
277 332 * Filters the server-declared game list. Mirrors the JS-side
278 - * `desktop-mode.games` filter so plugins can add, hide, or
333 + * `os.games` filter so plugins can add, hide, or
279 334 * override entries at boot without round-tripping through the
280 335 * JS registry.
281 336 *
282 - * @since 0.9.6
283 - *
284 337 * @param array[] $registry The registered game entries, keyed by id.
285 338 */
286 - $registry = apply_filters( 'desktop_mode_games', $registry );
339 + $registry = apply_filters( 'openstation_games', $registry );
287 340
288 341 return is_array( $registry ) ? $registry : array();
289 342 }
290 343
@@ -291,19 +344,17 @@
291 344 /**
292 345 * Whether a game id is known to the server registry (post-filter).
293 346 * REST routes 404 unknown games through this.
294 347 *
295 - * @since 0.9.6
296 - *
297 348 * @param string $id Game id.
298 349 * @return bool
299 350 */
300 -function desktop_mode_games_is_registered( $id ) {
351 +function openstation_games_is_registered( $id ) {
301 352 $id = sanitize_key( (string) $id );
302 353 if ( '' === $id ) {
303 354 return false;
304 355 }
305 - $registry = desktop_mode_games_get_registered();
356 + $registry = openstation_games_get_registered();
306 357 if ( isset( $registry[ $id ] ) ) {
307 358 return true;
308 359 }
309 360 // Filter authors may return a plain list instead of an id-keyed
@@ -320,19 +371,17 @@
320 371 * Build the game list for the shell payload. Only metadata + the
321 372 * resolved script URL cross the wire; the game's render callback is
322 373 * announced via the JS global its (lazily loaded) script sets up.
323 374 *
324 - * @since 0.9.6
325 - *
326 375 * @return array[]
327 376 */
328 -function desktop_mode_build_desktop_games_payload() {
377 +function openstation_build_desktop_games_payload() {
329 378 // The module doesn't load when the framework is disabled, so this
330 379 // only guards a mid-request flip (the admin just saved the toggle).
331 - if ( ! desktop_mode_games_enabled() ) {
380 + if ( ! openstation_games_enabled() ) {
332 381 return array();
333 382 }
334 - $registry = desktop_mode_games_get_registered();
383 + $registry = openstation_games_get_registered();
335 384 if ( empty( $registry ) ) {
336 385 return array();
337 386 }
338 387 $out = array();
@@ -340,9 +389,9 @@
340 389 if ( ! is_array( $entry ) || empty( $entry['id'] ) ) {
341 390 continue;
342 391 }
343 392 $handle = isset( $entry['script'] ) ? (string) $entry['script'] : '';
344 - $payload = desktop_mode_resolve_script_payload( $handle );
393 + $payload = openstation_resolve_script_payload( $handle );
345 394 $out[] = array(
346 395 'id' => (string) $entry['id'],
347 396 'title' => isset( $entry['title'] ) ? (string) $entry['title'] : '',
348 397 'description' => isset( $entry['description'] ) ? (string) $entry['description'] : '',
@@ -359,11 +408,19 @@
359 408 $entry['score_columns']
360 409 )
361 410 : array(),
362 411 'config' => array_merge(
363 - desktop_mode_games_framework_config(),
412 + openstation_games_framework_config(),
364 413 isset( $entry['config'] ) && is_array( $entry['config'] ) ? $entry['config'] : array()
365 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(),
366 423 'scriptUrl' => $payload['url'],
367 424 'scriptHandle' => $handle,
368 425 'scriptBefore' => $payload['before'],
369 426 'scriptAfter' => $payload['after'],
@@ -368,8 +425,11 @@
368 425 'scriptBefore' => $payload['before'],
369 426 'scriptAfter' => $payload['after'],
370 427 'scriptL10n' => $payload['l10n'],
371 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 ),
372 432 );
373 433 }
374 434 return $out;
375 435 }