rgame 0.1.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 (161) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +406 -0
  4. data/docs/api/README.md +167 -0
  5. data/docs/api/app.md +192 -0
  6. data/docs/api/assets.md +426 -0
  7. data/docs/api/audio.md +208 -0
  8. data/docs/api/components.md +321 -0
  9. data/docs/api/drawing.md +330 -0
  10. data/docs/api/game.md +99 -0
  11. data/docs/api/images.md +118 -0
  12. data/docs/api/input.md +179 -0
  13. data/docs/api/internals.md +110 -0
  14. data/docs/api/scene_graph.md +159 -0
  15. data/docs/api/signals.md +142 -0
  16. data/docs/api/systems.md +98 -0
  17. data/docs/api/text.md +116 -0
  18. data/docs/api/toolbox.md +240 -0
  19. data/docs/api/values.md +101 -0
  20. data/ext/README.md +225 -0
  21. data/ext/rgame_core/app/app.c +721 -0
  22. data/ext/rgame_core/app/app_gl.h +64 -0
  23. data/ext/rgame_core/app/frame_loop.c +42 -0
  24. data/ext/rgame_core/app/frame_loop.h +54 -0
  25. data/ext/rgame_core/audio/audio.c +466 -0
  26. data/ext/rgame_core/audio/audio_internal.h +45 -0
  27. data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
  28. data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
  29. data/ext/rgame_core/example.rb +188 -0
  30. data/ext/rgame_core/extconf.rb +167 -0
  31. data/ext/rgame_core/graphics/backend.c +52 -0
  32. data/ext/rgame_core/graphics/backend.h +64 -0
  33. data/ext/rgame_core/graphics/canvas.c +247 -0
  34. data/ext/rgame_core/graphics/canvas.h +143 -0
  35. data/ext/rgame_core/graphics/clip.c +87 -0
  36. data/ext/rgame_core/graphics/clip.h +89 -0
  37. data/ext/rgame_core/graphics/draw_queue.c +216 -0
  38. data/ext/rgame_core/graphics/draw_queue.h +174 -0
  39. data/ext/rgame_core/graphics/gl_backend.c +122 -0
  40. data/ext/rgame_core/graphics/gl_backend.h +43 -0
  41. data/ext/rgame_core/graphics/image.c +304 -0
  42. data/ext/rgame_core/graphics/image_internal.h +30 -0
  43. data/ext/rgame_core/graphics/primitives.c +189 -0
  44. data/ext/rgame_core/graphics/primitives.h +111 -0
  45. data/ext/rgame_core/graphics/recording.c +119 -0
  46. data/ext/rgame_core/graphics/recording.h +88 -0
  47. data/ext/rgame_core/graphics/texture.c +181 -0
  48. data/ext/rgame_core/graphics/texture.h +165 -0
  49. data/ext/rgame_core/graphics/transform.c +128 -0
  50. data/ext/rgame_core/graphics/transform.h +106 -0
  51. data/ext/rgame_core/include/rgame/core.h +577 -0
  52. data/ext/rgame_core/input/device_slots.c +103 -0
  53. data/ext/rgame_core/input/device_slots.h +93 -0
  54. data/ext/rgame_core/input/gamepad.c +145 -0
  55. data/ext/rgame_core/input/gamepad.h +63 -0
  56. data/ext/rgame_core/input/input.c +109 -0
  57. data/ext/rgame_core/input/input.h +99 -0
  58. data/ext/rgame_core/ruby/audio_ext.c +321 -0
  59. data/ext/rgame_core/ruby/core_ext.c +513 -0
  60. data/ext/rgame_core/ruby/core_ext.h +51 -0
  61. data/ext/rgame_core/ruby/font_ext.c +168 -0
  62. data/ext/rgame_core/ruby/image_ext.c +230 -0
  63. data/ext/rgame_core/ruby/recording_ext.c +186 -0
  64. data/ext/rgame_core/ruby/renderer_ext.c +376 -0
  65. data/ext/rgame_core/text/atlas.c +59 -0
  66. data/ext/rgame_core/text/atlas.h +85 -0
  67. data/ext/rgame_core/text/font.c +281 -0
  68. data/ext/rgame_core/text/font.h +139 -0
  69. data/ext/rgame_core/text/font_atlas.c +385 -0
  70. data/ext/rgame_core/text/font_internal.h +47 -0
  71. data/ext/rgame_core/text/glyph_cache.c +142 -0
  72. data/ext/rgame_core/text/glyph_cache.h +89 -0
  73. data/ext/rgame_core/vendor/README.md +159 -0
  74. data/ext/rgame_core/vendor/miniaudio.h +95864 -0
  75. data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
  76. data/ext/rgame_core/vendor/stb_image.h +7988 -0
  77. data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
  78. data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
  79. data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
  80. data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
  81. data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
  82. data/ext/rgame_util/color.c +19 -0
  83. data/ext/rgame_util/color.h +60 -0
  84. data/ext/rgame_util/color_ext.c +156 -0
  85. data/ext/rgame_util/extconf.rb +27 -0
  86. data/ext/rgame_util/tensor.c +186 -0
  87. data/ext/rgame_util/util_ext.c +27 -0
  88. data/ext/rgame_util/util_ext.h +16 -0
  89. data/lib/rgame/boot.rb +13 -0
  90. data/lib/rgame/core/app.rb +82 -0
  91. data/lib/rgame/core/asset_manager.rb +224 -0
  92. data/lib/rgame/core/audio.rb +124 -0
  93. data/lib/rgame/core/font.rb +49 -0
  94. data/lib/rgame/core/gamepad.rb +55 -0
  95. data/lib/rgame/core/image.rb +55 -0
  96. data/lib/rgame/core/input.rb +77 -0
  97. data/lib/rgame/core/nine_slice.rb +163 -0
  98. data/lib/rgame/core/recording.rb +52 -0
  99. data/lib/rgame/core/renderer.rb +363 -0
  100. data/lib/rgame/core/sprite_sheet.rb +108 -0
  101. data/lib/rgame/core/tile_map_renderer.rb +160 -0
  102. data/lib/rgame/core/ui_atlas.rb +86 -0
  103. data/lib/rgame/core.rb +24 -0
  104. data/lib/rgame/engine/actor.rb +53 -0
  105. data/lib/rgame/engine/animation_set.rb +49 -0
  106. data/lib/rgame/engine/animator.rb +44 -0
  107. data/lib/rgame/engine/audio_bus.rb +24 -0
  108. data/lib/rgame/engine/audio_director.rb +29 -0
  109. data/lib/rgame/engine/body.rb +49 -0
  110. data/lib/rgame/engine/cached_label.rb +33 -0
  111. data/lib/rgame/engine/camera.rb +33 -0
  112. data/lib/rgame/engine/camera_view.rb +28 -0
  113. data/lib/rgame/engine/circle_collider.rb +32 -0
  114. data/lib/rgame/engine/collision_box.rb +34 -0
  115. data/lib/rgame/engine/collision_system.rb +44 -0
  116. data/lib/rgame/engine/component.rb +30 -0
  117. data/lib/rgame/engine/components/action_trigger.rb +41 -0
  118. data/lib/rgame/engine/components/animated_sprite.rb +63 -0
  119. data/lib/rgame/engine/components/character_body.rb +70 -0
  120. data/lib/rgame/engine/components/circle_collider.rb +44 -0
  121. data/lib/rgame/engine/components/collision_world.rb +103 -0
  122. data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
  123. data/lib/rgame/engine/components/path_follow.rb +84 -0
  124. data/lib/rgame/engine/components/player_controller.rb +24 -0
  125. data/lib/rgame/engine/components/pool.rb +53 -0
  126. data/lib/rgame/engine/components/screen_wrap.rb +27 -0
  127. data/lib/rgame/engine/components/sprite.rb +31 -0
  128. data/lib/rgame/engine/components/targeting.rb +54 -0
  129. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  130. data/lib/rgame/engine/components/tile_world.rb +68 -0
  131. data/lib/rgame/engine/components/timer.rb +75 -0
  132. data/lib/rgame/engine/components/velocity.rb +27 -0
  133. data/lib/rgame/engine/components/wander_controller.rb +60 -0
  134. data/lib/rgame/engine/debug_overlay.rb +106 -0
  135. data/lib/rgame/engine/i18n.rb +97 -0
  136. data/lib/rgame/engine/input/action_mapper.rb +46 -0
  137. data/lib/rgame/engine/input/actions.rb +41 -0
  138. data/lib/rgame/engine/input/player_controller.rb +14 -0
  139. data/lib/rgame/engine/matrix.rb +32 -0
  140. data/lib/rgame/engine/node2d.rb +271 -0
  141. data/lib/rgame/engine/path.rb +78 -0
  142. data/lib/rgame/engine/pool.rb +51 -0
  143. data/lib/rgame/engine/resettable.rb +67 -0
  144. data/lib/rgame/engine/scene/scene_stack.rb +65 -0
  145. data/lib/rgame/engine/signal.rb +75 -0
  146. data/lib/rgame/engine/spatial_hash.rb +71 -0
  147. data/lib/rgame/engine/tile_collision.rb +78 -0
  148. data/lib/rgame/engine/tile_map.rb +149 -0
  149. data/lib/rgame/engine/tileset.rb +101 -0
  150. data/lib/rgame/engine/timer.rb +51 -0
  151. data/lib/rgame/engine.rb +68 -0
  152. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  153. data/lib/rgame/fonts/OFL.txt +102 -0
  154. data/lib/rgame/game.rb +129 -0
  155. data/lib/rgame/util/color.rb +27 -0
  156. data/lib/rgame/util/controls.rb +107 -0
  157. data/lib/rgame/util/tensor.rb +12 -0
  158. data/lib/rgame/util.rb +8 -0
  159. data/lib/rgame/version.rb +12 -0
  160. data/lib/rgame.rb +20 -0
  161. metadata +215 -0
