rgame 0.1.0 → 0.3.0

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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +228 -0
  3. data/README.md +142 -243
  4. data/docs/api/README.md +141 -90
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -166
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1045 -254
  10. data/docs/api/drawing.md +187 -119
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +165 -45
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +344 -85
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +503 -123
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +197 -64
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +400 -147
  23. data/docs/api/ui.md +927 -0
  24. data/docs/api/values.md +234 -19
  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/README.md +6 -5
  92. data/ext/rgame_core/app/app.c +215 -11
  93. data/ext/rgame_core/app/locale.c +67 -0
  94. data/ext/rgame_core/app/locale.h +28 -0
  95. data/ext/rgame_core/audio/audio.c +113 -2
  96. data/ext/rgame_core/example.rb +18 -56
  97. data/ext/rgame_core/extconf.rb +16 -113
  98. data/ext/rgame_core/graphics/canvas.c +45 -4
  99. data/ext/rgame_core/graphics/canvas.h +65 -10
  100. data/ext/rgame_core/graphics/clip.c +22 -13
  101. data/ext/rgame_core/include/rgame/core.h +151 -5
  102. data/ext/rgame_core/input/gamepad.c +57 -3
  103. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  104. data/ext/rgame_core/ruby/core_ext.c +46 -7
  105. data/ext/rgame_core/ruby/core_ext.h +3 -0
  106. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  107. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  108. data/ext/rgame_core/ruby/renderer_ext.c +42 -19
  109. data/ext/rgame_util/color_ext.c +12 -3
  110. data/ext/rgame_util/extconf.rb +2 -20
  111. data/ext/rgame_util/route_search.c +305 -0
  112. data/ext/rgame_util/route_search.h +86 -0
  113. data/ext/rgame_util/route_search_ext.c +150 -0
  114. data/ext/rgame_util/solid_grid.c +58 -0
  115. data/ext/rgame_util/solid_grid.h +49 -0
  116. data/ext/rgame_util/solid_grid_ext.c +161 -0
  117. data/ext/rgame_util/tile_sweep.c +164 -0
  118. data/ext/rgame_util/tile_sweep.h +62 -0
  119. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  120. data/ext/rgame_util/util_ext.c +3 -0
  121. data/ext/rgame_util/util_ext.h +15 -0
  122. data/lib/rgame/boot.rb +0 -10
  123. data/lib/rgame/cli/new_project.rb +139 -0
  124. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  125. data/lib/rgame/cli/templates/README.md.tt +93 -0
  126. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  127. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  128. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  129. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  130. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  131. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  132. data/lib/rgame/cli/templates/rspec.tt +2 -0
  133. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  134. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  135. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  136. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  137. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  138. data/lib/rgame/cli.rb +66 -0
  139. data/lib/rgame/core/app.rb +6 -42
  140. data/lib/rgame/core/asset_manager.rb +13 -31
  141. data/lib/rgame/core/audio.rb +37 -16
  142. data/lib/rgame/core/font.rb +0 -3
  143. data/lib/rgame/core/input.rb +35 -41
  144. data/lib/rgame/core/locale.rb +22 -0
  145. data/lib/rgame/core/nine_slice.rb +0 -21
  146. data/lib/rgame/core/recording.rb +3 -1
  147. data/lib/rgame/core/renderer.rb +75 -84
  148. data/lib/rgame/core/sprite_sheet.rb +0 -3
  149. data/lib/rgame/core/tile_map_renderer.rb +77 -65
  150. data/lib/rgame/core/ui_atlas.rb +28 -13
  151. data/lib/rgame/core.rb +1 -8
  152. data/lib/rgame/engine/actor_blockers.rb +131 -0
  153. data/lib/rgame/engine/animation_set.rb +1 -0
  154. data/lib/rgame/engine/audio_director.rb +36 -6
  155. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  156. data/lib/rgame/engine/camera.rb +55 -10
  157. data/lib/rgame/engine/circle_collider.rb +4 -2
  158. data/lib/rgame/engine/collision_box.rb +26 -1
  159. data/lib/rgame/engine/collision_system.rb +110 -22
  160. data/lib/rgame/engine/component.rb +35 -1
  161. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  162. data/lib/rgame/engine/components/animated_sprite.rb +31 -23
  163. data/lib/rgame/engine/components/box_collider.rb +99 -0
  164. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  165. data/lib/rgame/engine/components/character_body.rb +21 -41
  166. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  167. data/lib/rgame/engine/components/collision_world.rb +159 -31
  168. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  169. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  170. data/lib/rgame/engine/components/hop.rb +76 -0
  171. data/lib/rgame/engine/components/identity.rb +73 -0
  172. data/lib/rgame/engine/components/mover.rb +285 -0
  173. data/lib/rgame/engine/components/navigator.rb +145 -0
  174. data/lib/rgame/engine/components/path_follow.rb +123 -31
  175. data/lib/rgame/engine/components/player_controller.rb +5 -2
  176. data/lib/rgame/engine/components/pool.rb +1 -1
  177. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  178. data/lib/rgame/engine/components/sprite.rb +22 -6
  179. data/lib/rgame/engine/components/targeting.rb +9 -11
  180. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  181. data/lib/rgame/engine/components/tile_world.rb +70 -29
  182. data/lib/rgame/engine/components/timer.rb +1 -1
  183. data/lib/rgame/engine/components/velocity.rb +23 -7
  184. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  185. data/lib/rgame/engine/components/world.rb +133 -0
  186. data/lib/rgame/engine/contact_set.rb +74 -0
  187. data/lib/rgame/engine/culling.rb +45 -0
  188. data/lib/rgame/engine/debug_overlay.rb +23 -18
  189. data/lib/rgame/engine/i18n/plural.rb +45 -0
  190. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  191. data/lib/rgame/engine/i18n/template.rb +59 -0
  192. data/lib/rgame/engine/i18n.rb +276 -51
  193. data/lib/rgame/engine/input/action_mapper.rb +76 -22
  194. data/lib/rgame/engine/input/actions.rb +63 -12
  195. data/lib/rgame/engine/input/input_map.rb +196 -0
  196. data/lib/rgame/engine/layout.rb +82 -0
  197. data/lib/rgame/engine/nav_grid.rb +87 -0
  198. data/lib/rgame/engine/node2d.rb +364 -80
  199. data/lib/rgame/engine/path.rb +4 -6
  200. data/lib/rgame/engine/player.rb +69 -0
  201. data/lib/rgame/engine/player_layer.rb +70 -0
  202. data/lib/rgame/engine/players.rb +205 -0
  203. data/lib/rgame/engine/presentation.rb +171 -0
  204. data/lib/rgame/engine/scene/scene_stack.rb +29 -7
  205. data/lib/rgame/engine/sealed_privates.rb +54 -0
  206. data/lib/rgame/engine/spatial_hash.rb +53 -8
  207. data/lib/rgame/engine/text.rb +194 -0
  208. data/lib/rgame/engine/tile_blockers.rb +63 -0
  209. data/lib/rgame/engine/tile_map.rb +2 -3
  210. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  211. data/lib/rgame/engine/tileset.rb +2 -4
  212. data/lib/rgame/engine/timer.rb +2 -2
  213. data/lib/rgame/engine/ui/button.rb +248 -0
  214. data/lib/rgame/engine/ui/column.rb +20 -0
  215. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  216. data/lib/rgame/engine/ui/menu.rb +290 -0
  217. data/lib/rgame/engine/ui/navigation.rb +57 -0
  218. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  219. data/lib/rgame/engine/ui/option_button.rb +163 -0
  220. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  221. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  222. data/lib/rgame/engine/ui/pointing.rb +146 -0
  223. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  224. data/lib/rgame/engine/ui/ring.rb +55 -0
  225. data/lib/rgame/engine/ui/row.rb +21 -0
  226. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  227. data/lib/rgame/engine/ui/stack.rb +58 -0
  228. data/lib/rgame/engine/ui/stepping.rb +93 -0
  229. data/lib/rgame/engine/ui/text_button.rb +59 -0
  230. data/lib/rgame/engine/view.rb +76 -0
  231. data/lib/rgame/engine/viewports.rb +171 -0
  232. data/lib/rgame/engine/world_view.rb +71 -0
  233. data/lib/rgame/engine.rb +43 -25
  234. data/lib/rgame/game.rb +164 -22
  235. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  236. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  237. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  238. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  239. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  240. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  241. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  242. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  243. data/lib/rgame/rubocop/default.yml +39 -0
  244. data/lib/rgame/rubocop/plugin.rb +45 -0
  245. data/lib/rgame/rubocop.rb +11 -0
  246. data/lib/rgame/util/color.rb +20 -24
  247. data/lib/rgame/util/controls.rb +106 -44
  248. data/lib/rgame/util/route_search.rb +27 -0
  249. data/lib/rgame/util/save_file.rb +107 -0
  250. data/lib/rgame/util/solid_grid.rb +37 -0
  251. data/lib/rgame/util/tensor.rb +0 -9
  252. data/lib/rgame/util/tile_sweep.rb +36 -0
  253. data/lib/rgame/util/z.rb +123 -0
  254. data/lib/rgame/util.rb +5 -3
  255. data/lib/rgame/version.rb +1 -1
  256. data/lib/rgame.rb +0 -15
  257. metadata +176 -20
  258. data/lib/rgame/engine/actor.rb +0 -53
  259. data/lib/rgame/engine/body.rb +0 -49
  260. data/lib/rgame/engine/cached_label.rb +0 -33
  261. data/lib/rgame/engine/camera_view.rb +0 -28
  262. data/lib/rgame/engine/input/player_controller.rb +0 -14
  263. data/lib/rgame/engine/matrix.rb +0 -32
  264. data/lib/rgame/engine/resettable.rb +0 -67
  265. data/lib/rgame/engine/tile_collision.rb +0 -78
