rgame 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +3 -5
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +167 -12
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
@@ -0,0 +1,67 @@
1
+ /*
2
+ * locale.c — the user's preferred locales, as the operating system reports
3
+ * them through SDL.
4
+ *
5
+ * rgame_preferred_locales is a thin shim over SDL_GetPreferredLocales; the
6
+ * joining and the buffer contract are rgame_locale_append, which is pure and
7
+ * Check-tested. Choosing among the locales is not here at all: it is
8
+ * RGame::Engine::I18n.choose, specced headless.
9
+ *
10
+ * SDL answers without SDL_Init (measured on 2.0.20 under Linux, where it reads
11
+ * LANG and then LANGUAGE), so none of this belongs to an app.
12
+ */
13
+
14
+ #include "app/locale.h"
15
+
16
+ #include <SDL2/SDL.h>
17
+ #include <string.h>
18
+
19
+ #include "rgame/core.h"
20
+
21
+ static size_t append_text(char *out, size_t capacity, size_t length, const char *text) {
22
+ for (; *text; text++, length++) {
23
+ if (length + 1 < capacity) {
24
+ out[length] = *text;
25
+ }
26
+ }
27
+ return length;
28
+ }
29
+
30
+ size_t rgame_locale_append(char *out, size_t capacity, size_t length, const char *language,
31
+ const char *country) {
32
+ if (language == NULL || language[0] == '\0') {
33
+ return length;
34
+ }
35
+
36
+ if (length > 0) {
37
+ length = append_text(out, capacity, length, ",");
38
+ }
39
+ length = append_text(out, capacity, length, language);
40
+ if (country != NULL && country[0] != '\0') {
41
+ length = append_text(out, capacity, length, "-");
42
+ length = append_text(out, capacity, length, country);
43
+ }
44
+
45
+ if (capacity > 0) {
46
+ out[length < capacity ? length : capacity - 1] = '\0';
47
+ }
48
+ return length;
49
+ }
50
+
51
+ size_t rgame_preferred_locales(char *out, size_t capacity) {
52
+ if (capacity > 0) {
53
+ out[0] = '\0';
54
+ }
55
+
56
+ SDL_Locale *locales = SDL_GetPreferredLocales();
57
+ if (locales == NULL) {
58
+ return 0;
59
+ }
60
+
61
+ size_t length = 0;
62
+ for (const SDL_Locale *locale = locales; locale->language != NULL; locale++) {
63
+ length = rgame_locale_append(out, capacity, length, locale->language, locale->country);
64
+ }
65
+ SDL_free(locales);
66
+ return length;
67
+ }
@@ -0,0 +1,28 @@
1
+ #ifndef RGAME_LOCALE_H
2
+ #define RGAME_LOCALE_H
3
+
4
+ #include <stddef.h>
5
+
6
+ /*
7
+ * The pure half of reading the user's preferred locales: joining them into one
8
+ * "de-AT,en" string in a caller's buffer. No SDL, so the Check suite covers the
9
+ * truncation contract on every platform, whatever locales the machine it runs
10
+ * on happens to prefer. locale.c's rgame_preferred_locales is the SDL shim
11
+ * that feeds it.
12
+ */
13
+
14
+ /*
15
+ * Appends one locale to a list whose full length so far is `length`: a comma
16
+ * unless the list is empty, then `language`, then "-" and `country` when a
17
+ * country is given. Returns the list's new full length.
18
+ *
19
+ * It behaves like snprintf: `out` receives as much as fits in `capacity`,
20
+ * always NUL-terminated when `capacity` is non-zero, and the return value is
21
+ * the length the whole list needs, so a result >= `capacity` means it was
22
+ * truncated. `length` may already exceed `capacity`. A NULL or empty
23
+ * `language` appends nothing.
24
+ */
25
+ size_t rgame_locale_append(char *out, size_t capacity, size_t length, const char *language,
26
+ const char *country);
27
+
28
+ #endif /* RGAME_LOCALE_H */
@@ -53,6 +53,11 @@ struct rgame_audio {
53
53
  * lookup to find.
54
54
  */
55
55
  int forced_no_device;
56
+ /*
57
+ * How many things still need this engine: the owner counts as one, and so
58
+ * does every sample and song loaded from it. See audio_release.
59
+ */
60
+ int refs;
56
61
  };
57
62
 
58
63
  struct rgame_sample {
@@ -213,6 +218,8 @@ static rgame_audio *create_audio(int offline, unsigned int sample_rate, char *er
213
218
  return NULL;
214
219
  }
215
220
 
221
+ /* The caller's own claim. Sounds add theirs as they are loaded. */
222
+ audio->refs = 1;
216
223
  return audio;
217
224
  }
