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
@@ -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
 
@@ -88,51 +55,48 @@ class Example < RGame::Core::App
88
55
  @ticks += 1
89
56
  @spin += dt * 45.0
90
57
  speed = 200.0 * dt
91
- @cursor_x -= speed if @input.down?(:left, device: @device)
92
- @cursor_x += speed if @input.down?(:right, device: @device)
93
- @cursor_y -= speed if @input.down?(:up, device: @device)
94
- @cursor_y += speed if @input.down?(:down, device: @device)
95
-
96
- # Analog sticks are additive on top of the dpad; the keyboard reads 0.0.
97
- @cursor_x += @input.axis(:move_x, device: @device) * speed
98
- @cursor_y += @input.axis(:move_y, device: @device) * speed
58
+ @cursor_x -= speed if held?(Controls::KEY_LEFT, Controls::PAD_DPAD_LEFT)
59
+ @cursor_x += speed if held?(Controls::KEY_RIGHT, Controls::PAD_DPAD_RIGHT)
60
+ @cursor_y -= speed if held?(Controls::KEY_UP, Controls::PAD_DPAD_UP)
61
+ @cursor_y += speed if held?(Controls::KEY_DOWN, Controls::PAD_DPAD_DOWN)
62
+
63
+ @cursor_x += @input.axis(Controls::AXIS_LEFT_X, device: @device) * speed
64
+ @cursor_y += @input.axis(Controls::AXIS_LEFT_Y, device: @device) * speed
99
65
  end
100
66
 
67
+ # Core is the raw layer: Input answers "is this scancode down on this device",
68
+ # and naming the key *and* the pad button for one intent is the caller's job.
69
+ # Asking both costs nothing, because a device only answers for its own kind of
70
+ # input — a gamepad reads false for a scancode.
71
+ #
72
+ # A game does not write this. RGame::Engine::InputMap is a table of exactly
73
+ # these pairs, one entry per action, and the scene graph reads named actions
74
+ # instead. This example is here to exercise Core on its own, so it does
75
+ # without that and shows what the layer above is for.
76
+ def held?(key, pad) = @input.down?(key, device: @device) || @input.down?(pad, device: @device)
77
+
101
78
  # One of everything, so a look at the window checks the whole drawing path.
102
79
  # Colours are RGame::Util::Color values rather than arrays: a Color is frozen
103
80
  # and allocates nothing when it is drawn, which is what per-frame code wants.
104
81
  def draw
105
82
  r = @renderer
106
83
 
107
- # Baked on the first frame and replayed after that: forty rectangles cost
108
- # one call per frame instead of forty. A real game bakes its tile layers
109
- # this way. Recording has to happen inside #draw, which is why it is here
110
- # rather than in initialize.
111
84
  @baked ||= r.record do
112
85
  40.times { |i| r.rect(i * 18, 0, 12, 12, color: GREEN, z: 0) }
113
86
  end
114
- # Scrolls with the same value the spinning square turns by, so it is
115
- # visibly the *replay* moving and not a rebake.
116
87
  @baked.draw((@spin % 18) - 18, 560)
117
88
  r.rect(40, 40, 160, 100, color: RED, z: 0)
118
- # Higher z wins wherever they overlap, whatever order the calls came in.
119
89
  r.rect(120, 90, 160, 100, color: TRANSLUCENT_WHITE, z: 1)
120
90
  r.triangle(400, 40, 480, 180, 320, 180, color: GREEN, z: 0)
121
91
  r.circle(620, 110, 70, color: BLUE, z: 0)
122
92
  r.line(40, 240, 760, 240, thickness: 6, color: YELLOW, z: 0)
123
93
 
124
- # The transform stack: a square turning about its own centre.
125
94
  r.rotated(@spin, 400, 380) { r.rect(340, 320, 120, 120, color: GREEN, z: 0) }
126
95
 