@@ -0,0 +1,106 @@
1
+ #ifndef RGAME_TRANSFORM_H
2
+ #define RGAME_TRANSFORM_H
3
+
4
+ /*
5
+ * The 2D affine transform stack — pure arithmetic, no SDL, no GL, no I/O.
6
+ *
7
+ * This is what lets a node spin its whole subtree about its own origin, and
8
+ * what lets a camera shift a world subtree by (-camera_x, -camera_y) without
9
+ * any world-space node knowing a camera exists. Split-screen is the same
10
+ * mechanism used twice in one frame, which is why this is a real stack rather
11
+ * than one "current transform" global.
12
+ *
13
+ * ---------------------------------------------------------------------------
14
+ * What an affine transform is, since this is the first one in the project
15
+ * ---------------------------------------------------------------------------
16
+ *
17
+ * Every operation we need — move, rotate, scale, and any combination — can be
18
+ * written as six numbers applied to a point like this:
19
+ *
20
+ * x' = a*x + c*y + tx
21
+ * y' = b*x + d*y + ty
22
+ *
23
+ * `a b c d` are the "linear" part (rotation and scale); `tx ty` are the
24
+ * translation. Six floats, and the same six lines of arithmetic no matter how
25
+ * many transforms are nested — because *composing* two transforms produces
26
+ * another six numbers. That is the whole reason to use matrices here: nesting
27
+ * ten transforms costs the same per-vertex work as nesting one.
28
+ *
29
+ * ---------------------------------------------------------------------------
30
+ * Rotation direction — measured, not assumed
31
+ * ---------------------------------------------------------------------------
32
+ *
33
+ * A positive angle rotates **clockwise on screen**: a point to the right of the
34
+ * pivot moves to below it. Established by rendering a rotated image off-screen
35
+ * and reading back which pixel the ink landed on, not by reasoning about the
36
+ * matrix — `spec_core/rgame/core/renderer_spec.rb` still does exactly that.
37
+ *
38
+ * It falls out of the standard rotation matrix without a sign flip, because
39
+ * screen y points *down*: the same matrix that turns anticlockwise on graph
40
+ * paper turns clockwise here. Worth knowing rather than rediscovering — get it
41
+ * backwards and every rotated sprite in the game mirrors.
42
+ *
43
+ * Angles are degrees, which is what `Renderer#rotated` callers pass and what
44
+ * a level editor writes.
45
+ */
46
+
47
+ /* Deep enough for any sane scene graph; a bounded stack cannot run away, and
48
+ * an overflow is reported rather than silently scribbling past the end. */
49
+ #define RGAME_TRANSFORM_STACK_DEPTH 32
50
+
51
+ typedef struct {
52
+ float a, b, c, d; /* linear part: rotation and scale */
53
+ float tx, ty; /* translation */
54
+ } rgame_transform;
55
+
56
+ typedef struct {
57
+ /* entries[0] is always identity — the un-transformed base — so `depth` is
58
+ * the number of pushes outstanding and entries[depth] is the current top. */
59
+ rgame_transform entries[RGAME_TRANSFORM_STACK_DEPTH];
60
+ int depth;
61
+ } rgame_transform_stack;
62
+
63
+ rgame_transform rgame_transform_identity(void);
64
+
65
+ /*
66
+ * Composition: the result applies `inner` first, then `outer`. That is,
67
+ *
68
+ * apply(multiply(outer, inner), p) == apply(outer, apply(inner, p))
69
+ *
70
+ * which is the order nesting reads in: in `translated { rotated { draw } }`
71
+ * the rotation happens to the point first and the translation moves the
72
+ * already-rotated result.
73
+ */
74
+ rgame_transform rgame_transform_multiply(rgame_transform outer, rgame_transform inner);
75
+
76
+ void rgame_transform_stack_init(rgame_transform_stack *stack);
77
+
78
+ /*
79
+ * Each push composes with the current top and pushes the *result*, so `apply`
80
+ * stays a single six-multiply operation however deep the nesting goes.
81
+ *
82
+ * All three return 1 on success and 0 if the stack is full. A full stack leaves
83
+ * the existing entries untouched — the caller's drawing comes out unrotated
84
+ * rather than corrupt — but it does mean a matching pop must not be issued, so
85
+ * check the return value if the depth is not statically known.
86
+ */
87
+ int rgame_transform_push_translate(rgame_transform_stack *stack, float dx, float dy);
88
+ int rgame_transform_push_rotate(rgame_transform_stack *stack, float degrees,
89
+ float pivot_x, float pivot_y);
90
+ int rgame_transform_push_scale(rgame_transform_stack *stack, float sx, float sy);
91
+
92
+ /* Pops the current top. Popping the base is a no-op rather than an underflow. */
93
+ void rgame_transform_pop(rgame_transform_stack *stack);
94
+
95
+ rgame_transform rgame_transform_current(const rgame_transform_stack *stack);
96
+ int rgame_transform_depth(const rgame_transform_stack *stack);
97
+
98
+ /* Maps a point through the current top. This is the per-vertex hot path. */
99
+ void rgame_transform_apply(const rgame_transform_stack *stack, float x, float y,
100
+ float *out_x, float *out_y);
101
+
102
+ /* Whether the current top would leave every point where it is. Lets a caller
103
+ * skip work that would be a no-op — the common case for un-transformed draws. */
104
+ int rgame_transform_is_identity(const rgame_transform_stack *stack);
105
+
106
+ #endif /* RGAME_TRANSFORM_H */
@@ -0,0 +1,577 @@
1
+ #ifndef RGAME_CORE_H
2
+ #define RGAME_CORE_H
3
+
4
+ #ifdef __cplusplus
5
+ extern "C" {
6
+ #endif
7
+
8
+ /* size_t only — this header still exposes no SDL or GL types. */
9
+ #include <stddef.h>
10
+
11
+ /*
12
+ * Public API of the core engine.
13
+ *
14
+ * This header intentionally exposes no SDL or OpenGL types. Callers (the
15
+ * standalone app in src/main.c, the Ruby C extension in core_ext.c) only ever
16
+ * see an opaque handle. That keeps the door open for wrapping this library
17
+ * from Ruby without leaking C-library-specific types into ruby.h-using code.
18
+ */
19
+
20
+ typedef struct rgame_app rgame_app;
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);
24
+
25
+ /* Destroys the GL context/window and frees the app. Safe to call with NULL. */
26
+ void rgame_app_destroy(rgame_app *app);
27
+
28
+ /*
29
+ * ---------------------------------------------------------------------------
30
+ * Input: devices and buttons
31
+ * ---------------------------------------------------------------------------
32
+ *
33
+ * One flat button-id space covers every input device, so a single "is this
34
+ * held" query serves them all. It is partitioned into ranges rather than
35
+ * packed into bit fields, which means adding a device class later appends to
36
+ * an unused range instead of renumbering anything that already exists:
37
+ *
38
+ * 0x0000–0x0FFF keyboard (SDL scancode values)
39
+ * 0x1000–0x10FF gamepad buttons and dpad
40
+ *
41
+ * (The range a mouse would have occupied is deliberately left unused. Mouse
42
+ * input is not part of this engine — see RGame::Core::Input — and leaving the
43
+ * gap costs nothing while renumbering later would cost every saved keybinding.)
44
+ *
45
+ * Keyboard ids are SDL scancodes, but callers must not need SDL to name them:
46
+ * src/main.c includes only this header, so the constants it uses have to live
47
+ * here. app.c carries a _Static_assert for every one, so these can never
48
+ * silently drift from the SDL values they mirror.
49
+ */
50
+ #define RGAME_BUTTON_KEYBOARD_FIRST 0x0000
51
+ #define RGAME_BUTTON_KEYBOARD_LAST 0x0FFF
52
+ #define RGAME_BUTTON_GAMEPAD_FIRST 0x1000
53
+ #define RGAME_BUTTON_GAMEPAD_LAST 0x10FF
54
+
55
+ /* Keyboard ids == SDL scancodes. Only the keys the engine actually binds are
56
+ * named; adding one is a #define plus a _Static_assert in app.c. */
57
+ #define RGAME_KEY_RETURN 40
58
+ #define RGAME_KEY_ESCAPE 41
59
+ #define RGAME_KEY_SPACE 44
60
+ #define RGAME_KEY_F1 58
61
+ #define RGAME_KEY_RIGHT 79
62
+ #define RGAME_KEY_LEFT 80
63
+ #define RGAME_KEY_DOWN 81
64
+ #define RGAME_KEY_UP 82
65
+
66
+ /* Gamepad button ids: the gamepad range plus SDL's controller button number.
67
+ * app.c asserts each against the SDL constant it mirrors, as with the keys. */
68
+ #define RGAME_PAD_A (RGAME_BUTTON_GAMEPAD_FIRST + 0)
69
+ #define RGAME_PAD_B (RGAME_BUTTON_GAMEPAD_FIRST + 1)
70
+ #define RGAME_PAD_X (RGAME_BUTTON_GAMEPAD_FIRST + 2)
71
+ #define RGAME_PAD_Y (RGAME_BUTTON_GAMEPAD_FIRST + 3)
72
+ #define RGAME_PAD_BACK (RGAME_BUTTON_GAMEPAD_FIRST + 4)
73
+ #define RGAME_PAD_GUIDE (RGAME_BUTTON_GAMEPAD_FIRST + 5)
74
+ #define RGAME_PAD_START (RGAME_BUTTON_GAMEPAD_FIRST + 6)
75
+ #define RGAME_PAD_LEFT_STICK (RGAME_BUTTON_GAMEPAD_FIRST + 7)
76
+ #define RGAME_PAD_RIGHT_STICK (RGAME_BUTTON_GAMEPAD_FIRST + 8)
77
+ #define RGAME_PAD_LEFT_SHOULDER (RGAME_BUTTON_GAMEPAD_FIRST + 9)
78
+ #define RGAME_PAD_RIGHT_SHOULDER (RGAME_BUTTON_GAMEPAD_FIRST + 10)
79
+ #define RGAME_PAD_DPAD_UP (RGAME_BUTTON_GAMEPAD_FIRST + 11)
80
+ #define RGAME_PAD_DPAD_DOWN (RGAME_BUTTON_GAMEPAD_FIRST + 12)
81
+ #define RGAME_PAD_DPAD_LEFT (RGAME_BUTTON_GAMEPAD_FIRST + 13)
82
+ #define RGAME_PAD_DPAD_RIGHT (RGAME_BUTTON_GAMEPAD_FIRST + 14)
83
+
84
+ /*
85
+ * Analog axes are their own small id space rather than part of the button
86
+ * space: they are float-valued and read through a different call, so folding
87
+ * them in would only invite asking for an axis as if it were a button.
88
+ *
89
+ * Stick axes read -1.0 to 1.0 (Y is positive *downwards*, as SDL reports it);
90
+ * triggers read 0.0 to 1.0. No dead zone is applied — where to put one is a
91
+ * game decision, and a resting stick genuinely does report small non-zero
92
+ * values.
93
+ */
94
+ #define RGAME_AXIS_LEFT_X 0
95
+ #define RGAME_AXIS_LEFT_Y 1
96
+ #define RGAME_AXIS_RIGHT_X 2
97
+ #define RGAME_AXIS_RIGHT_Y 3
98
+ #define RGAME_AXIS_TRIGGER_LEFT 4
99
+ #define RGAME_AXIS_TRIGGER_RIGHT 5
100
+
101
+ /*
102
+ * Which device a query is about. The keyboard is device 0 so single-player
103
+ * code can ignore the parameter entirely; gamepads follow, one per player
104
+ * slot, in the stable slot order the hot-plug table hands out.
105
+ */
106
+ #define RGAME_INPUT_KEYBOARD 0
107
+ #define RGAME_INPUT_GAMEPAD_FIRST 1
108
+ #define RGAME_INPUT_MAX_GAMEPADS 4
109
+ #define RGAME_INPUT_GAMEPAD(slot) (RGAME_INPUT_GAMEPAD_FIRST + (slot))
110
+ #define RGAME_INPUT_DEVICE_COUNT (RGAME_INPUT_GAMEPAD_FIRST + RGAME_INPUT_MAX_GAMEPADS)
111
+
112
+ /*
113
+ * Per-frame and event callbacks.
114
+ *
115
+ * These are deliberately fixed-arity (each takes exactly the arguments it
116
+ * needs, plus an opaque userdata pointer) rather than variadic. A variadic
117
+ * calling convention on a per-frame hot path allocates/marshals on every
118
+ * single call for no benefit — see docs/c_engine_feature_specs.md section 4,
119
+ * which calls this out as a concrete cost worth designing out from the start.
120
+ */
121
+ typedef void (*rgame_frame_begin_fn)(void *userdata);
122
+ typedef void (*rgame_update_fn)(void *userdata, double dt_seconds);
123
+ typedef void (*rgame_draw_fn)(void *userdata);
124
+ typedef int (*rgame_needs_redraw_fn)(void *userdata);
125
+ typedef void (*rgame_button_fn)(void *userdata, int button_id);
126
+ typedef void (*rgame_resize_fn)(void *userdata, int width, int height);
127
+ typedef void (*rgame_gamepad_fn)(void *userdata, int slot);
128
+
129
+ /*
130
+ * The callbacks rgame_app_run drives, gathered into one struct rather than
131
+ * passed as an ever-growing list of positional arguments. Every member may be
132
+ * NULL, in which case that hook is simply skipped.
133
+ *
134
+ * - `frame_begin` runs once per rendered frame, before that frame's
135
+ * simulation ticks. It is the place to sample input once and reuse the
136
+ * result across every catch-up tick, so a key held for one frame behaves
137
+ * the same however many ticks that frame ends up running.
138
+ * - `update` runs at a fixed timestep via an internal accumulator, so it may
139
+ * be called zero or several times per rendered frame; dt_seconds is that
140
+ * fixed step, not wall-clock frame time.
141
+ * - `needs_redraw` is polled before drawing; returning 0 skips the draw for
142
+ * that frame (simulation still advances). NULL means always redraw.
143
+ * - `draw` renders one frame.
144
+ * - `button_down`/`button_up` report discrete key presses and releases. Key
145
+ * repeats are filtered out, so holding a key reports exactly one press.
146
+ * - `resize` reports a new window size; the GL viewport is already updated.
147
+ * - `gamepad_connected`/`gamepad_disconnected` report a controller arriving
148
+ * at or leaving a player slot. A slot is stable across a momentary
149
+ * unplug/replug, so "player 2" stays player 2.
150
+ * - `userdata` is forwarded to every callback; may be NULL.
151
+ */
152
+ typedef struct {
153
+ rgame_frame_begin_fn frame_begin;
154
+ rgame_update_fn update;
155
+ rgame_needs_redraw_fn needs_redraw;
156
+ rgame_draw_fn draw;
157
+ rgame_button_fn button_down;
158
+ rgame_button_fn button_up;
159
+ rgame_resize_fn resize;
160
+ rgame_gamepad_fn gamepad_connected;
161
+ rgame_gamepad_fn gamepad_disconnected;
162
+ void *userdata;
163
+ } rgame_app_callbacks;
164
+
165
+ /*
166
+ * Runs the main loop until the window is closed or rgame_app_close is called.
167
+ *
168
+ * The engine owns the loop and calls back out through `callbacks`. Note there
169
+ * is no built-in quit key: closing on Escape is a game decision, so it belongs
170
+ * in a button_down handler rather than in here.
171
+ */
172
+ void rgame_app_run(rgame_app *app, const rgame_app_callbacks *callbacks);
173
+
174
+ /*
175
+ * Asks the loop to stop. Safe to call from inside a callback — the loop checks
176
+ * between steps, so it exits without starting further work this frame.
177
+ */
178
+ void rgame_app_close(rgame_app *app);
179
+
180
+ /* Current window size, in window coordinates. */
181
+ int rgame_app_width(const rgame_app *app);
182
+ int rgame_app_height(const rgame_app *app);
183
+
184
+ /* Window title. The returned string is owned by the window, not the caller. */
185
+ const char *rgame_app_title(const rgame_app *app);
186
+ void rgame_app_set_title(rgame_app *app, const char *title);
187
+
188
+ /*
189
+ * Is `button_id` currently held on `device`?
190
+ *
191
+ * This reads a snapshot taken once per frame, when the event queue is pumped —
192
+ * not live hardware state. That is deliberate: the answer is then identical
193
+ * for every simulation tick within one frame, so a key held for a single frame
194
+ * behaves the same whether that frame ran one catch-up tick or five. Sampling
195
+ * live state would make the result depend on tick count, which is exactly the
196
+ * nondeterminism a fixed timestep exists to remove.
197
+ *
198
+ * A device only answers for buttons in its own range, so asking a gamepad
199
+ * about a keyboard key is 0 rather than an error.
200
+ */
201
+ int rgame_app_input_down(const rgame_app *app, int device, int button_id);
202
+
203
+ /*
204
+ * Current value of an analog axis on `device`, read from the same per-frame
205
+ * snapshot as rgame_app_input_down. Sticks read -1.0..1.0, triggers 0.0..1.0.
206
+ * An unknown device or axis, or a slot with no controller, reads 0.0.
207
+ */
208
+ float rgame_app_input_axis(const rgame_app *app, int device, int axis_id);
209
+
210
+ /* Is a controller plugged into player `slot` (0-based, < RGAME_INPUT_MAX_GAMEPADS)? */
211
+ int rgame_app_gamepad_connected(const rgame_app *app, int slot);
212
+
213
+ /*
214
+ * Human-readable name of the controller in `slot`, for "Player 2: connect a
215
+ * controller" UI. Returns NULL when the slot is empty. The string is owned by
216
+ * the engine and is only valid while that controller stays connected.
217
+ */
218
+ const char *rgame_app_gamepad_name(const rgame_app *app, int slot);
219
+
220
+ /* How many controllers are currently connected. */
221
+ int rgame_app_gamepad_count(const rgame_app *app);
222
+
223
+ /* Monotonic milliseconds since startup. For time-based animation phase, etc. */
224
+ unsigned int rgame_app_ticks_ms(const rgame_app *app);
225
+
226
+ /* Most recent frames-per-second reading (updated ~once per second). */
227
+ double rgame_app_fps(const rgame_app *app);
228
+
229
+ /*
230
+ * ---------------------------------------------------------------------------
231
+ * Images
232
+ * ---------------------------------------------------------------------------
233
+ *
234
+ * An image is a rectangle of an uploaded texture. `rgame_image_load` decodes a
235
+ * PNG and uploads it; `rgame_image_subimage` and `rgame_image_tile` carve that
236
+ * upload into sprites *without decoding or uploading anything again* — they
237
+ * are views sharing one GPU texture, which is what makes slicing a sprite
238
+ * sheet into hundreds of frames cheap.
239
+ *
240
+ * Every handle returned here, views included, must be passed to
241
+ * `rgame_image_destroy`. The shared texture is deleted when the last of them
242
+ * goes, in whatever order they go — so a sheet can be dropped while its tiles
243
+ * are still in use.
244
+ *
245
+ * Images belong to the `app` whose GL context they were uploaded into, but they
246
+ * do not have to be destroyed before it. Destroying the app first closes the
247
+ * window immediately and takes its textures with it — a GL context frees
248
+ * everything in it — and the images left behind stay valid handles that free
249
+ * cleanly afterwards. Either order works, which matters because a garbage
250
+ * collector picks the order, not the programmer.
251
+ *
252
+ * Images are scaled with nearest-neighbour sampling, always: the engine exists
253
+ * to draw pixel art, and smoothing it is never the intent.
254
+ */
255
+ typedef struct rgame_image rgame_image;
256
+
257
+ /*
258
+ * Decodes the PNG at `path` and uploads it. Returns NULL on failure, writing a
259
+ * human-readable reason into `err` (which may be NULL if the caller does not
260
+ * want one). Failure is ordinary — a missing or corrupt asset file — so it is
261
+ * reported rather than logged and swallowed.
262
+ */
263
+ rgame_image *rgame_image_load(rgame_app *app, const char *path, char *err, size_t err_size);
264
+
265
+ /*
266
+ * A view of part of `image`, in pixels relative to `image` itself — so a
267
+ * subimage of a subimage composes, and none of them can address pixels outside
268
+ * what they were cut from. Returns NULL if the rectangle does not fit.
269
+ */
270
+ rgame_image *rgame_image_subimage(const rgame_image *image, int x, int y, int width, int height);
271
+
272
+ /*
273
+ * Grid slicing, for sprite sheets. `rgame_image_tile_count` reports how many
274
+ * whole tiles of that size fit (a partial tile at the right or bottom edge is
275
+ * padding and is not counted); `rgame_image_tile` returns the index-th of
276
+ * them, counting left to right and then top to bottom, or NULL if the index is
277
+ * out of range.
278
+ */
279
+ int rgame_image_tile_count(const rgame_image *image, int tile_width, int tile_height);
280
+ rgame_image *rgame_image_tile(const rgame_image *image, int tile_width, int tile_height,
281
+ int index);
282
+
283
+ int rgame_image_width(const rgame_image *image);
284
+ int rgame_image_height(const rgame_image *image);
285
+
286
+ /* Releases this handle's share of the texture. Safe to call with NULL. */
287
+ void rgame_image_destroy(rgame_image *image);
288
+
289
+ /*
290
+ * ---------------------------------------------------------------------------
291
+ * Drawing
292
+ * ---------------------------------------------------------------------------
293
+ *
294
+ * Every call here is only valid **inside the draw callback**. The app opens a
295
+ * frame before calling it and closes the frame afterwards, sorting and
296
+ * submitting what was drawn; outside that window there is no frame to draw
297
+ * into, and these calls do nothing. `rgame_app_is_drawing` says which state the
298
+ * app is in, so a binding can raise instead of silently discarding.
299
+ *
300
+ * Drawing is not immediate. A call appends to a queue that is z-sorted and
301
+ * batched when the frame closes, so the order calls are made in does not
302
+ * decide what ends up on top — `z` does, and equal z keeps call order.
303
+ *
304
+ * Colours are packed 0xRRGGBBAA, matching RGame::Util::Color#packed.
305
+ *
306
+ * Coordinates are in screen pixels with (0,0) at the top-left and y growing
307
+ * downwards, transformed by whatever is on the stack (see the push/pop calls
308
+ * below). Angles are in degrees, and positive turns clockwise on screen.
309
+ */
310
+
311
+ /* Is the app between opening and closing a frame — that is, inside `draw`? */
312
+ int rgame_app_is_drawing(const rgame_app *app);
313
+
314
+ void rgame_app_draw_rect(rgame_app *app, float x, float y, float width, float height,
315
+ unsigned int color, double z);
316
+
317
+ /*
318
+ * `xy8` is four points, `xy6` three, as flat x,y pairs. A quad's corners are
319
+ * taken in loop order — for a rectangle: top-left, top-right, bottom-right,
320
+ * bottom-left — so listing them in Z order gives an hourglass, not a shape.
321
+ */
322
+ void rgame_app_draw_quad(rgame_app *app, const float *xy8, unsigned int color, double z);
323
+ void rgame_app_draw_triangle(rgame_app *app, const float *xy6, unsigned int color, double z);
324
+
325
+ /* A line of real thickness, drawn as a quad — GL's own line width is a
326
+ * suggestion drivers may ignore above 1px. */
327
+ void rgame_app_draw_line(rgame_app *app, float x1, float y1, float x2, float y2,
328
+ float thickness, unsigned int color, double z);
329
+
330
+ /* A filled circle as a fan of `segments` triangles. */
331
+ void rgame_app_draw_circle(rgame_app *app, float cx, float cy, float radius, int segments,
332
+ unsigned int color, double z);
333
+
334
+ /*
335
+ * Images. Both return 0 without drawing if the image belongs to a *different*
336
+ * app, and 1 otherwise.
337
+ *
338
+ * That check is not pedantry. A GL texture lives in one context and is not
339
+ * shared with another, so drawing another window's image samples nothing and
340
+ * paints a plain white quad — no GL error, nothing in a log, just a white
341
+ * rectangle where the sprite should be. Reporting it lets a binding raise.
342
+ */
343
+
344
+ /* An image with its top-left at (x, y), at its natural size. */
345
+ int rgame_app_draw_image(rgame_app *app, const rgame_image *image, float x, float y,
346
+ unsigned int color, double z);
347
+
348
+ /*
349
+ * An image with its top-left at (x, y), scaled independently per axis. A
350
+ * negative scale mirrors the image *inside the same rectangle* rather than
351
+ * about the anchor, so (x, y) is the top-left corner whatever the sign; a zero
352
+ * scale draws nothing.
353
+ */
354
+ int rgame_app_draw_image_scaled(rgame_app *app, const rgame_image *image, float x, float y,
355
+ float scale_x, float scale_y, unsigned int color, double z);
356
+
357
+ /* An image centred on (cx, cy), rotated clockwise about that centre and
358
+ * uniformly scaled. */
359
+ int rgame_app_draw_image_rot(rgame_app *app, const rgame_image *image, float cx, float cy,
360
+ float angle_degrees, float scale, unsigned int color, double z);
361
+
362
+ /*
363
+ * The transform and clip stacks. Every push is undone by the same
364
+ * `rgame_app_pop`, so a caller can never pop the wrong one; a push that cannot
365
+ * be honoured (a full stack) is still counted, so pops stay balanced and the
366
+ * drawing comes out untransformed rather than desynchronised.
367
+ *
368
+ * A clip *narrows*: pushing one can only shrink the visible region, never widen
369
+ * it, so a child can never draw outside what its parent allowed. That is what
370
+ * makes split-screen a matter of one push per viewport.
371
+ */
372
+ void rgame_app_push_translate(rgame_app *app, float dx, float dy);
373
+ void rgame_app_push_rotate(rgame_app *app, float degrees, float pivot_x, float pivot_y);
374
+ void rgame_app_push_scale(rgame_app *app, float sx, float sy);
375
+ /*
376
+ * Returns 0 without pushing if a recording is open — see the recording section
377
+ * below for why a clip cannot be baked — and 1 otherwise. The other pushes
378
+ * always succeed as far as the caller is concerned.
379
+ */
380
+ int rgame_app_push_clip(rgame_app *app, int x, int y, int width, int height);
381
+ void rgame_app_pop(rgame_app *app);
382
+
383
+ /*
384
+ * ---------------------------------------------------------------------------
385
+ * Text
386
+ * ---------------------------------------------------------------------------
387
+ *
388
+ * A font is a typeface at **one pixel size**, with a glyph atlas behind it.
389
+ * Two sizes are two fonts. Glyphs are rasterised the first time they are drawn
390
+ * and kept, so the cost is bounded by the character set a game uses rather than
391
+ * by how many strings it draws — a score that changes every frame costs nothing
392
+ * after the first ten digits.
393
+ *
394
+ * Like an image, a font belongs to the app whose GL context its atlas lives in,
395
+ * and either may be destroyed first.
396
+ *
397
+ * Measuring needs no GL and works outside a frame, which is where laying out a
398
+ * menu happens. Drawing, like everything else, is only valid inside `draw`.
399
+ */
400
+ typedef struct rgame_font rgame_font;
401
+
402
+ /*
403
+ * Loads a TrueType font at `pixel_height`. Returns NULL on failure, writing a
404
+ * reason into `err` (which may be NULL). There is no font-*name* lookup and no
405
+ * system font database: a caller names a file. The engine ships one — see
406
+ * lib/rgame/fonts/ — and the Ruby binding defaults to it.
407
+ */
408
+ rgame_font *rgame_font_load(rgame_app *app, const char *path, int pixel_height, char *err,
409
+ size_t err_size);
410
+ void rgame_font_destroy(rgame_font *font);
411
+
412
+ /* The size the font was loaded at, which is also the line height to step by for
413
+ * a second line. */
414
+ int rgame_font_height(const rgame_font *font);
415
+
416
+ /*
417
+ * The width a string would draw at, in pixels, kerning included. `text` is
418
+ * UTF-8; malformed bytes measure as one replacement character each.
419
+ *
420
+ * This and `rgame_app_draw_text` walk the same code, so a label measured and
421
+ * then centred lands where it was measured to.
422
+ */
423
+ float rgame_font_measure(const rgame_font *font, const char *text, size_t length);
424
+
425
+ /*
426
+ * Draws one line of UTF-8 text with its top-left corner at (x, y) — the top of
427
+ * the line box, not the baseline, so a caller places text the way it places
428
+ * everything else.
429
+ *
430
+ * Newlines are not special: this draws one line. A caller wanting two splits
431
+ * the string and steps by `rgame_font_height`.
432
+ *
433
+ * Returns 0 without drawing if the font belongs to a different app, for the
434
+ * same reason images do.
435
+ */
436
+ int rgame_app_draw_text(rgame_app *app, rgame_font *font, const char *text, size_t length,
437
+ float x, float y, unsigned int color, double z);
438
+
439
+ /*
440
+ * ---------------------------------------------------------------------------
441
+ * Audio
442
+ * ---------------------------------------------------------------------------
443
+ *
444
+ * The one subsystem that touches neither SDL nor OpenGL. It talks to the
445
+ * platform's sound system directly — ALSA or PulseAudio on Linux, found at
446
+ * runtime — so a sound belongs to an `rgame_audio`, not to an `rgame_app`, and
447
+ * none of the window-lifetime rules that govern images and fonts apply here.
448
+ *
449
+ * With no working sound device the engine falls back to a silent one rather
450
+ * than failing: a game with no audio hardware runs, quietly. That is also what
451
+ * lets the whole stack be tested with no sound card.
452
+ *
453
+ * Two kinds of sound:
454
+ *
455
+ * rgame_sample short, decoded up front, played many times and overlapping
456
+ * rgame_song long, streamed from disk, one at a time, stoppable
457
+ *
458
+ * A three-minute track decoded up front would be some forty megabytes of PCM,
459
+ * and a footstep re-decoded on every step would be silly. Hence two types
460
+ * rather than one with a flag: each has only the operations that make sense for
461
+ * it, so there is no `playing?` on a fire-and-forget effect to answer wrongly.
462
+ *
463
+ * Ogg Vorbis and WAV. Both are read by code the engine ships; nothing has to be
464
+ * installed for sound to work.
465
+ */
466
+ typedef struct rgame_audio rgame_audio;
467
+ typedef struct rgame_sample rgame_sample;
468
+ typedef struct rgame_song rgame_song;
469
+
470
+ /*
471
+ * Opens the sound device. Returns NULL only if the audio engine could not be
472
+ * created at all, writing a reason into `err` (which may be NULL) — a machine
473
+ * with no sound hardware still gets a working, silent `rgame_audio`.
474
+ */
475
+ rgame_audio *rgame_audio_create(char *err, size_t err_size);
476
+ void rgame_audio_destroy(rgame_audio *audio);
477
+
478
+ /*
479
+ * Master volume, applied to everything. 1.0 is unchanged, 0.0 is silence, and
480
+ * above 1.0 amplifies — which can clip, and is the caller's business. Negative
481
+ * values are clamped to zero.
482
+ */
483
+ void rgame_audio_set_volume(rgame_audio *audio, float volume);
484
+ float rgame_audio_volume(const rgame_audio *audio);
485
+
486
+ /* Which sound system is in use — "PulseAudio", "ALSA", "Null" and so on. For
487
+ * diagnostics and for tests that want to know whether they are hearing
488
+ * anything. */
489
+ const char *rgame_audio_backend(const rgame_audio *audio);
490
+
491
+ /*
492
+ * A short sound, decoded into memory once. Playing it again while it is still
493
+ * sounding starts a second voice rather than restarting it, which is what makes
494
+ * a rapid-fire effect sound right.
495
+ *
496
+ * There is deliberately no stop and no `playing?`: a one-shot has no single
497
+ * voice to ask about. Volume is per sample rather than per play, and applies to
498
+ * voices already sounding.
499
+ */
500
+ rgame_sample *rgame_sample_load(rgame_audio *audio, const char *path, char *err,
501
+ size_t err_size);
502
+ void rgame_sample_destroy(rgame_sample *sample);
503
+ void rgame_sample_play(rgame_sample *sample);
504
+ void rgame_sample_set_volume(rgame_sample *sample, float volume);
505
+ float rgame_sample_volume(const rgame_sample *sample);
506
+
507
+ /*
508
+ * A long sound, streamed from disk. One voice, so playing it again while it
509
+ * sounds restarts it rather than layering.
510
+ *
511
+ * "Only one song at a time" is *not* enforced here — that is a policy a game
512
+ * decides, and the Ruby layer owns it.
513
+ */
514
+ rgame_song *rgame_song_load(rgame_audio *audio, const char *path, char *err, size_t err_size);
515
+ void rgame_song_destroy(rgame_song *song);
516
+ void rgame_song_play(rgame_song *song, int looping);
517
+ void rgame_song_stop(rgame_song *song);
518
+ int rgame_song_playing(const rgame_song *song);
519
+ /* Whether the last `play` asked for looping. */
520
+ int rgame_song_looping(const rgame_song *song);
521
+ void rgame_song_set_volume(rgame_song *song, float volume);
522
+ float rgame_song_volume(const rgame_song *song);
523
+
524
+ /*
525
+ * ---------------------------------------------------------------------------
526
+ * Recordings: drawing baked once and replayed cheaply
527
+ * ---------------------------------------------------------------------------
528
+ *
529
+ * A tile layer is a couple of thousand quads that have not changed since the
530
+ * level loaded. Between `rgame_app_begin_record` and `rgame_app_end_record`,
531
+ * draw calls are captured instead of added to the frame; what comes back is the
532
+ * finished, already-batched geometry, which replays as one call per texture
533
+ * however many draws went into it.
534
+ *
535
+ * Recording happens *inside* a frame (there is drawing to capture, after all),
536
+ * and nests nowhere: begin_record returns 0 if the app is not drawing or is
537
+ * already recording.
538
+ *
539
+ * Transforms inside the block are baked in; the transform in effect at replay
540
+ * is applied on top, which is what lets a baked layer scroll under a camera.
541
+ * Clips are not baked — clipping happens when pixels are rasterised, so a clip
542
+ * rectangle captured at one place on screen would be wrong everywhere else the
543
+ * recording is drawn. `rgame_app_push_clip` refuses while recording; clip the
544
+ * replay instead.
545
+ */
546
+ typedef struct rgame_recording rgame_recording;
547
+
548
+ /* Starts capturing. Returns 1 on success, 0 if not drawing or already
549
+ * recording. */
550
+ int rgame_app_begin_record(rgame_app *app);
551
+
552
+ /*
553
+ * Ends capturing and returns the baked result, which the caller owns and must
554
+ * pass to `rgame_recording_free`. Returns NULL if no recording was open or if
555
+ * memory ran out.
556
+ */
557
+ rgame_recording *rgame_app_end_record(rgame_app *app);
558
+
559
+ /* Ends capturing and throws the result away — for unwinding when whatever was
560
+ * being recorded failed part-way through. */
561
+ void rgame_app_cancel_record(rgame_app *app);
562
+
563
+ void rgame_recording_free(rgame_recording *recording);
564
+
565
+ /*
566
+ * Replays a recording with its origin at (x, y), at `z`, tinted by `color`
567
+ * (0xFFFFFFFF leaves the recorded colours alone). Unlike the layer this
568
+ * replaces, a recording is not limited to drawing white.
569
+ */
570
+ void rgame_app_draw_recording(rgame_app *app, const rgame_recording *recording, float x,
571
+ float y, unsigned int color, double z);
572
+
573
+ #ifdef __cplusplus
574
+ }
575
+ #endif
576
+
577
+ #endif /* RGAME_CORE_H */