218
225
 
@@ -234,8 +241,28 @@ unsigned int rgame_audio_read(rgame_audio *audio, float *out, unsigned int frame
234
241
  return (unsigned int)read;
235
242
  }
236
243
 
237
- void rgame_audio_destroy(rgame_audio *audio) {
238
- if (!audio) {
244
+ /*
245
+ * Drops one claim on the device, and tears it down when the last one goes.
246
+ *
247
+ * The device has to outlive every sound made from it, and a refcount is what
248
+ * makes that true regardless of the order things are released in. That order is
249
+ * not ours to choose: Ruby's collector frees unreachable objects in whatever
250
+ * order it sweeps them, so a Song and the Audio it came from can be freed in
251
+ * the same pass with the Audio going first. Tearing down a *streaming* voice
252
+ * then asks the resource manager's job thread to acknowledge it — and that
253
+ * thread went with the engine, so the wait never returns and the process hangs
254
+ * rather than crashing.
255
+ *
256
+ * Marking the device from the sound, which audio_ext.c does, is a different
257
+ * guarantee: it keeps the device alive while a sound is still *reachable*. It
258
+ * says nothing about two objects that became garbage together.
259
+ *
260
+ * So the owner holds a reference, every sound holds one, and the last one out
261
+ * does the uninit. Same shape as the refcounted texture sheet in
262
+ * graphics/texture.c, and for the same kind of reason.
263
+ */
264
+ static void audio_release(rgame_audio *audio) {
265
+ if (!audio || --audio->refs > 0) {
239
266
  return;
240
267
  }
241
268
 
@@ -246,6 +273,10 @@ void rgame_audio_destroy(rgame_audio *audio) {
246
273
  free(audio);
247
274
  }
248
275
 
276
+ void rgame_audio_destroy(rgame_audio *audio) {
277
+ audio_release(audio);
278
+ }
279
+
249
280
  void rgame_audio_set_volume(rgame_audio *audio, float volume) {
250
281
  if (audio) {
251
282
  ma_engine_set_volume(&audio->engine, clamp_volume(volume));
@@ -367,6 +398,7 @@ rgame_sample *rgame_sample_load(rgame_audio *audio, const char *path, char *err,
367
398
  strcpy(sample->path, path);
368
399
 
369
400
  sample->audio = audio;
401
+ audio->refs++;
370
402
  live_sounds++;
371
403
  return sample;
372
404
  }
@@ -394,8 +426,10 @@ void rgame_sample_destroy(rgame_sample *sample) {
394
426
  * same file hold their own, so the last one out frees it. */
395
427
  ma_sound_uninit(&sample->decoded);
396
428
  free(sample->path);
429
+ rgame_audio *audio = sample->audio;
397
430
  free(sample);
398
431
  live_sounds--;
432
+ audio_release(audio);
399
433
  }
400
434
 
401
435
  void rgame_sample_play(rgame_sample *sample) {
@@ -470,6 +504,7 @@ rgame_song *rgame_song_load(rgame_audio *audio, const char *path, char *err, siz
470
504
  }
471
505
 
472
506
  song->audio = audio;
507
+ audio->refs++;
473
508
  live_sounds++;
474
509
  return song;
475
510
  }
@@ -480,8 +515,10 @@ void rgame_song_destroy(rgame_song *song) {
480
515
  }
481
516
 
482
517
  ma_sound_uninit(&song->sound);
518
+ rgame_audio *audio = song->audio;
483
519
  free(song);
484
520
  live_sounds--;
521
+ audio_release(audio);
485
522
  }
486
523
 
487
524
  void rgame_song_play(rgame_song *song, int looping) {
@@ -1,30 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Manual/visual smoke test for the core extension (layer 3 — the real
4
- # SDL/GL path). Build it first, then run this from the project root:
5
- #
6
- # make ext-core
7
- # ruby ext/rgame_core/example.rb
8
- #
9
- # It opens a window with one of each drawing primitive in it. Arrow keys (or a
10
- # controller's dpad/stick) move the marker; Escape or closing the window quits.
11
- # This is the Ruby-side mirror of src/main.c, and it exists to exercise every
12
- # callback and every draw call the engine offers.
13
- #
14
- # Pass a sound file (Ogg Vorbis or WAV) to check audio as well:
15
- #
16
- # ruby ext/rgame_core/example.rb assets/theme.ogg
17
- #
18
- # Space plays it as a one-shot sample, Return starts and stops it as looping
19
- # music. There is no asset here to default to, and that is on purpose: this is
20
- # the only place a *real* sound device is driven — every automated test runs
21
- # against a null or offline one — so what it is worth checking is your own
22
- # file, out of your own speakers.
23
- #
24
- # The load path points at lib/, not at this directory: `make ext-core`
25
- # copies the built core_ext.so to lib/rgame/, so this runs against exactly
26
- # the layout a user of the library would see.
27
-
28
3
  $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
29
4
  require 'rgame'
30
5
  require 'rgame/core'
@@ -41,8 +16,6 @@ class Example < RGame::Core::App
41
16
  YELLOW = Color.new(224, 192, 64)
42
17
  TRANSLUCENT_WHITE = Color.new(255, 255, 255, 128)
43
18
 
44
- # Frozen and chosen between rather than built: #draw runs sixty times a
45
- # second, and a string per frame is a garbage collection waiting to happen.
46
19
  AUDIO_HINT_NONE = 'audio: pass a sound file on the command line to try it'
47
20
  AUDIO_HINT_STOPPED = 'audio: Space plays a sample, Return starts the music'
48
21
  AUDIO_HINT_PLAYING = 'audio: Space plays a sample, Return stops the music'
@@ -62,10 +35,6 @@ class Example < RGame::Core::App
62
35
  @baked = nil
63
36
  @device = Controls::KEYBOARD
64
37
 
65
- # The device is opened whether or not there is anything to play through it,
66
- # so that starting with no sound card takes the same path as starting with
67
- # one. Nothing here is tied to the window: audio has no GL context and
68
- # survives one being recreated.
69
38
  @audio = RGame::Core::Audio.new
70
39
  puts "audio backend: #{@audio.backend}"
71
40
  return unless sound_path
@@ -78,8 +47,6 @@ class Example < RGame::Core::App
78
47
  # here rather than per tick is the pattern the engine is shaped around — one
79
48
  # sample per frame, reused by however many catch-up ticks follow.
80
49
  def frame_begin
81
- # Poll rather than track: Gamepad answers straight from the engine, so
82
- # there is no local copy of the connection state to keep in step.
83
50
  @device = @pads.connected?(0) ? @pads.device(0) : Controls::KEYBOARD
84
51
  end
85
52
 
@@ -93,7 +60,6 @@ class Example < RGame::Core::App
93
60
  @cursor_y -= speed if held?(Controls::KEY_UP, Controls::PAD_DPAD_UP)
94
61
  @cursor_y += speed if held?(Controls::KEY_DOWN, Controls::PAD_DPAD_DOWN)
95
62
 
96
- # Analog sticks are additive on top of the dpad; the keyboard reads 0.0.
97
63
  @cursor_x += @input.axis(Controls::AXIS_LEFT_X, device: @device) * speed
98
64
  @cursor_y += @input.axis(Controls::AXIS_LEFT_Y, device: @device) * speed
99
65
  end
@@ -115,35 +81,22 @@ class Example < RGame::Core::App
115
81
  def draw
116
82
  r = @renderer
117
83
 
118
- # Baked on the first frame and replayed after that: forty rectangles cost
119
- # one call per frame instead of forty. A real game bakes its tile layers
120
- # this way. Recording has to happen inside #draw, which is why it is here
121
- # rather than in initialize.
122
84
  @baked ||= r.record do
123
85
  40.times { |i| r.rect(i * 18, 0, 12, 12, color: GREEN, z: 0) }
124
86
  end
125
- # Scrolls with the same value the spinning square turns by, so it is
126
- # visibly the *replay* moving and not a rebake.
127
87
  @baked.draw((@spin % 18) - 18, 560)
128
88
  r.rect(40, 40, 160, 100, color: RED, z: 0)
129
- # Higher z wins wherever they overlap, whatever order the calls came in.
130
89
  r.rect(120, 90, 160, 100, color: TRANSLUCENT_WHITE, z: 1)
131
90
  r.triangle(400, 40, 480, 180, 320, 180, color: GREEN, z: 0)
132
91
  r.circle(620, 110, 70, color: BLUE, z: 0)
133
92
  r.line(40, 240, 760, 240, thickness: 6, color: YELLOW, z: 0)
134
93
 
135
- # The transform stack: a square turning about its own centre.
136
94
  r.rotated(@spin, 400, 380) { r.rect(340, 320, 120, 120, color: GREEN, z: 0) }
137
95
 
138
- # The clip stack: the rectangle is twice the size of what shows.
139
96
  r.clipped(60, 480, 200, 80) { r.rect(60, 440, 400, 160, color: RED, z: 0) }
140
97
 
141
- # The cursor the arrow keys move.
142
98
  r.debug_box(@cursor_x - 8, @cursor_y - 8, 16, 16)
143
99
 
144
- # Text, including accents and punctuation the shipped font has to cover, so
145
- # a look at the window checks kerning and coverage at once. The second line
146
- # is centred using text_width, which is what a UI actually does with it.
147
100
  r.text('rgame — Grüße, œuvre, 5 €', 40, 270, color: YELLOW)
148
101
  label = format('%d fps', fps)
149
102
  r.text(label, 400 - (r.text_width(label) / 2), 270 + r.text_height)
@@ -155,8 +108,6 @@ class Example < RGame::Core::App
155
108
  def button_down(id)
156
109
  case id
157
110
  when Controls::KEY_ESCAPE then close
158
- # Held down, this is the overlap check: each press is another voice rather
159
- # than a restart, so a fast run of them should pile up rather than stutter.
160
111
  when Controls::KEY_SPACE then @sample&.play
161
112
  when Controls::KEY_RETURN then toggle_music
162
113
  end
@@ -1,117 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # extconf.rb — run by `ruby extconf.rb` to generate a Makefile via mkmf.
4
- #
5
- # mkmf is Ruby's "make makefile" library. It probes the system (headers,
6
- # libraries, pkg-config) and writes a Makefile that knows how to compile a
7
- # loadable extension (`core_ext.so`) against the running Ruby's headers.
8
- # This is the same tool that `gem install` uses under the hood, which is why the
9
- # root Makefile was written to look like what mkmf emits — the mental model
10
- # carries straight over.
11
- #
12
- # This is the SDL/OpenGL half of the project: everything under RGame::Core.
13
- # The graphics-free half lives in ../rgame_util (RGame::Util) and links none of
14
- # these libraries.
15
-
16
3
  require 'mkmf'
17
4
 
18
- # The engine sources live in this directory rather than a top-level src/:
19
- # `gem install` unpacks the gem and runs this script, and an extension must be
20
- # buildable from its own directory.
21
- #
22
- # They are grouped into subdirectories by subsystem (app, graphics, text, input,
23
- # audio) plus ruby/ for the Ruby-facing glue and vendor/ for third-party code.
24
- # mkmf's default is to compile every .c in the extension's *own* directory and
25
- # nothing deeper, so the source list has to be spelled out — which is what
26
- # $srcs and $VPATH below do.
27
- #
28
- # $srcs — what to compile. mkmf names each object after the source's
29
- # *basename*, so the .o files land flat in this directory no matter
30
- # how deep the .c was. Basenames therefore have to be unique across
31
- # the subdirectories; mkmf aborts with "source files duplication" if
32
- # they ever aren't, so that rule enforces itself.
33
- # $VPATH — where make looks for a source when a rule names it by basename.
34
- # The generic `.c.o` rule mkmf emits does exactly that, so without
35
- # this every object would come up as a missing prerequisite.
36
- #
37
- # vendor/ contributes only its <name>_impl.c translation units. The libraries
38
- # themselves are #included *by* those files (stb_vorbis even ships as a .c), so
39
- # compiling them separately would duplicate every symbol in them.
40
5
  SOURCE_DIRS = %w[app graphics text input audio ruby].freeze
41
6
 
42
- # Dir.glob sorts its results, so the object list is the same on every machine.
43
7
  $srcs = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.c") } +
44
8
  Dir.glob("#{$srcdir}/vendor/*_impl.c")
45
9
 
46
10
  (SOURCE_DIRS + %w[vendor]).each { |dir| $VPATH << "$(srcdir)/#{dir}" }
47
11
 
48
- # The extension directory itself, so a cross-subsystem include names the folder
49
- # it comes from: graphics/canvas.c says `#include "graphics/clip.h"`, and text/
50
- # reaching into graphics/ is visible in the source rather than hidden in a
51
- # search path. It is also what resolves `#include "vendor/stb_image.h"`.
52
- # $(srcdir) stays literal here — make expands it, not Ruby.
53
12
  $INCFLAGS << ' -I$(srcdir)'
54
13
 
55
- # Public API header (include/rgame/core.h), so `#include "rgame/core.h"`
56
- # resolves.
57
14
  $INCFLAGS << ' -I$(srcdir)/include'
58
15
 
59
- # RGame::Util's colour header, for the RGBA byte order the renderer writes into
60
- # every vertex. It is header-only (static inline), so this creates no link
61
- # dependency between the two extensions — they stay separate .so files. Both
62
- # directories ship together in the gem, so the relative path holds there too.
63
16
  $INCFLAGS << ' -I$(srcdir)/../rgame_util'
64
17
 
65
- # SDL2: pkg_config("sdl2") shells out to pkg-config and folds the resulting
66
- # cflags and libs into mkmf's globals ($CFLAGS/$libs) for us. Returns nil if
67
- # SDL2 isn't found, so we can fail with a clear message instead of a confusing
68
- # later compile error.
69
18
  abort 'SDL2 not found (pkg-config --exists sdl2 failed). Install libsdl2-dev.' unless pkg_config('sdl2')
70
19
 
71
- # The GL header, probed as the sources actually spell it: SDL's own
72
- # <SDL2/SDL_opengl.h>, which resolves to GL/gl.h, OpenGL/gl.h or the Windows SDK's
73
- # per platform. Probing GL/gl.h directly would ask the wrong question off Linux.
74
- #
75
- # It needs SDL's cflags, which pkg_config folded into $CFLAGS just above — this
76
- # is also what makes the probe a real check of the include path rather than of
77
- # the filesystem.
78
20
  abort 'SDL2 OpenGL header not found (SDL2/SDL_opengl.h). Install libsdl2-dev.' unless have_header('SDL2/SDL_opengl.h')
79
21
 
80
- # Three platforms, three different ways to link the same OpenGL — the one place
81
- # in this file that has to branch, and the mirror of the root Makefile's own
82
- # branch (see its GL_LIBS). The engine calls only GL 1.1 entry points, which all
83
- # three expose directly, so no extension loader (GLAD/GLEW) is involved anywhere;
84
- # see CLAUDE.md's Conventions on why the legacy profile is deliberate.
85
- #
86
- # Probed rather than assumed, because this script is what runs on someone else's
87
- # machine during `gem install` and a missing OpenGL is a routine thing to hit
88
- # there. Without a probe it surfaces as a linker error about an undefined
89
- # `glClear` at the end of a long build; with one, the install stops on a
90
- # sentence naming what to install. Same reasoning as the SDL2 abort above — and
91
- # the message is per-platform, since "install libgl1-mesa-dev" is useless advice
92
- # on a Mac.
93
- #
94
- # `have_library` appends `-lGL`/`-lopengl32` to $libs on success; `have_framework`
95
- # appends `-framework OpenGL` to $LDFLAGS. Both end up on the link line.
96
22
  case RbConfig::CONFIG['host_os']
97
23
  when /darwin/
98
- # macOS ships OpenGL as a framework rather than a library, so there is no
99
- # `-lGL` to find. It is part of the OS — a failure here means the toolchain
100
- # is missing, not the graphics stack.
101
24
  unless have_framework('OpenGL')
102
25
  abort 'OpenGL framework not found. It ships with macOS, so this usually means ' \
103
26
  'the command line tools are missing: xcode-select --install'
104
27
  end
105
28
  when /mingw|mswin|cygwin/
106
- # Windows' OpenGL is an OS component: opengl32.dll is in system32 on every
107
- # installation, and the import library comes with the toolchain. Nothing to
108
- # install from a graphics vendor.
109
29
  unless have_library('opengl32', 'glClear')
110
30
  abort 'opengl32 not found. It is part of Windows itself, so this means an ' \
111
31
  'incomplete MSYS2 toolchain: pacman -S mingw-w64-ucrt-x86_64-gcc'
112
32
  end
113
33
  else
114
- # Linux and the BSDs.
115
34
  unless have_library('GL', 'glClear')
116
35
  abort 'OpenGL library not found (-lGL). Install the OpenGL development package: ' \
117
36
  'libgl1-mesa-dev on Debian/Ubuntu, mesa-libGL-devel on Fedora.'
@@ -120,45 +39,13 @@ end
120
39
 
121
40
  $libs = append_library($libs, 'm')
122
41
 
123
- # miniaudio's threading, and the dlopen it uses to find ALSA or PulseAudio at
124
- # runtime — the property that makes audio cost no new system dependency.
125
- #
126
- # Probed rather than branched, because unlike OpenGL above the *answer* differs
127
- # per platform but the question does not: link them where they exist. On
128
- # glibc both are system libraries nobody installs; macOS resolves `-lpthread`
129
- # into libSystem and needs no `-ldl`; MSYS2 supplies winpthreads. Appending
130
- # either unconditionally would break the link wherever it is absent.
131
42
  $libs = append_library($libs, 'pthread') if have_library('pthread')
132
43
  $libs = append_library($libs, 'dl') if have_library('dl')
133
44
 
134
- # Match the project's C standard and warning flags. Note: gnu17, not plain
135
- # c17 — Ruby's headers occasionally lean on GNU extensions, and gnu17 is a
136
- # superset of c17 so core.c (which targets c17) still compiles fine.
137
45
  $CFLAGS << ' -std=gnu17 -Wall -Wextra'
138
46
 
139
- # Emits the Makefile. "rgame/core_ext" -> loaded via
140
- # `require "rgame/core_ext"`, entry point Init_core_ext (the basename)
141
- # in core_ext.c. Namespacing it under rgame/ mirrors rgame/util_ext, keeps
142
- # both extensions off the top of the load path, and — importantly — leaves the
143
- # bare name "rgame" to lib/rgame.rb, which is the pure-Ruby entry point.
144
47
  create_makefile('rgame/core_ext')
145
48
 
146
- # The vendored implementations, compiled with warnings off (see
147
- # vendor/README.md). mkmf has no per-file flag setting, so the rules are
148
- # appended to the Makefile it just wrote. An explicit rule for a specific
149
- # target beats mkmf's generic .c.o suffix rule, so these are what get used for
150
- # those objects and nothing else.
151
- #
152
- # Written from one list rather than one block per library: a second hand-copied
153
- # rule is how the first one drifts. They are explicit rules rather than a `%`
154
- # pattern because mkmf's Makefiles are meant to work with whatever `make` the
155
- # platform has, and pattern rules are a GNU extension.
156
- #
157
- # `-w` comes last on the command line and switches every warning back off,
158
- # which is simpler and more robust than trying to subtract -Wall -Wextra from
159
- # $(CFLAGS) — everything else about how the extension compiles stays identical.
160
- # Each vendored library's implementation TU (<name>_impl.c) and the file it
161
- # instantiates. stb_vorbis is the odd one out: it ships as a .c, not a .h.
162
49
  VENDORED = {
163
50
  'stb_image' => 'stb_image.h',
164
51
  'stb_truetype' => 'stb_truetype.h',
@@ -176,18 +63,6 @@ File.open('Makefile', 'a') do |makefile|
176
63
  MAKE
177
64
  end
178
65
 
179
- # Rebuild everything when any of our headers changes.
180
- #
181
- # mkmf emits `$(OBJS): $(HDRS)` for this, but it builds HDRS by globbing the
182
- # extension's own directory only — with the headers a level down in
183
- # graphics/, text/ and the rest, HDRS comes out empty and editing a header
184
- # rebuilds *nothing*. That failure is silent: the build succeeds and links
185
- # objects compiled against the previous version of the struct.
186
- #
187
- # One coarse dependency rather than a real per-object dependency scan, for the
188
- # same reason the root Makefile treats the vendored sources as one list: this
189
- # is a small project where a full rebuild costs seconds, and a hand-maintained
190
- # dependency graph is a thing to get quietly wrong.
191
66
  headers = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.h") }
192
67
  .map { |path| "$(srcdir)/#{path.delete_prefix("#{$srcdir}/")}" }
193
68
 
@@ -19,8 +19,19 @@ extern "C" {
19
19
 
20
20
  typedef struct rgame_app rgame_app;
21
21
 
22
- /* Creates the window, GL context and internal state. Returns NULL on failure. */
23
- rgame_app *rgame_app_create(int width, int height, const char *title);
22
+ /*
23
+ * Creates the window, GL context and internal state. Returns NULL on failure.
24
+ *
25
+ * `fullscreen` non-zero opens the window fullscreen straight away, rather than
26
+ * opening it windowed and switching after. The difference is visible: a switch
27
+ * made after creation shows one windowed frame first, which is the flash a
28
+ * player sees when a game starts up wrong.
29
+ *
30
+ * `width` and `height` still matter when starting fullscreen — they are the
31
+ * size the window returns to when it leaves fullscreen, and the size SDL keeps
32
+ * for it in the meantime.
33
+ */
34
+ rgame_app *rgame_app_create(int width, int height, const char *title, int fullscreen);
24
35
 
25
36
  /* Destroys the GL context/window and frees the app. Safe to call with NULL. */
26
37
  void rgame_app_destroy(rgame_app *app);
@@ -272,6 +283,19 @@ void rgame_app_close(rgame_app *app);
272
283
  int rgame_app_width(const rgame_app *app);
273
284
  int rgame_app_height(const rgame_app *app);
274
285
 
286
+ /*
287
+ * Fullscreen, toggled at any time. This is *desktop* fullscreen: the window
288
+ * takes the whole screen at the screen's own resolution, rather than asking the
289
+ * display to change mode. That makes the switch instant and reversible, costs
290
+ * no mode list to choose from, and leaves other windows where they were.
291
+ *
292
+ * Switching resizes the window, so the `resize` callback fires with the new
293
+ * size — the same path a user dragging a window edge takes. Nothing else needs
294
+ * telling.
295
+ */
296
+ void rgame_app_set_fullscreen(rgame_app *app, int fullscreen);
297
+ int rgame_app_fullscreen(const rgame_app *app);
298
+
275
299
  /* Window title. The returned string is owned by the window, not the caller. */
276
300
  const char *rgame_app_title(const rgame_app *app);
277
301
  void rgame_app_set_title(rgame_app *app, const char *title);
@@ -317,6 +341,18 @@ unsigned int rgame_app_ticks_ms(const rgame_app *app);
317
341
  /* Most recent frames-per-second reading (updated ~once per second). */
318
342
  double rgame_app_fps(const rgame_app *app);
319
343
 
344
+ /*
345
+ * The user's preferred locales, most preferred first, written into `out` as
346
+ * "de-AT,en": a language, a hyphen and a country when the OS names one, and
347
+ * no trailing comma. Needs no app.
348
+ *
349
+ * Returns the length of the whole list, not counting the NUL, like snprintf:
350
+ * a result >= `capacity` means `out` holds a truncated prefix, and a buffer of
351
+ * result + 1 bytes fits it. `out` is always NUL-terminated when `capacity` is
352
+ * non-zero. Returns 0, with `out` empty, when the OS reports none.
353
+ */
354
+ size_t rgame_preferred_locales(char *out, size_t capacity);
355
+
320
356
  /*
321
357
  * ---------------------------------------------------------------------------
322
358
  * Images
@@ -114,10 +114,15 @@ static VALUE audio_s_debug_live_sounds(VALUE klass) {
114
114
  * ------------------------------------------------------------------------- */
115
115
 
116
116
  /*
117
- * Both wrap a C handle plus the Ruby Audio it came from. Marking the device is
118
- * what stops the collector taking it first: the sound is a voice inside that
119
- * device's mixer, and freeing the mixer out from under it would be a crash at
120
- * an unpredictable moment.
117
+ * Both wrap a C handle plus the Ruby Audio it came from. Marking the device
118
+ * keeps it alive for as long as any sound made from it is *reachable*: the
119
+ * sound is a voice inside that device's mixer, and freeing the mixer out from
120
+ * under a live sound would be a crash at an unpredictable moment.
121
+ *
122
+ * It is only half the guarantee, and the half it leaves out is worth knowing.
123
+ * When a sound and its device become garbage in the same collection, marking
124
+ * has nothing left to say and the sweep frees them in whatever order it reaches
125
+ * them. audio.c refcounts the device for that case; see audio_release there.
121
126
  */
122
127
  typedef struct {
123
128
  void *handle; /* rgame_sample * or rgame_song * */
@@ -312,7 +317,7 @@ void rgame_init_audio(VALUE mCore) {
312
317
  rb_define_class_under(cSong, "LoadError", rb_eStandardError);
313
318
  rb_define_alloc_func(cSong, song_alloc);
314
319
  rb_define_method(cSong, "initialize", song_initialize, 2);
315
- rb_define_method(cSong, "play_looping", song_play, 1);
320
+ rb_define_private_method(cSong, "play_looping", song_play, 1);
316
321
  rb_define_method(cSong, "stop", song_stop, 0);
317
322
  rb_define_method(cSong, "playing?", song_playing_p, 0);
318
323
  rb_define_method(cSong, "looping?", song_looping_p, 0);
@@ -61,6 +61,7 @@ static ID id_width;
61
61
  static ID id_height;
62
62
  static ID id_caption;
63
63
  static ID id_media_root;
64
+ static ID id_fullscreen;
64
65
 
65
66
  /* ------------------------------------------------------------------------- *
66
67
  * rgame_app lifetime, wrapped as a Ruby object
@@ -110,8 +111,9 @@ static VALUE app_alloc(VALUE klass) {
110
111
  return TypedData_Wrap_Struct(klass, &app_data_type, NULL);
111
112
  }
112
113
 
113
- /* App.new(width:, height:, caption:) — keyword arguments, matching the shape a
114
- * subclass's own initialize will forward to via super. */
114
+ /* App.new(width:, height:, caption:, media_root:, fullscreen:) — keyword
115
+ * arguments, matching the shape a subclass's own initialize will forward to via
116
+ * super. The first three are required; the last two have defaults. */
115
117
  static VALUE app_initialize(int argc, VALUE *argv, VALUE self) {
116
118
  VALUE opts = Qnil;
117
119
  rb_scan_args(argc, argv, "0:", &opts); /* no positional args, keywords only */
@@ -119,14 +121,19 @@ static VALUE app_initialize(int argc, VALUE *argv, VALUE self) {
119
121
  rb_raise(rb_eArgError, "missing keywords: :width, :height, :caption");
120
122
  }
121
123
 
122
- const ID keys[4] = { id_width, id_height, id_caption, id_media_root };
123
- VALUE values[4];
124
- /* 3 required, 1 optional: raises on a missing or unknown keyword. An absent
124
+ const ID keys[5] = { id_width, id_height, id_caption, id_media_root, id_fullscreen };
125
+ VALUE values[5];
126
+ /* 3 required, 2 optional: raises on a missing or unknown keyword. An absent
125
127
  * optional comes back as Qundef. */
126
- rb_get_kwargs(opts, keys, 3, 1, values);
128
+ rb_get_kwargs(opts, keys, 3, 2, values);
127
129
 
130
+ /* Passed to create rather than set afterwards, so a game that starts
131
+ * fullscreen never shows a windowed frame first. RTEST treats both nil and
132
+ * false as off, so `fullscreen: settings[:fullscreen]` works with a key that
133
+ * is missing. */
128
134
  rgame_app *app = rgame_app_create(NUM2INT(values[0]), NUM2INT(values[1]),
129
- StringValueCStr(values[2]));
135
+ StringValueCStr(values[2]),
136
+ values[4] != Qundef && RTEST(values[4]));
130
137
  if (!app) {
131
138
  rb_raise(rb_eRuntimeError, "failed to create rgame app");
132
139
  }
@@ -363,6 +370,18 @@ static VALUE app_set_caption(VALUE self, VALUE title) {
363
370
  return title;
364
371
  }
365
372
 
373
+ static VALUE app_fullscreen_p(VALUE self) {
374
+ return rgame_app_fullscreen(rgame_app_unwrap(self)) ? Qtrue : Qfalse;
375
+ }
376
+
377
+ /* Returns what it was given, as an assignment must in Ruby — so
378
+ * `app.fullscreen = settings.fullscreen?` reads back the value that was set,
379
+ * not the window's state. Read #fullscreen? for that. */
380
+ static VALUE app_set_fullscreen(VALUE self, VALUE on) {
381
+ rgame_app_set_fullscreen(rgame_app_unwrap(self), RTEST(on));
382
+ return on;
383
+ }
384
+
366
385
  /*
367
386
  * Raw input queries. These take the numeric device and button/axis ids from
368
387
  * RGame::Core::Input rather than symbolic action names — turning `:fire` into
@@ -482,6 +501,7 @@ void Init_core_ext(void) {
482
501
  id_height = rb_intern("height");
483
502
  id_caption = rb_intern("caption");
484
503
  id_media_root = rb_intern("media_root");
504
+ id_fullscreen = rb_intern("fullscreen");
485
505
 
486
506
  /*
487
507
  * rb_define_module is idempotent — it returns the existing RGame if some
@@ -501,6 +521,8 @@ void Init_core_ext(void) {
501
521
  rb_define_method(cApp, "height", app_height, 0);
502
522
  rb_define_method(cApp, "caption", app_caption, 0);
503
523
  rb_define_method(cApp, "caption=", app_set_caption, 1);
524
+ rb_define_method(cApp, "fullscreen?", app_fullscreen_p, 0);
525
+ rb_define_method(cApp, "fullscreen=", app_set_fullscreen, 1);
504
526
  rb_define_method(cApp, "ticks_ms", app_ticks_ms, 0);
505
527
  rb_define_method(cApp, "fps", app_fps, 0);
506
528
  rb_define_method(cApp, "input_down?", app_input_down_p, 2);
@@ -526,4 +548,5 @@ void Init_core_ext(void) {
526
548
  rgame_init_recording(mCore);
527
549
  rgame_init_font(mCore);
528
550
  rgame_init_audio(mCore);
551
+ rgame_init_locale(mCore);
529
552
  }
@@ -21,6 +21,9 @@ void rgame_init_renderer(VALUE mCore);
21
21
  void rgame_init_recording(VALUE mCore);
22
22
  void rgame_init_font(VALUE mCore);
23
23
 
24
+ /* Not a class: RGame::Core.preferred_locales, a module function. */
25
+ void rgame_init_locale(VALUE mCore);
26
+
24
27
  /* Defines Audio, Sample and Song together — see audio_ext.c for why those three
25
28
  * share a file when everything else here does not. */
26
29
  void rgame_init_audio(VALUE mCore);