127
- # The clip stack: the rectangle is twice the size of what shows.
128
96
  r.clipped(60, 480, 200, 80) { r.rect(60, 440, 400, 160, color: RED, z: 0) }
129
97
 
130
- # The cursor the arrow keys move.
131
98
  r.debug_box(@cursor_x - 8, @cursor_y - 8, 16, 16)
132
99
 
133
- # Text, including accents and punctuation the shipped font has to cover, so
134
- # a look at the window checks kerning and coverage at once. The second line
135
- # is centred using text_width, which is what a UI actually does with it.
136
100
  r.text('rgame — Grüße, œuvre, 5 €', 40, 270, color: YELLOW)
137
101
  label = format('%d fps', fps)
138
102
  r.text(label, 400 - (r.text_width(label) / 2), 270 + r.text_height)
@@ -144,8 +108,6 @@ class Example < RGame::Core::App
144
108
  def button_down(id)
145
109
  case id
146
110
  when Controls::KEY_ESCAPE then close
147
- # Held down, this is the overlap check: each press is another voice rather
148
- # than a restart, so a fast run of them should pile up rather than stutter.
149
111
  when Controls::KEY_SPACE then @sample&.play
150
112
  when Controls::KEY_RETURN then toggle_music
151
113
  end
@@ -1,136 +1,51 @@
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
- # OpenGL + libm, matching the link line in the root Makefile. append_library
72
- # adds `-lGL`/`-lm` in the right spot on the link command.
73
- #
74
- # Checked rather than assumed, unlike in the root Makefile. This script is what
75
- # runs on someone else's machine during `gem install`, and a missing OpenGL
76
- # development package is a routine thing to hit there. Without a check it
77
- # surfaces as a linker error about an undefined `glClear` at the end of a long
78
- # build; with one, the install stops on a sentence naming the package to
79
- # install. Same reasoning as the SDL2 abort above.
80
- #
81
- # The header probed is the one the sources actually include — SDL's own
82
- # <SDL2/SDL_opengl.h>, which resolves to GL/gl.h or OpenGL/gl.h per platform, so
83
- # probing GL/gl.h directly would ask the wrong question off Linux. It needs
84
- # SDL's cflags, which pkg_config folded into $CFLAGS just above.
85
20
  abort 'SDL2 OpenGL header not found (SDL2/SDL_opengl.h). Install libsdl2-dev.' unless have_header('SDL2/SDL_opengl.h')
86
21
 
87
- # `-lGL` is how OpenGL is linked on Linux and the BSDs; macOS wants
88
- # `-framework OpenGL` instead. Only the first is implemented — as in the root
89
- # Makefile, which hardcodes -lGL — so this aborts on a platform it cannot link
90
- # rather than emitting undefined symbols for every gl* call. have_library
91
- # appends -lGL to $libs itself when the probe succeeds.
92
- unless have_library('GL', 'glClear')
93
- abort 'OpenGL library not found (-lGL). Install libgl1-mesa-dev. ' \
94
- '(macOS needs -framework OpenGL, which is not implemented yet.)'
22
+ case RbConfig::CONFIG['host_os']
23
+ when /darwin/
24
+ unless have_framework('OpenGL')
25
+ abort 'OpenGL framework not found. It ships with macOS, so this usually means ' \
26
+ 'the command line tools are missing: xcode-select --install'
27
+ end
28
+ when /mingw|mswin|cygwin/
29
+ unless have_library('opengl32', 'glClear')
30
+ abort 'opengl32 not found. It is part of Windows itself, so this means an ' \
31
+ 'incomplete MSYS2 toolchain: pacman -S mingw-w64-ucrt-x86_64-gcc'
32
+ end
33
+ else
34
+ unless have_library('GL', 'glClear')
35
+ abort 'OpenGL library not found (-lGL). Install the OpenGL development package: ' \
36
+ 'libgl1-mesa-dev on Debian/Ubuntu, mesa-libGL-devel on Fedora.'
37
+ end
95
38
  end