@@ -95,7 +95,11 @@ struct rgame_app {
95
95
  int drawing;
96
96
  };
97
97
 
98
- rgame_app *rgame_app_create(int width, int height, const char *title) {
98
+ /* Desktop fullscreen, never a mode change — see the header for why. Named once
99
+ * so the creation flags and the runtime toggle cannot disagree. */
100
+ #define RGAME_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
101
+
102
+ rgame_app *rgame_app_create(int width, int height, const char *title, int fullscreen) {
99
103
  if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) {
100
104
  SDL_Log("SDL_Init failed: %s", SDL_GetError());
101
105
  return NULL;
@@ -114,7 +118,8 @@ rgame_app *rgame_app_create(int width, int height, const char *title) {
114
118
  title,
115
119
  SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
116
120
  width, height,
117
- SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
121
+ SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN |
122
+ (fullscreen ? RGAME_FULLSCREEN_FLAG : 0u));
118
123
  if (!app->window) {
119
124
  SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
120
125
  free(app);
@@ -227,14 +232,168 @@ void rgame_app_gl_restore(const rgame_gl_context_save *saved) {
227
232
  * the compiler checks — it must not include SDL. These assertions close that
228
233
  * gap at compile time, so the two can never drift apart silently.
229
234
  */
230
- _Static_assert(RGAME_KEY_RETURN == SDL_SCANCODE_RETURN, "key id must match SDL scancode");
231
- _Static_assert(RGAME_KEY_ESCAPE == SDL_SCANCODE_ESCAPE, "key id must match SDL scancode");
232
- _Static_assert(RGAME_KEY_SPACE == SDL_SCANCODE_SPACE, "key id must match SDL scancode");
233
- _Static_assert(RGAME_KEY_F1 == SDL_SCANCODE_F1, "key id must match SDL scancode");
234
- _Static_assert(RGAME_KEY_RIGHT == SDL_SCANCODE_RIGHT, "key id must match SDL scancode");
235
- _Static_assert(RGAME_KEY_LEFT == SDL_SCANCODE_LEFT, "key id must match SDL scancode");
236
- _Static_assert(RGAME_KEY_DOWN == SDL_SCANCODE_DOWN, "key id must match SDL scancode");
237
- _Static_assert(RGAME_KEY_UP == SDL_SCANCODE_UP, "key id must match SDL scancode");
235
+ _Static_assert(RGAME_KEY_A == SDL_SCANCODE_A,
236
+ "key id must match SDL scancode");
237
+ _Static_assert(RGAME_KEY_B == SDL_SCANCODE_B,
238
+ "key id must match SDL scancode");
239
+ _Static_assert(RGAME_KEY_C == SDL_SCANCODE_C,
240
+ "key id must match SDL scancode");
241
+ _Static_assert(RGAME_KEY_D == SDL_SCANCODE_D,
242
+ "key id must match SDL scancode");
243
+ _Static_assert(RGAME_KEY_E == SDL_SCANCODE_E,
244
+ "key id must match SDL scancode");
245
+ _Static_assert(RGAME_KEY_F == SDL_SCANCODE_F,
246
+ "key id must match SDL scancode");
247
+ _Static_assert(RGAME_KEY_G == SDL_SCANCODE_G,
248
+ "key id must match SDL scancode");
249
+ _Static_assert(RGAME_KEY_H == SDL_SCANCODE_H,
250
+ "key id must match SDL scancode");
251
+ _Static_assert(RGAME_KEY_I == SDL_SCANCODE_I,
252
+ "key id must match SDL scancode");
253
+ _Static_assert(RGAME_KEY_J == SDL_SCANCODE_J,
254
+ "key id must match SDL scancode");
255
+ _Static_assert(RGAME_KEY_K == SDL_SCANCODE_K,
256
+ "key id must match SDL scancode");
257
+ _Static_assert(RGAME_KEY_L == SDL_SCANCODE_L,
258
+ "key id must match SDL scancode");
259
+ _Static_assert(RGAME_KEY_M == SDL_SCANCODE_M,
260
+ "key id must match SDL scancode");
261
+ _Static_assert(RGAME_KEY_N == SDL_SCANCODE_N,
262
+ "key id must match SDL scancode");
263
+ _Static_assert(RGAME_KEY_O == SDL_SCANCODE_O,
264
+ "key id must match SDL scancode");
265
+ _Static_assert(RGAME_KEY_P == SDL_SCANCODE_P,
266
+ "key id must match SDL scancode");
267
+ _Static_assert(RGAME_KEY_Q == SDL_SCANCODE_Q,
268
+ "key id must match SDL scancode");
269
+ _Static_assert(RGAME_KEY_R == SDL_SCANCODE_R,
270
+ "key id must match SDL scancode");
271
+ _Static_assert(RGAME_KEY_S == SDL_SCANCODE_S,
272
+ "key id must match SDL scancode");
273
+ _Static_assert(RGAME_KEY_T == SDL_SCANCODE_T,
274
+ "key id must match SDL scancode");
275
+ _Static_assert(RGAME_KEY_U == SDL_SCANCODE_U,
276
+ "key id must match SDL scancode");
277
+ _Static_assert(RGAME_KEY_V == SDL_SCANCODE_V,
278
+ "key id must match SDL scancode");
279
+ _Static_assert(RGAME_KEY_W == SDL_SCANCODE_W,
280
+ "key id must match SDL scancode");
281
+ _Static_assert(RGAME_KEY_X == SDL_SCANCODE_X,
282
+ "key id must match SDL scancode");
283
+ _Static_assert(RGAME_KEY_Y == SDL_SCANCODE_Y,
284
+ "key id must match SDL scancode");
285
+ _Static_assert(RGAME_KEY_Z == SDL_SCANCODE_Z,
286
+ "key id must match SDL scancode");
287
+ _Static_assert(RGAME_KEY_1 == SDL_SCANCODE_1,
288
+ "key id must match SDL scancode");
289
+ _Static_assert(RGAME_KEY_2 == SDL_SCANCODE_2,
290
+ "key id must match SDL scancode");
291
+ _Static_assert(RGAME_KEY_3 == SDL_SCANCODE_3,
292
+ "key id must match SDL scancode");
293
+ _Static_assert(RGAME_KEY_4 == SDL_SCANCODE_4,
294
+ "key id must match SDL scancode");
295
+ _Static_assert(RGAME_KEY_5 == SDL_SCANCODE_5,
296
+ "key id must match SDL scancode");
297
+ _Static_assert(RGAME_KEY_6 == SDL_SCANCODE_6,
298
+ "key id must match SDL scancode");
299
+ _Static_assert(RGAME_KEY_7 == SDL_SCANCODE_7,
300
+ "key id must match SDL scancode");
301
+ _Static_assert(RGAME_KEY_8 == SDL_SCANCODE_8,
302
+ "key id must match SDL scancode");
303
+ _Static_assert(RGAME_KEY_9 == SDL_SCANCODE_9,
304
+ "key id must match SDL scancode");
305
+ _Static_assert(RGAME_KEY_0 == SDL_SCANCODE_0,
306
+ "key id must match SDL scancode");
307
+ _Static_assert(RGAME_KEY_RETURN == SDL_SCANCODE_RETURN,
308
+ "key id must match SDL scancode");
309
+ _Static_assert(RGAME_KEY_ESCAPE == SDL_SCANCODE_ESCAPE,
310
+ "key id must match SDL scancode");
311
+ _Static_assert(RGAME_KEY_BACKSPACE == SDL_SCANCODE_BACKSPACE,
312
+ "key id must match SDL scancode");
313
+ _Static_assert(RGAME_KEY_TAB == SDL_SCANCODE_TAB,
314
+ "key id must match SDL scancode");
315
+ _Static_assert(RGAME_KEY_SPACE == SDL_SCANCODE_SPACE,
316
+ "key id must match SDL scancode");
317
+ _Static_assert(RGAME_KEY_MINUS == SDL_SCANCODE_MINUS,
318
+ "key id must match SDL scancode");
319
+ _Static_assert(RGAME_KEY_EQUALS == SDL_SCANCODE_EQUALS,
320
+ "key id must match SDL scancode");
321
+ _Static_assert(RGAME_KEY_LEFTBRACKET == SDL_SCANCODE_LEFTBRACKET,
322
+ "key id must match SDL scancode");
323
+ _Static_assert(RGAME_KEY_RIGHTBRACKET == SDL_SCANCODE_RIGHTBRACKET,
324
+ "key id must match SDL scancode");
325
+ _Static_assert(RGAME_KEY_BACKSLASH == SDL_SCANCODE_BACKSLASH,
326
+ "key id must match SDL scancode");
327
+ _Static_assert(RGAME_KEY_SEMICOLON == SDL_SCANCODE_SEMICOLON,
328
+ "key id must match SDL scancode");
329
+ _Static_assert(RGAME_KEY_APOSTROPHE == SDL_SCANCODE_APOSTROPHE,
330
+ "key id must match SDL scancode");
331
+ _Static_assert(RGAME_KEY_GRAVE == SDL_SCANCODE_GRAVE,
332
+ "key id must match SDL scancode");
333
+ _Static_assert(RGAME_KEY_COMMA == SDL_SCANCODE_COMMA,
334
+ "key id must match SDL scancode");
335
+ _Static_assert(RGAME_KEY_PERIOD == SDL_SCANCODE_PERIOD,
336
+ "key id must match SDL scancode");
337
+ _Static_assert(RGAME_KEY_SLASH == SDL_SCANCODE_SLASH,
338
+ "key id must match SDL scancode");
339
+ _Static_assert(RGAME_KEY_CAPSLOCK == SDL_SCANCODE_CAPSLOCK,
340
+ "key id must match SDL scancode");
341
+ _Static_assert(RGAME_KEY_F1 == SDL_SCANCODE_F1,
342
+ "key id must match SDL scancode");
343
+ _Static_assert(RGAME_KEY_F2 == SDL_SCANCODE_F2,
344
+ "key id must match SDL scancode");
345
+ _Static_assert(RGAME_KEY_F3 == SDL_SCANCODE_F3,
346
+ "key id must match SDL scancode");
347
+ _Static_assert(RGAME_KEY_F4 == SDL_SCANCODE_F4,
348
+ "key id must match SDL scancode");
349
+ _Static_assert(RGAME_KEY_F5 == SDL_SCANCODE_F5,
350
+ "key id must match SDL scancode");
351
+ _Static_assert(RGAME_KEY_F6 == SDL_SCANCODE_F6,
352
+ "key id must match SDL scancode");
353
+ _Static_assert(RGAME_KEY_F7 == SDL_SCANCODE_F7,
354
+ "key id must match SDL scancode");
355
+ _Static_assert(RGAME_KEY_F8 == SDL_SCANCODE_F8,
356
+ "key id must match SDL scancode");
357
+ _Static_assert(RGAME_KEY_F9 == SDL_SCANCODE_F9,
358
+ "key id must match SDL scancode");
359
+ _Static_assert(RGAME_KEY_F10 == SDL_SCANCODE_F10,
360
+ "key id must match SDL scancode");
361
+ _Static_assert(RGAME_KEY_F11 == SDL_SCANCODE_F11,
362
+ "key id must match SDL scancode");
363
+ _Static_assert(RGAME_KEY_F12 == SDL_SCANCODE_F12,
364
+ "key id must match SDL scancode");
365
+ _Static_assert(RGAME_KEY_INSERT == SDL_SCANCODE_INSERT,
366
+ "key id must match SDL scancode");
367
+ _Static_assert(RGAME_KEY_HOME == SDL_SCANCODE_HOME,
368
+ "key id must match SDL scancode");
369
+ _Static_assert(RGAME_KEY_PAGEUP == SDL_SCANCODE_PAGEUP,
370
+ "key id must match SDL scancode");
371
+ _Static_assert(RGAME_KEY_DELETE == SDL_SCANCODE_DELETE,
372
+ "key id must match SDL scancode");
373
+ _Static_assert(RGAME_KEY_END == SDL_SCANCODE_END,
374
+ "key id must match SDL scancode");
375
+ _Static_assert(RGAME_KEY_PAGEDOWN == SDL_SCANCODE_PAGEDOWN,
376
+ "key id must match SDL scancode");
377
+ _Static_assert(RGAME_KEY_RIGHT == SDL_SCANCODE_RIGHT,
378
+ "key id must match SDL scancode");
379
+ _Static_assert(RGAME_KEY_LEFT == SDL_SCANCODE_LEFT,
380
+ "key id must match SDL scancode");
381
+ _Static_assert(RGAME_KEY_DOWN == SDL_SCANCODE_DOWN,
382
+ "key id must match SDL scancode");
383
+ _Static_assert(RGAME_KEY_UP == SDL_SCANCODE_UP,
384
+ "key id must match SDL scancode");
385
+ _Static_assert(RGAME_KEY_LCTRL == SDL_SCANCODE_LCTRL,
386
+ "key id must match SDL scancode");
387
+ _Static_assert(RGAME_KEY_LSHIFT == SDL_SCANCODE_LSHIFT,
388
+ "key id must match SDL scancode");
389
+ _Static_assert(RGAME_KEY_LALT == SDL_SCANCODE_LALT,
390
+ "key id must match SDL scancode");
391
+ _Static_assert(RGAME_KEY_RCTRL == SDL_SCANCODE_RCTRL,
392
+ "key id must match SDL scancode");
393
+ _Static_assert(RGAME_KEY_RSHIFT == SDL_SCANCODE_RSHIFT,
394
+ "key id must match SDL scancode");
395
+ _Static_assert(RGAME_KEY_RALT == SDL_SCANCODE_RALT,
396
+ "key id must match SDL scancode");
238
397
 
239
398
  /* The snapshot array must be exactly as long as SDL's keyboard state array,
240
399
  * since filling it is a straight copy. */
@@ -313,7 +472,13 @@ static void rgame_app_poll_events(rgame_app *app, const rgame_app_callbacks *cb)
313
472
  break;
314
473
  }
315
474
  case SDL_WINDOWEVENT:
316
- if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
475
+ /* SIZE_CHANGED rather than RESIZED, and the difference matters:
476
+ * SDL sends RESIZED only when something *outside* the program
477
+ * resized the window, and SIZE_CHANGED for that case as well as for
478
+ * a size this program asked for. Toggling fullscreen is the second
479
+ * kind, so a listener on RESIZED alone never hears about it and the
480
+ * game carries on laying out for the old size. */
481
+ if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
317
482
  /* The viewport is set from the current size every frame, in the
318
483
  * backend's begin_frame, so there is nothing to update here. */
319
484
  if (cb->resize) {
@@ -400,6 +565,9 @@ void rgame_app_run(rgame_app *app, const rgame_app_callbacks *cb) {
400
565
 
401
566
  rgame_draw_backend backend = rgame_gl_backend_table(&app->gl);
402
567
  rgame_canvas_submit(&app->canvas, &backend);
568
+ if (cb->frame_end) {
569
+ cb->frame_end(cb->userdata);
570
+ }
403
571
  SDL_GL_SwapWindow(app->window);
404
572
  }
405
573
  }
@@ -599,6 +767,23 @@ int rgame_app_push_clip(rgame_app *app, int x, int y, int width, int height) {
599
767
  return 1;
600
768
  }
601
769
 
770
+ void rgame_app_push_layer(rgame_app *app, double base) {
771
+ rgame_canvas *canvas = drawing_canvas(app);
772
+ if (canvas) {
773
+ rgame_canvas_push_layer(canvas, base);
774
+ }
775
+ }
776
+
777
+ double rgame_app_layer(rgame_app *app) {
778
+ rgame_canvas *canvas = drawing_canvas(app);
779
+ return canvas ? rgame_canvas_layer(canvas) : 0.0;
780
+ }
781
+
782
+ unsigned int rgame_app_next_layer_slot(rgame_app *app, int band) {
783
+ rgame_canvas *canvas = drawing_canvas(app);
784
+ return canvas ? rgame_canvas_next_slot(canvas, band) : 0;
785
+ }
786
+
602
787
  void rgame_app_pop(rgame_app *app) {
603
788
  rgame_canvas *canvas = drawing_canvas(app);
604
789
  if (canvas) {
@@ -691,6 +876,25 @@ void rgame_app_set_title(rgame_app *app, const char *title) {
691
876
  SDL_SetWindowTitle(app->window, title);
692
877
  }
693
878
 
879
+ void rgame_app_set_fullscreen(rgame_app *app, int fullscreen) {
880
+ if (!app) {
881
+ return;
882
+ }
883
+ SDL_SetWindowFullscreen(app->window, fullscreen ? RGAME_FULLSCREEN_FLAG : 0u);
884
+ }
885
+
886
+ int rgame_app_fullscreen(const rgame_app *app) {
887
+ if (!app) {
888
+ return 0;
889
+ }
890
+ /* Tested against the whole mask rather than for any bit in it:
891
+ * SDL_WINDOW_FULLSCREEN_DESKTOP is SDL_WINDOW_FULLSCREEN plus one more,
892
+ * so `flags & FLAG` is also true of an exclusive-fullscreen window — which
893
+ * this engine never asks for, but a caller could have set behind our back. */
894
+ Uint32 flags = SDL_GetWindowFlags(app->window);
895
+ return (flags & RGAME_FULLSCREEN_FLAG) == RGAME_FULLSCREEN_FLAG;
896
+ }
897
+
694
898
  int rgame_app_input_down(const rgame_app *app, int device, int button_id) {
695
899
  return rgame_input_state_down(&app->input, device, button_id);
696
900
  }
@@ -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 */
@@ -44,6 +44,20 @@
44
44
  struct rgame_audio {
45
45
  ma_resource_manager resources;
46
46
  ma_engine engine;
47
+ /*
48
+ * Set only on Windows, only when construction fell back to `noDevice`
49
+ * because there was nothing to open — see create_audio. Exists so
50
+ * rgame_audio_backend can still answer "Null" for this engine, matching
51
+ * what every other no-hardware case on every platform already reports,
52
+ * even though this engine has no `ma_device` at all for the normal
53
+ * lookup to find.
54
+ */
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;
47
61
  };
48
62
 
49
63
  struct rgame_sample {
@@ -131,6 +145,43 @@ static rgame_audio *create_audio(int offline, unsigned int sample_rate, char *er
131
145
  * no sound card — a build server, a container — gets a working engine that
132
146
  * plays silence rather than an error a game has to handle. Verified: with
133
147
  * ALSA and PulseAudio unavailable, the chosen backend is "Null".
148
+ *
149
+ * That fallback is *inside* the real backend's own device-open attempt —
150
+ * it only helps if opening fails cleanly. Verified on Linux, which is the
151
+ * one platform where it demonstrably does, and where an earlier attempt to
152
+ * generalise the workaround below broke a leg that had been working.
153
+ *
154
+ * On Windows, a machine with zero playback devices at all (every GitHub
155
+ * Actions Windows runner, among others: confirmed via
156
+ * `Get-CimInstance Win32_SoundDevice`) crashes instead of failing inside
157
+ * WASAPI's default-device lookup, so the fallback above never gets a
158
+ * chance to run.
159
+ *
160
+ * Windows alone gets an extra step first: enumerate devices (read-only,
161
+ * does not attempt to open anything, so it does not hit whatever WASAPI's
162
+ * own open path gets wrong with no endpoint to find) and, when there are
163
+ * none, fall back to `noDevice` — the same mode the offline constructor
164
+ * below uses — rather than the auto-detect list's own real-backend open
165
+ * attempt.
166
+ *
167
+ * macOS looks like the same bug and is not, which is worth stating because
168
+ * extending this branch to it does nothing. A device-less Mac still
169
+ * enumerates a playback device, so a zero-device test never fires there;
170
+ * its equivalent crash is CoreAudio being initialised in a **forked
171
+ * child**, which macOS forbids outright. That belongs to the test harness
172
+ * rather than here — see test/test_main.c.
173
+ *
174
+ * An *explicit* null-backend device was tried first and rejected: it still
175
+ * spins up a real background device thread, and that thread crashed inside
176
+ * a Ruby process in a way the plain-C Check build of the same code never
177
+ * did. `noDevice` sidesteps the question entirely: with no device and no
178
+ * background thread, there is nothing left that could crash the way a
179
+ * thread can.
180
+ *
181
+ * Linux is deliberately excluded rather than merely untested. Generalising
182
+ * this to every platform is what broke its leg once already, and its own
183
+ * fallback is proven — so the rule here is that a platform opts in on the
184
+ * strength of a measured crash, not on the logic looking harmless.
134
185
  */
135
186
  ma_engine_config engine = ma_engine_config_init();
136
187
  engine.pResourceManager = &audio->resources;
@@ -141,6 +192,24 @@ static rgame_audio *create_audio(int offline, unsigned int sample_rate, char *er
141
192
  engine.channels = 2;
142
193
  engine.sampleRate = sample_rate;
143
194
  }
195
+ #ifdef _WIN32
196
+ else {
197
+ ma_context probe;
198
+ if (ma_context_init(NULL, 0, NULL, &probe) == MA_SUCCESS) {
199
+ ma_uint32 playback_count = 0;
200
+ ma_result enum_result =
201
+ ma_context_get_devices(&probe, NULL, &playback_count, NULL, NULL);
202
+ ma_context_uninit(&probe);
203
+
204
+ if (enum_result == MA_SUCCESS && playback_count == 0) {
205
+ engine.noDevice = MA_TRUE;
206
+ engine.channels = 2;
207
+ engine.sampleRate = 44100;
208
+ audio->forced_no_device = 1;
209
+ }
210
+ }
211
+ }
212
+ #endif
144
213
 
145
214
  if (ma_engine_init(&engine, &audio->engine) != MA_SUCCESS) {
146
215
  ma_resource_manager_uninit(&audio->resources);
@@ -149,6 +218,8 @@ static rgame_audio *create_audio(int offline, unsigned int sample_rate, char *er
149
218
  return NULL;
150
219
  }
151
220
 
221
+ /* The caller's own claim. Sounds add theirs as they are loaded. */
222
+ audio->refs = 1;
152
223
  return audio;
153
224
  }
154
225
 
@@ -170,8 +241,28 @@ unsigned int rgame_audio_read(rgame_audio *audio, float *out, unsigned int frame
170
241
  return (unsigned int)read;
171
242
  }
172
243
 
173
- void rgame_audio_destroy(rgame_audio *audio) {
174
- 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) {
175
266
  return;
176
267
  }
177
268
 
@@ -182,6 +273,10 @@ void rgame_audio_destroy(rgame_audio *audio) {
182
273
  free(audio);
183
274
  }
184
275
 
276
+ void rgame_audio_destroy(rgame_audio *audio) {
277
+ audio_release(audio);
278
+ }
279
+
185
280
  void rgame_audio_set_volume(rgame_audio *audio, float volume) {
186
281
  if (audio) {
187
282
  ma_engine_set_volume(&audio->engine, clamp_volume(volume));
@@ -199,6 +294,16 @@ const char *rgame_audio_backend(const rgame_audio *audio) {
199
294
  return "none";
200
295
  }
201
296
 
297
+ /* The forced-noDevice fallback (see create_audio) has no ma_device for
298
+ * the normal lookup below to find, but it is standing in for exactly what
299
+ * the Null backend already means elsewhere — no hardware, silence — so it
300
+ * answers the same way rather than the more literal but less useful
301
+ * "none", which every other caller of this function reserves for "there
302
+ * is no audio object at all". */
303
+ if (audio->forced_no_device) {
304
+ return "Null";
305
+ }
306
+
202
307
  ma_device *device = ma_engine_get_device((ma_engine *)&audio->engine);
203
308
  return device ? ma_get_backend_name(device->pContext->backend) : "none";
204
309
  }
@@ -293,6 +398,7 @@ rgame_sample *rgame_sample_load(rgame_audio *audio, const char *path, char *err,
293
398
  strcpy(sample->path, path);
294
399
 
295
400
  sample->audio = audio;
401
+ audio->refs++;
296
402
  live_sounds++;
297
403
  return sample;
298
404
  }
@@ -320,8 +426,10 @@ void rgame_sample_destroy(rgame_sample *sample) {
320
426
  * same file hold their own, so the last one out frees it. */
321
427
  ma_sound_uninit(&sample->decoded);
322
428
  free(sample->path);
429
+ rgame_audio *audio = sample->audio;
323
430
  free(sample);
324
431
  live_sounds--;
432
+ audio_release(audio);
325
433
  }
326
434
 
327
435
  void rgame_sample_play(rgame_sample *sample) {
@@ -396,6 +504,7 @@ rgame_song *rgame_song_load(rgame_audio *audio, const char *path, char *err, siz
396
504
  }
397
505
 
398
506
  song->audio = audio;
507
+ audio->refs++;
399
508
  live_sounds++;
400
509
  return song;
401
510
  }
@@ -406,8 +515,10 @@ void rgame_song_destroy(rgame_song *song) {
406
515
  }
407
516
 
408
517
  ma_sound_uninit(&song->sound);
518
+ rgame_audio *audio = song->audio;
409
519
  free(song);
410
520
  live_sounds--;
521
+ audio_release(audio);
411
522
  }
412
523
 
413
524
  void rgame_song_play(rgame_song *song, int looping) {