96
39
 
97
40
  $libs = append_library($libs, 'm')
98
41
 
99
- # miniaudio's threading, and the dlopen it uses to find ALSA or PulseAudio at
100
- # runtime — the property that makes audio cost no new system dependency. Both
101
- # live in glibc on Linux/BSD. Windows and macOS need neither, and appending them
102
- # unconditionally would fail the link there, so each is probed first.
103
42
  $libs = append_library($libs, 'pthread') if have_library('pthread')
104
43
  $libs = append_library($libs, 'dl') if have_library('dl')
105
44
 
106
- # Match the project's C standard and warning flags. Note: gnu17, not plain
107
- # c17 — Ruby's headers occasionally lean on GNU extensions, and gnu17 is a
108
- # superset of c17 so core.c (which targets c17) still compiles fine.
109
45
  $CFLAGS << ' -std=gnu17 -Wall -Wextra'
110
46
 
111
- # Emits the Makefile. "rgame/core_ext" -> loaded via
112
- # `require "rgame/core_ext"`, entry point Init_core_ext (the basename)
113
- # in core_ext.c. Namespacing it under rgame/ mirrors rgame/util_ext, keeps
114
- # both extensions off the top of the load path, and — importantly — leaves the
115
- # bare name "rgame" to lib/rgame.rb, which is the pure-Ruby entry point.
116
47
  create_makefile('rgame/core_ext')
117
48
 
118
- # The vendored implementations, compiled with warnings off (see
119
- # vendor/README.md). mkmf has no per-file flag setting, so the rules are
120
- # appended to the Makefile it just wrote. An explicit rule for a specific
121
- # target beats mkmf's generic .c.o suffix rule, so these are what get used for
122
- # those objects and nothing else.
123
- #
124
- # Written from one list rather than one block per library: a second hand-copied
125
- # rule is how the first one drifts. They are explicit rules rather than a `%`
126
- # pattern because mkmf's Makefiles are meant to work with whatever `make` the
127
- # platform has, and pattern rules are a GNU extension.
128
- #
129
- # `-w` comes last on the command line and switches every warning back off,
130
- # which is simpler and more robust than trying to subtract -Wall -Wextra from
131
- # $(CFLAGS) — everything else about how the extension compiles stays identical.
132
- # Each vendored library's implementation TU (<name>_impl.c) and the file it
133
- # instantiates. stb_vorbis is the odd one out: it ships as a .c, not a .h.
134
49
  VENDORED = {
135
50
  'stb_image' => 'stb_image.h',
136
51
  'stb_truetype' => 'stb_truetype.h',
@@ -148,18 +63,6 @@ File.open('Makefile', 'a') do |makefile|
148
63
  MAKE
149
64
  end
150
65
 
151
- # Rebuild everything when any of our headers changes.
152
- #
153
- # mkmf emits `$(OBJS): $(HDRS)` for this, but it builds HDRS by globbing the
154
- # extension's own directory only — with the headers a level down in
155
- # graphics/, text/ and the rest, HDRS comes out empty and editing a header
156
- # rebuilds *nothing*. That failure is silent: the build succeeds and links
157
- # objects compiled against the previous version of the struct.
158
- #
159
- # One coarse dependency rather than a real per-object dependency scan, for the
160
- # same reason the root Makefile treats the vendored sources as one list: this
161
- # is a small project where a full rebuild costs seconds, and a hand-maintained
162
- # dependency graph is a thing to get quietly wrong.
163
66
  headers = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.h") }
164
67
  .map { |path| "$(srcdir)/#{path.delete_prefix("#{$srcdir}/")}" }
165
68
 
@@ -6,15 +6,26 @@
6
6
  enum {
7
7
  RGAME_PUSH_TRANSFORM,
8
8
  RGAME_PUSH_CLIP,
9
+ RGAME_PUSH_LAYER,
9
10
  /* The underlying stack was full. Nothing to undo, but it still occupies a
10
11
  * slot so that the caller's matching pop stays paired with this push. */
11
12
  RGAME_PUSH_NOTHING
12
13
  };
13
14
 
15
+ /* The layer stack and the per-band counters, both back to their frame start. */
16
+ static void reset_layers(rgame_canvas *canvas) {
17
+ canvas->layers[0] = 0.0;
18
+ canvas->layer_depth = 0;
19
+ for (int i = 0; i < RGAME_LAYER_BANDS; i++) {
20
+ canvas->slots[i] = 0;
21
+ }
22
+ }
23
+
14
24
  void rgame_canvas_init(rgame_canvas *canvas) {
15
25
  rgame_draw_queue_init(&canvas->queue);
16
26
  rgame_transform_stack_init(&canvas->transforms);
17
27
  rgame_clip_stack_init(&canvas->clips, 0, 0);
28
+ reset_layers(canvas);
18
29
  canvas->push_depth = 0;
19
30
  canvas->unrecorded_pushes = 0;
20
31
  canvas->width = 0;
@@ -29,6 +40,7 @@ void rgame_canvas_begin_frame(rgame_canvas *canvas, int width, int height) {
29
40
  rgame_draw_queue_reset(&canvas->queue);
30
41
  rgame_transform_stack_init(&canvas->transforms);
31
42
  rgame_clip_stack_init(&canvas->clips, width, height);
43
+ reset_layers(canvas);
32
44
  canvas->push_depth = 0;
33
45
  canvas->unrecorded_pushes = 0;
34
46
  canvas->width = width;
@@ -126,6 +138,26 @@ void rgame_canvas_push_clip(rgame_canvas *canvas, rgame_rect rect) {
126
138
  account_push(canvas, ok, RGAME_PUSH_CLIP);
127
139
  }
128
140
 
141
+ void rgame_canvas_push_layer(rgame_canvas *canvas, double base) {
142
+ int ok = canvas->layer_depth + 1 < RGAME_LAYER_STACK_DEPTH;
143
+ if (ok) {
144
+ /* Replaces rather than accumulates: see canvas.h. */
145
+ canvas->layers[++canvas->layer_depth] = base;
146
+ }
147
+ account_push(canvas, ok, RGAME_PUSH_LAYER);
148
+ }
149
+
150
+ double rgame_canvas_layer(const rgame_canvas *canvas) {
151
+ return canvas->layers[canvas->layer_depth];
152
+ }
153
+
154
+ unsigned int rgame_canvas_next_slot(rgame_canvas *canvas, int band) {
155
+ if (band < 0 || band >= RGAME_LAYER_BANDS) {
156
+ return 0;
157
+ }
158
+ return canvas->slots[band]++;
159
+ }
160
+
129
161
  void rgame_canvas_pop(rgame_canvas *canvas) {
130
162
  /* Unrecorded pushes unwind first: they are the most recent ones, since a
131
163
  * push is only left unrecorded once the record stack is already full. */
@@ -144,11 +176,20 @@ void rgame_canvas_pop(rgame_canvas *canvas) {
144
176
  case RGAME_PUSH_CLIP:
145
177
  rgame_clip_pop(&canvas->clips);
146
178
  break;
179
+ case RGAME_PUSH_LAYER:
180
+ canvas->layer_depth--;
181
+ break;
147
182
  default:
148
183
  break; /* RGAME_PUSH_NOTHING: the push never took effect */
149
184
  }
150
185
  }
151
186
 
187
+ /* Every z that reaches the queue is an offset from the current layer base. One
188
+ * addition, in the one place all four queueing paths pass through. */
189
+ static double layer_z(const rgame_canvas *canvas, double z) {
190
+ return canvas->layers[canvas->layer_depth] + z;
191
+ }
192
+
152
193
  /* Fills one vertex: map the point into screen space, copy through the texture
153
194
  * coordinate, and write the colour as the four bytes GL reads. */
154
195
  static void write_vertex(const rgame_canvas *canvas, rgame_vertex *vertex, float x, float y,
@@ -161,7 +202,7 @@ static void write_vertex(const rgame_canvas *canvas, rgame_vertex *vertex, float
161
202
 
162
203
  void rgame_canvas_triangle(rgame_canvas *canvas, const float *xy6, rgame_color color,
163
204
  double z) {
164
- rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 3, z, 0,
205
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 3, layer_z(canvas, z), 0,
165
206
  rgame_clip_current(&canvas->clips));
166
207
  for (int i = 0; i < 3; i++) {
167
208
  write_vertex(canvas, &out[i], xy6[i * 2], xy6[(i * 2) + 1], 0.0f, 0.0f, color);
@@ -172,7 +213,7 @@ void rgame_canvas_triangle(rgame_canvas *canvas, const float *xy6, rgame_color c
172
213
  static const int RGAME_QUAD_TRIANGLES[6] = { 0, 1, 2, 0, 2, 3 };
173
214
 
174
215
  void rgame_canvas_quad(rgame_canvas *canvas, const float *xy8, rgame_color color, double z) {
175
- rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, z, 0,
216
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, layer_z(canvas, z), 0,
176
217
  rgame_clip_current(&canvas->clips));
177
218
  for (int i = 0; i < 6; i++) {
178
219
  int corner = RGAME_QUAD_TRIANGLES[i];
@@ -183,7 +224,7 @@ void rgame_canvas_quad(rgame_canvas *canvas, const float *xy8, rgame_color color
183
224
 
184
225
  void rgame_canvas_textured_quad(rgame_canvas *canvas, unsigned int texture, const float *xy8,
185
226
  const float *uv8, rgame_color color, double z) {
186
- rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, z, texture,
227
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, 6, layer_z(canvas, z), texture,
187
228
  rgame_clip_current(&canvas->clips));
188
229
  for (int i = 0; i < 6; i++) {
189
230
  int corner = RGAME_QUAD_TRIANGLES[i];
@@ -213,7 +254,7 @@ void rgame_canvas_replay(rgame_canvas *canvas, const rgame_recording *recording,
213
254
  /* One command per baked batch, rather than one per original draw call:
214
255
  * the whole point of a recording is that the per-tile work happened
215
256
  * once, at bake time. */
216
- rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, batch->vertex_count, z,
257
+ rgame_vertex *out = rgame_draw_queue_alloc(&canvas->queue, batch->vertex_count, layer_z(canvas, z),
217
258
  batch->texture,
218
259
  rgame_clip_current(&canvas->clips));
219
260
 
@@ -46,25 +46,67 @@
46
46
  * rotates a clip today.
47
47
  *
48
48
  * ---------------------------------------------------------------------------
49
+ * The layer stack: where a caller's z is measured from
50
+ * ---------------------------------------------------------------------------
51
+ *
52
+ * A `z` handed to a primitive is an *offset*, added to whatever base is on top
53
+ * of the layer stack. The stack starts at 0, so a caller that never pushes one
54
+ * gets exactly the z it passed.
55
+ *
56
+ * What it is for: the scene graph gives every node its own narrow window of z
57
+ * values, so a node's drawing can only ever be ordered against its own other
58
+ * drawing — never against another node's, and never out of the band it is in.
59
+ * Composing that is one addition here, and the alternative is every caller
60
+ * remembering to add the same number to every z it passes.
61
+ *
62
+ * `rgame_canvas_next_slot` is the other half: a small set of counters, one per
63
+ * band, reset at the start of each frame and handing out increasing indices. It
64
+ * is deliberately ignorant of what a band *is* — that vocabulary lives in
65
+ * RGame::Util::Z, a layer up — and does nothing but count, because "which node
66
+ * comes next" is already the order the draw traversal visits nodes in.
67
+ *
68
+ * A push *replaces* rather than adds. Nesting bases would re-introduce the
69
+ * additive relative z this design exists to remove: the point is that a node's
70
+ * window is decided by where the traversal reached it, not by summing what its
71
+ * ancestors happened to pick.
72
+ *
73
+ * ---------------------------------------------------------------------------
49
74
  * One pop for any push
50
75
  * ---------------------------------------------------------------------------
51
76
  *
52
- * `push_translate`, `push_rotate`, `push_scale` and `push_clip` are all undone
53
- * by the same `pop`. The canvas remembers which stack each push went to, so a
54
- * caller never has to — and cannot pop the wrong one. Pushes that could not be
55
- * honoured (a full stack) are still counted, so pop stays balanced and the
56
- * drawing comes out untransformed rather than desynchronised.
77
+ * `push_translate`, `push_rotate`, `push_scale`, `push_clip` and `push_layer`
78
+ * are all undone by the same `pop`. The canvas remembers which stack each push
79
+ * went to, so a caller never has to — and cannot pop the wrong one. Pushes that
80
+ * could not be honoured (a full stack) are still counted, so pop stays balanced
81
+ * and the drawing comes out untransformed rather than desynchronised.
57
82
  */
58
83
 
59
- /* Deep enough that it cannot fill before both underlying stacks have; pushes
60
- * beyond that are counted rather than recorded, so balance always holds. */
61
- #define RGAME_CANVAS_STACK_DEPTH (RGAME_TRANSFORM_STACK_DEPTH + RGAME_CLIP_STACK_DEPTH)
84
+ /* Deep enough for any sane scene graph, like the two stacks it sits beside. */
85
+ #define RGAME_LAYER_STACK_DEPTH 32
86
+
87
+ /* How many independent slot counters a frame has. RGame::Util::Z uses four;
88
+ * the spare room costs four unsigned ints. */
89
+ #define RGAME_LAYER_BANDS 8
90
+
91
+ /* Deep enough that it cannot fill before all three underlying stacks have;
92
+ * pushes beyond that are counted rather than recorded, so balance always
93
+ * holds. */
94
+ #define RGAME_CANVAS_STACK_DEPTH \
95
+ (RGAME_TRANSFORM_STACK_DEPTH + RGAME_CLIP_STACK_DEPTH + RGAME_LAYER_STACK_DEPTH)
62
96
 
63
97
  typedef struct {
64
98
  rgame_transform_stack transforms;
65
99
  rgame_clip_stack clips;
66
100
  rgame_draw_queue queue;
67
101
 
102
+ /* entries[0] is 0 — the base an un-layered draw sits on — so `layer_depth`
103
+ * is the number of pushes outstanding and entries[layer_depth] is current. */
104
+ double layers[RGAME_LAYER_STACK_DEPTH];
105
+ int layer_depth;
106
+
107
+ /* Slots handed out per band this frame. Reset by begin_frame. */
108
+ unsigned int slots[RGAME_LAYER_BANDS];
109
+
68
110
  /* Which stack each outstanding push went to, so pop can undo the right one. */
69
111
  unsigned char pushes[RGAME_CANVAS_STACK_DEPTH];
70
112
  int push_depth;
@@ -79,8 +121,8 @@ void rgame_canvas_init(rgame_canvas *canvas);
79
121
  void rgame_canvas_destroy(rgame_canvas *canvas);
80
122
 
81
123
  /*
82
- * Starts a frame: empties the queue (keeping its buffers), resets both stacks,
83
- * and sets the clip base to the window. Re-stating the size every frame is also
124
+ * Starts a frame: empties the queue (keeping its buffers), resets all three
125
+ * stacks and the slot counters, and sets the clip base to the window. Re-stating the size every frame is also
84
126
  * how a resize takes effect, so there is no separate path to forget.
85
127
  */
86
128
  void rgame_canvas_begin_frame(rgame_canvas *canvas, int width, int height);
@@ -97,6 +139,19 @@ void rgame_canvas_push_scale(rgame_canvas *canvas, float sx, float sy);
97
139
  * transform before narrowing the clip. */
98
140
  void rgame_canvas_push_clip(rgame_canvas *canvas, rgame_rect rect);
99
141
 
142
+ /* Sets the base every subsequent z is measured from, until the matching pop. */
143
+ void rgame_canvas_push_layer(rgame_canvas *canvas, double base);
144
+
145
+ /* The base currently in effect. */
146
+ double rgame_canvas_layer(const rgame_canvas *canvas);
147
+
148
+ /*
149
+ * The next slot index in `band`, counting from 0 each frame. Out-of-range bands
150
+ * answer 0 rather than reading past the array — a caller with a bad band gets
151
+ * everything piled in one slot, which is wrong but bounded.
152
+ */
153
+ unsigned int rgame_canvas_next_slot(rgame_canvas *canvas, int band);
154
+
100
155
  void rgame_canvas_pop(rgame_canvas *canvas);
101
156
 
102
157
  /*
@@ -1,5 +1,7 @@
1
1
  #include "graphics/clip.h"
2
2
 
3
+ #include <stdint.h>
4
+
3
5
  /* The canonical empty rect. Every path that produces "nothing" produces this
4
6
  * exact value, so two empties compare equal and cannot split a batch. */
5
7
  static const rgame_rect RGAME_EMPTY_RECT = { 0, 0, 0, 0 };
@@ -30,20 +32,27 @@ rgame_rect rgame_rect_intersect(rgame_rect a, rgame_rect b) {
30
32
  }
31
33
 
32
34
  /*
33
- * Edges are computed in `long` rather than `int`. A caller can legitimately
34
- * pass a very large width to mean "as wide as possible", and x + w would
35
- * then overflow — which is undefined behaviour, not merely a wrong answer.
36
- * The result always fits back in an int because it is bounded by the
37
- * smaller of the two inputs.
35
+ * Edges are computed in `int64_t` rather than `int`. A caller can
36
+ * legitimately pass a very large width to mean "as wide as possible", and
37
+ * x + w would then overflow — which is undefined behaviour, not merely a
38
+ * wrong answer. The result always fits back in an int because it is
39
+ * bounded by the smaller of the two inputs.
40
+ *
41
+ * `int64_t` rather than `long`: `long` is only 32 bits on Windows (LLP64),
42
+ * the same width as `int` there, so it does not actually buy the extra
43
+ * headroom this needs — found by UBSan under clang64, which is the only
44
+ * configuration that caught it (the intermediate overflow this guards
45
+ * against never occurs in Check's own test values on a 64-bit `long`
46
+ * platform, so plain Linux/macOS runs never exercise the bug either).
38
47
  */
39
- long left = a.x > b.x ? a.x : b.x;
40
- long top = a.y > b.y ? a.y : b.y;
41
- long a_right = (long)a.x + a.w;
42
- long b_right = (long)b.x + b.w;
43
- long a_bottom = (long)a.y + a.h;
44
- long b_bottom = (long)b.y + b.h;
45
- long right = a_right < b_right ? a_right : b_right;
46
- long bottom = a_bottom < b_bottom ? a_bottom : b_bottom;
48
+ int64_t left = a.x > b.x ? a.x : b.x;
49
+ int64_t top = a.y > b.y ? a.y : b.y;
50
+ int64_t a_right = (int64_t)a.x + a.w;
51
+ int64_t b_right = (int64_t)b.x + b.w;
52
+ int64_t a_bottom = (int64_t)a.y + a.h;
53
+ int64_t b_bottom = (int64_t)b.y + b.h;
54
+ int64_t right = a_right < b_right ? a_right : b_right;
55
+ int64_t bottom = a_bottom < b_bottom ? a_bottom : b_bottom;
47
56
 
48
57
  if (right <= left || bottom <= top) {
49
58
  return RGAME_EMPTY_RECT;