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,29 @@
1
+ /*
2
+ * stb_vorbis_impl.c — the one translation unit that instantiates stb_vorbis.
3
+ *
4
+ * The odd one out among the vendored implementation files: stb_vorbis ships as
5
+ * a `.c` rather than a `.h`. The split still works the same way, because it
6
+ * honours `STB_VORBIS_HEADER_ONLY` — this file includes it *without* that macro
7
+ * to emit the code, and vorbis_decoder.c includes it *with* the macro to get
8
+ * only the declarations. Without that split both translation units would define
9
+ * every symbol and the link would fail.
10
+ *
11
+ * Decoding only. stb_vorbis has a pushdata API for streaming from a callback,
12
+ * which the engine does not use — see vorbis_decoder.c for why it buffers the
13
+ * compressed file instead.
14
+ *
15
+ * Licence: stb_vorbis is public domain / MIT. See vendor/README.md.
16
+ */
17
+
18
+ /*
19
+ * No integer-decode path: the decoder hands miniaudio 32-bit floats, which is
20
+ * what its data sources speak. It guards only the integer-output functions,
21
+ * none of which vorbis_decoder.c calls, so the declarations it sees without
22
+ * this macro still match what is compiled here.
23
+ */
24
+ #define STB_VORBIS_NO_INTEGER_CONVERSION
25
+
26
+ /* stdio stays in: opening by filename is one of the decoder's two entry
27
+ * points. */
28
+
29
+ #include "vendor/stb_vorbis.c"
@@ -0,0 +1,19 @@
1
+ #include "color.h"
2
+
3
+ static int clamp_component(int value) {
4
+ if (value < 0) {
5
+ return 0;
6
+ }
7
+ if (value > 255) {
8
+ return 255;
9
+ }
10
+ return value;
11
+ }
12
+
13
+ rgame_color rgame_color_rgba(int r, int g, int b, int a) {
14
+ return ((rgame_color)clamp_component(r) << 24) |
15
+ ((rgame_color)clamp_component(g) << 16) |
16
+ ((rgame_color)clamp_component(b) << 8) |
17
+ (rgame_color)clamp_component(a);
18
+ }
19
+
@@ -0,0 +1,60 @@
1
+ #ifndef RGAME_COLOR_H
2
+ #define RGAME_COLOR_H
3
+
4
+ #include <stdint.h>
5
+
6
+ /*
7
+ * RGBA colour packing — pure arithmetic, no Ruby, no SDL, no GL.
8
+ *
9
+ * A colour is a *value*: four bytes, no handle, nothing to release. That is why
10
+ * it lives in the graphics-free half of the project even though the renderer is
11
+ * its main consumer — the engine layer may hold Util values as attributes but
12
+ * may not name RGame::Core at all, so a colour in Core would be out of reach of
13
+ * the scene nodes that want to store one. See CLAUDE.md, "Value objects go in
14
+ * Util; only handle-owners go in Core".
15
+ *
16
+ * The packed form is 0xRRGGBBAA, which is how a human writes a colour and how
17
+ * `Color#packed` reads back.
18
+ */
19
+
20
+ typedef uint32_t rgame_color;
21
+
22
+ /* Components outside 0..255 are clamped, so this is total: there is no way to
23
+ * call it wrong. The Ruby wrapper is stricter and raises instead, because a
24
+ * Ruby caller passing 300 has a bug worth hearing about. */
25
+ rgame_color rgame_color_rgba(int r, int g, int b, int a);
26
+
27
+ /*
28
+ * The accessors are `static inline` in the header rather than functions in the
29
+ * .c, because the engine half of the project needs them too — the renderer
30
+ * writes these four bytes into every vertex. Core and Util are separate shared
31
+ * objects, so a real function would mean linking one into the other; an inline
32
+ * definition costs nothing and keeps a single source of truth for the byte
33
+ * order, which is the part that must not drift.
34
+ */
35
+ static inline int rgame_color_r(rgame_color color) { return (int)((color >> 24) & 0xFFu); }
36
+ static inline int rgame_color_g(rgame_color color) { return (int)((color >> 16) & 0xFFu); }
37
+ static inline int rgame_color_b(rgame_color color) { return (int)((color >> 8) & 0xFFu); }
38
+ static inline int rgame_color_a(rgame_color color) { return (int)(color & 0xFFu); }
39
+
40
+ /*
41
+ * Writes the four components into `out` in R, G, B, A order — the order
42
+ * OpenGL reads them with glColorPointer(4, GL_UNSIGNED_BYTE, ...).
43
+ *
44
+ * Use this rather than copying the packed uint32 into a vertex. The two are not
45
+ * interchangeable: on a little-endian machine the bytes of 0xRRGGBBAA come out
46
+ * A, B, G, R, so GL would read the alpha as red. Going through bytes sidesteps
47
+ * the question entirely and is endian-independent.
48
+ */
49
+ static inline void rgame_color_bytes(rgame_color color, unsigned char out[4]) {
50
+ out[0] = (unsigned char)rgame_color_r(color);
51
+ out[1] = (unsigned char)rgame_color_g(color);
52
+ out[2] = (unsigned char)rgame_color_b(color);
53
+ out[3] = (unsigned char)rgame_color_a(color);
54
+ }
55
+
56
+ #define RGAME_COLOR_WHITE ((rgame_color)0xFFFFFFFFu)
57
+ #define RGAME_COLOR_BLACK ((rgame_color)0x000000FFu)
58
+ #define RGAME_COLOR_TRANSPARENT ((rgame_color)0x00000000u)
59
+
60
+ #endif /* RGAME_COLOR_H */
@@ -0,0 +1,156 @@
1
+ /*
2
+ * color_ext.c — the Ruby binding for RGame::Util::Color.
3
+ *
4
+ * The arithmetic lives in color.{c,h}, which includes no ruby.h and is covered
5
+ * directly by the Check suite. This file is only the wrapper: argument
6
+ * checking, and the value semantics Ruby expects of a value object.
7
+ *
8
+ * Instances are frozen. A colour is a value, and a mutable one shared between
9
+ * two sprites would be a bug waiting for someone to tint one of them.
10
+ */
11
+
12
+ #include "util_ext.h"
13
+
14
+ #include "color.h"
15
+
16
+ /*
17
+ * TypedData lets a Ruby object own a raw C value. The payload here is a single
18
+ * uint32, small enough that it could have been an instance variable — but
19
+ * going through TypedData keeps the unwrap a pointer read rather than an ivar
20
+ * lookup, and matches how every other C-backed class in the project is built.
21
+ */
22
+ static const rb_data_type_t color_data_type = {
23
+ .wrap_struct_name = "rgame_color",
24
+ .function = {
25
+ .dmark = NULL, /* no Ruby objects inside */
26
+ .dfree = RUBY_TYPED_DEFAULT_FREE,
27
+ .dsize = NULL,
28
+ },
29
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
30
+ };
31
+
32
+ static rgame_color color_unwrap(VALUE self) {
33
+ rgame_color *color;
34
+ TypedData_Get_Struct(self, rgame_color, &color_data_type, color);
35
+ return *color;
36
+ }
37
+
38
+ static VALUE color_wrap(VALUE klass, rgame_color value) {
39
+ rgame_color *storage;
40
+ VALUE object = TypedData_Make_Struct(klass, rgame_color, &color_data_type, storage);
41
+ *storage = value;
42
+ return rb_obj_freeze(object);
43
+ }
44
+
45
+ /* Ruby is stricter than the C layer, which clamps: a Ruby caller passing 300
46
+ * has a bug, and silently turning it into 255 hides it. */
47
+ static int component(VALUE value, const char *name) {
48
+ int number = NUM2INT(value);
49
+ if (number < 0 || number > 255) {
50
+ rb_raise(rb_eArgError, "%s must be in 0..255, got %d", name, number);
51
+ }
52
+ return number;
53
+ }
54
+
55
+ /* Color.new(r, g, b, a = 255) */
56
+ static VALUE color_s_new(int argc, VALUE *argv, VALUE klass) {
57
+ VALUE r, g, b, a;
58
+ rb_scan_args(argc, argv, "31", &r, &g, &b, &a);
59
+
60
+ return color_wrap(klass,
61
+ rgame_color_rgba(component(r, "red"), component(g, "green"),
62
+ component(b, "blue"),
63
+ NIL_P(a) ? 255 : component(a, "alpha")));
64
+ }
65
+
66
+ /* Color.from_packed(0xRRGGBBAA) */
67
+ static VALUE color_s_from_packed(VALUE klass, VALUE packed) {
68
+ unsigned long value = NUM2ULONG(packed);
69
+ if (value > 0xFFFFFFFFul) {
70
+ rb_raise(rb_eArgError, "packed colour must fit in 32 bits");
71
+ }
72
+ return color_wrap(klass, (rgame_color)value);
73
+ }
74
+
75
+ /*
76
+ * Color.coerce(nil | [r,g,b] | [r,g,b,a] | Color) -> Color
77
+ *
78
+ * Every draw call accepts a colour in any of those forms, so the conversion
79
+ * lives in one place rather than being repeated in each primitive. nil means
80
+ * white, matching what an untinted draw has always meant.
81
+ */
82
+ static VALUE color_s_coerce(VALUE klass, VALUE value) {
83
+ if (NIL_P(value)) {
84
+ return rb_const_get(klass, rb_intern("WHITE"));
85
+ }
86
+ if (rb_obj_is_kind_of(value, klass)) {
87
+ return value;
88
+ }
89
+ if (RB_TYPE_P(value, T_ARRAY)) {
90
+ long length = RARRAY_LEN(value);
91
+ if (length != 3 && length != 4) {
92
+ rb_raise(rb_eArgError, "colour array must be [r, g, b] or [r, g, b, a], got %ld elements",
93
+ length);
94
+ }
95
+ VALUE alpha = length == 4 ? rb_ary_entry(value, 3) : INT2FIX(255);
96
+ VALUE args[4] = { rb_ary_entry(value, 0), rb_ary_entry(value, 1),
97
+ rb_ary_entry(value, 2), alpha };
98
+ return color_s_new(4, args, klass);
99
+ }
100
+ rb_raise(rb_eTypeError, "cannot coerce %" PRIsVALUE " into a colour", rb_obj_class(value));
101
+ }
102
+
103
+ static VALUE color_r(VALUE self) { return INT2FIX(rgame_color_r(color_unwrap(self))); }
104
+ static VALUE color_g(VALUE self) { return INT2FIX(rgame_color_g(color_unwrap(self))); }
105
+ static VALUE color_b(VALUE self) { return INT2FIX(rgame_color_b(color_unwrap(self))); }
106
+ static VALUE color_a(VALUE self) { return INT2FIX(rgame_color_a(color_unwrap(self))); }
107
+
108
+ /* The 0xRRGGBBAA form. This is what the renderer hands to C — note it is *not*
109
+ * what goes into a vertex byte-for-byte; see rgame_color_bytes in color.h. */
110
+ static VALUE color_packed(VALUE self) { return UINT2NUM(color_unwrap(self)); }
111
+
112
+ /* Value semantics: two colours with the same components are the same colour,
113
+ * and eql?/hash follow so a Color works as a Hash key. */
114
+ static VALUE color_equal(VALUE self, VALUE other) {
115
+ if (!rb_obj_is_kind_of(other, rb_obj_class(self))) {
116
+ return Qfalse;
117
+ }
118
+ return color_unwrap(self) == color_unwrap(other) ? Qtrue : Qfalse;
119
+ }
120
+
121
+ static VALUE color_hash(VALUE self) {
122
+ return rb_hash(UINT2NUM(color_unwrap(self)));
123
+ }
124
+
125
+ static VALUE color_inspect(VALUE self) {
126
+ rgame_color color = color_unwrap(self);
127
+ return rb_sprintf("#<RGame::Util::Color r=%d g=%d b=%d a=%d>", rgame_color_r(color),
128
+ rgame_color_g(color), rgame_color_b(color), rgame_color_a(color));
129
+ }
130
+
131
+ void rgame_init_color(VALUE mUtil) {
132
+ VALUE cColor = rb_define_class_under(mUtil, "Color", rb_cObject);
133
+
134
+ /* No alloc func: a Color is always built through one of these, so there is
135
+ * no half-initialized state to guard against. */
136
+ rb_undef_alloc_func(cColor);
137
+ rb_define_singleton_method(cColor, "new", color_s_new, -1);
138
+ rb_define_singleton_method(cColor, "rgba", color_s_new, -1);
139
+ rb_define_singleton_method(cColor, "from_packed", color_s_from_packed, 1);
140
+ rb_define_singleton_method(cColor, "coerce", color_s_coerce, 1);
141
+
142
+ rb_define_method(cColor, "r", color_r, 0);
143
+ rb_define_method(cColor, "g", color_g, 0);
144
+ rb_define_method(cColor, "b", color_b, 0);
145
+ rb_define_method(cColor, "a", color_a, 0);
146
+ rb_define_method(cColor, "packed", color_packed, 0);
147
+ rb_define_method(cColor, "==", color_equal, 1);
148
+ rb_define_method(cColor, "eql?", color_equal, 1);
149
+ rb_define_method(cColor, "hash", color_hash, 0);
150
+ rb_define_method(cColor, "inspect", color_inspect, 0);
151
+ rb_define_alias(cColor, "to_s", "inspect");
152
+
153
+ rb_define_const(cColor, "WHITE", color_wrap(cColor, RGAME_COLOR_WHITE));
154
+ rb_define_const(cColor, "BLACK", color_wrap(cColor, RGAME_COLOR_BLACK));
155
+ rb_define_const(cColor, "TRANSPARENT", color_wrap(cColor, RGAME_COLOR_TRANSPARENT));
156
+ }
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # extconf.rb — mkmf script for the `rgame/util_ext` extension.
4
+ #
5
+ # This is the deliberately minimal half of the project's two extensions. Unlike
6
+ # ext/rgame_core/extconf.rb (which links SDL2 + OpenGL for the engine), the
7
+ # util extension is pure data: RGame::Util::Tensor and future pure-logic helpers
8
+ # that must NOT drag SDL/GL into the process just to be required. So there is no
9
+ # pkg_config, no -lGL — only the C standard + warning flags.
10
+ #
11
+ # That split is the rule for deciding where new code goes: anything depending on
12
+ # SDL/OpenGL (or on something that does) belongs under RGame::Core in
13
+ # ../rgame_core; everything else belongs here under RGame::Util.
14
+
15
+ require 'mkmf'
16
+
17
+ # Match the project's C standard and warning flags. gnu17 (not plain c17) because
18
+ # Ruby's headers lean on GNU extensions; gnu17 is a superset so the code still
19
+ # compiles as c17. Same choice as ext/rgame_core/extconf.rb.
20
+ $CFLAGS << ' -std=gnu17 -Wall -Wextra'
21
+
22
+ # "rgame/util_ext" -> the built object is loaded via `require "rgame/util_ext"`
23
+ # and its entry point is Init_util_ext (the basename). Naming it under rgame/
24
+ # (rather than plain "util_ext") keeps it namespaced alongside the engine's
25
+ # rgame/core_ext.so and off the top of the load path, leaving the bare name
26
+ # "rgame" to lib/rgame.rb.
27
+ create_makefile('rgame/util_ext')
@@ -0,0 +1,186 @@
1
+ /*
2
+ * tensor.c — RGame::Util::Tensor, a C-backed re-implementation of what used to
3
+ * live in lib/rgame/util/tensor.rb.
4
+ *
5
+ * A fixed-size 3-D grid backed by a single flat C array, addressed as [x, y, z].
6
+ * Layout is x-fastest then y then z, so one z-slice ([*, *, z]) is a contiguous
7
+ * run — same layout the Ruby version documented.
8
+ *
9
+ * This is a *pure data* extension: it includes only <ruby.h>, never SDL/GL, so
10
+ * requiring it costs nothing but the Ruby object machinery. It is a separate
11
+ * .so from the SDL-linked engine extension for exactly that reason.
12
+ *
13
+ * The one genuinely C-extension-specific concern here is the garbage collector.
14
+ * Each cell holds an arbitrary Ruby object (nil, an Integer, a Symbol, ...). If
15
+ * we stash those VALUEs in a plain C array, the GC has no idea they're reachable
16
+ * and will happily collect them out from under us. TypedData lets us register a
17
+ * `mark` function so the GC walks our array and keeps every stored value alive.
18
+ */
19
+
20
+ #include "util_ext.h"
21
+
22
+ /*
23
+ * The C struct each RGame::Util::Tensor Ruby object wraps. `data` is a flat
24
+ * array of `size` VALUEs (size == plane * depth, plane == width * height).
25
+ * Dimensions are `long` to match Ruby's integer width and NUM2LONG.
26
+ */
27
+ typedef struct {
28
+ long width;
29
+ long height;
30
+ long depth;
31
+ long plane; /* width * height — cached, one z-slice's worth of cells */
32
+ long size; /* plane * depth — total cell count / length of `data` */
33
+ VALUE *data;
34
+ } rgame_tensor;
35
+
36
+ /*
37
+ * GC mark: tell the collector every Ruby object we're holding is still alive.
38
+ * Called during GC while this Tensor is reachable. rb_gc_mark_locations marks a
39
+ * contiguous [start, end) range of VALUEs in one call, which is exactly our
40
+ * layout. Guard against data == NULL for a half-initialized object (allocated
41
+ * but #initialize not yet run / failed).
42
+ */
43
+ static void tensor_mark(void *ptr) {
44
+ rgame_tensor *t = ptr;
45
+ if (t->data) {
46
+ rb_gc_mark_locations(t->data, t->data + t->size);
47
+ }
48
+ }
49
+
50
+ /* Free the backing array and the struct itself when the Tensor is collected. */
51
+ static void tensor_free(void *ptr) {
52
+ rgame_tensor *t = ptr;
53
+ xfree(t->data); /* xfree(NULL) is safe */
54
+ xfree(t);
55
+ }
56
+
57
+ /* Report how much memory we own, for ObjectSpace / GC accounting. */
58
+ static size_t tensor_memsize(const void *ptr) {
59
+ const rgame_tensor *t = ptr;
60
+ return sizeof(rgame_tensor) + (t->data ? (size_t)t->size * sizeof(VALUE) : 0);
61
+ }
62
+
63
+ static const rb_data_type_t tensor_data_type = {
64
+ .wrap_struct_name = "RGame::Util::Tensor",
65
+ .function = {
66
+ .dmark = tensor_mark,
67
+ .dfree = tensor_free,
68
+ .dsize = tensor_memsize,
69
+ },
70
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
71
+ };
72
+
73
+ /* Pull the C struct back out of a Tensor Ruby object. */
74
+ static rgame_tensor *tensor_unwrap(VALUE self) {
75
+ rgame_tensor *t;
76
+ TypedData_Get_Struct(self, rgame_tensor, &tensor_data_type, t);
77
+ return t;
78
+ }
79
+
80
+ /*
81
+ * alloc: make the wrapper object with a zero-filled struct (TypedData_Make_Struct
82
+ * zeroes it), so data == NULL until #initialize allocates it. Splitting alloc
83
+ * from initialize is the standard Ruby way and means a failure mid-initialize
84
+ * still leaves a valid, freeable object.
85
+ */
86
+ static VALUE tensor_alloc(VALUE klass) {
87
+ rgame_tensor *t;
88
+ return TypedData_Make_Struct(klass, rgame_tensor, &tensor_data_type, t);
89
+ }
90
+
91
+ /*
92
+ * RGame::Util::Tensor.new(width, height, depth, initial: nil)
93
+ *
94
+ * rb_scan_args "30:" = 3 required positional args plus a trailing keyword/options
95
+ * hash (the ":"). We read :initial out of that hash; an absent key yields nil via
96
+ * rb_hash_aref, which matches the Ruby default of `initial: nil`.
97
+ */
98
+ static VALUE tensor_initialize(int argc, VALUE *argv, VALUE self) {
99
+ VALUE width, height, depth, opts;
100
+ rb_scan_args(argc, argv, "30:", &width, &height, &depth, &opts);
101
+
102
+ long w = NUM2LONG(width);
103
+ long h = NUM2LONG(height);
104
+ long d = NUM2LONG(depth);
105
+ if (w < 0 || h < 0 || d < 0) {
106
+ rb_raise(rb_eArgError, "tensor dimensions must be non-negative");
107
+ }
108
+
109
+ VALUE initial = Qnil;
110
+ if (!NIL_P(opts)) {
111
+ initial = rb_hash_aref(opts, ID2SYM(rb_intern("initial")));
112
+ }
113
+
114
+ rgame_tensor *t = tensor_unwrap(self);
115
+ t->width = w;
116
+ t->height = h;
117
+ t->depth = d;
118
+ t->plane = w * h;
119
+ t->size = t->plane * d;
120
+ t->data = ALLOC_N(VALUE, t->size); /* ALLOC_N(_, 0) returns a valid pointer */
121
+ for (long i = 0; i < t->size; i++) {
122
+ t->data[i] = initial;
123
+ }
124
+
125
+ return self;
126
+ }
127
+
128
+ /*
129
+ * Flat index for [x, y, z], with bounds checking. The Ruby version leaned on
130
+ * Array#[] semantics (nil past the end, negative-index wrap); on a raw C array
131
+ * those would be out-of-bounds reads, so instead we range-check every coordinate
132
+ * and raise IndexError — a clean, safe contract for a fixed-size grid.
133
+ */
134
+ static long tensor_offset(rgame_tensor *t, VALUE x, VALUE y, VALUE z) {
135
+ long xi = NUM2LONG(x);
136
+ long yi = NUM2LONG(y);
137
+ long zi = NUM2LONG(z);
138
+ if (xi < 0 || xi >= t->width ||
139
+ yi < 0 || yi >= t->height ||
140
+ zi < 0 || zi >= t->depth) {
141
+ rb_raise(rb_eIndexError,
142
+ "index (%ld, %ld, %ld) out of bounds for tensor "
143
+ "(%ld, %ld, %ld)",
144
+ xi, yi, zi, t->width, t->height, t->depth);
145
+ }
146
+ return (zi * t->plane) + (yi * t->width) + xi;
147
+ }
148
+
149
+ static VALUE tensor_aref(VALUE self, VALUE x, VALUE y, VALUE z) {
150
+ rgame_tensor *t = tensor_unwrap(self);
151
+ return t->data[tensor_offset(t, x, y, z)];
152
+ }
153
+
154
+ static VALUE tensor_aset(VALUE self, VALUE x, VALUE y, VALUE z, VALUE value) {
155
+ rgame_tensor *t = tensor_unwrap(self);
156
+ t->data[tensor_offset(t, x, y, z)] = value;
157
+ return value; /* Ruby returns the assigned value from []= regardless */
158
+ }
159
+
160
+ static VALUE tensor_width(VALUE self) {
161
+ return LONG2NUM(tensor_unwrap(self)->width);
162
+ }
163
+
164
+ static VALUE tensor_height(VALUE self) {
165
+ return LONG2NUM(tensor_unwrap(self)->height);
166
+ }
167
+
168
+ static VALUE tensor_depth(VALUE self) {
169
+ return LONG2NUM(tensor_unwrap(self)->depth);
170
+ }
171
+
172
+ /*
173
+ * Class init, called by Init_util_ext in util_ext.c. Splitting the entry point
174
+ * out means adding another Util class does not mean editing this file.
175
+ */
176
+ void rgame_init_tensor(VALUE mUtil) {
177
+ VALUE cTensor = rb_define_class_under(mUtil, "Tensor", rb_cObject);
178
+
179
+ rb_define_alloc_func(cTensor, tensor_alloc);
180
+ rb_define_method(cTensor, "initialize", tensor_initialize, -1);
181
+ rb_define_method(cTensor, "[]", tensor_aref, 3);
182
+ rb_define_method(cTensor, "[]=", tensor_aset, 4);
183
+ rb_define_method(cTensor, "width", tensor_width, 0);
184
+ rb_define_method(cTensor, "height", tensor_height, 0);
185
+ rb_define_method(cTensor, "depth", tensor_depth, 0);
186
+ }
@@ -0,0 +1,27 @@
1
+ /*
2
+ * util_ext.c — entry point for the graphics-free extension.
3
+ *
4
+ * Ruby calls Init_<basename of the required path> when the .so is loaded; we
5
+ * require it as "rgame/util_ext", so this must be Init_util_ext, matching
6
+ * create_makefile("rgame/util_ext") in extconf.rb.
7
+ *
8
+ * Its only job is to hand the RGame::Util module to each class's init
9
+ * function. Nothing here links SDL or OpenGL, and nothing ever should: the
10
+ * whole point of this extension is that `require "rgame"` costs no graphics
11
+ * libraries. See CLAUDE.md, "The Core / Util split".
12
+ */
13
+
14
+ #include "util_ext.h"
15
+
16
+ void Init_util_ext(void) {
17
+ /*
18
+ * rb_define_module is idempotent — it returns the existing RGame if some
19
+ * other extension or Ruby file defined it first, so load order between the
20
+ * two extensions doesn't matter.
21
+ */
22
+ VALUE mRGame = rb_define_module("RGame");
23
+ VALUE mUtil = rb_define_module_under(mRGame, "Util");
24
+
25
+ rgame_init_tensor(mUtil);
26
+ rgame_init_color(mUtil);
27
+ }
@@ -0,0 +1,16 @@
1
+ #ifndef RGAME_UTIL_EXT_H
2
+ #define RGAME_UTIL_EXT_H
3
+
4
+ #include <ruby.h>
5
+
6
+ /*
7
+ * Each Ruby-visible class in this extension exposes one init function, and
8
+ * util_ext.c calls them all. Adding a class means adding a file and one line
9
+ * there — rather than editing an unrelated class's file, which is where
10
+ * Init_util_ext used to live.
11
+ */
12
+
13
+ void rgame_init_tensor(VALUE mUtil);
14
+ void rgame_init_color(VALUE mUtil);
15
+
16
+ #endif /* RGAME_UTIL_EXT_H */
data/lib/rgame/boot.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Turns on YJIT, Ruby's JIT compiler, for a meaningful speedup in the game loop.
4
+ #
5
+ # Required by RGame::Game rather than left to each game to remember, because a
6
+ # game loop is the case JIT was built for and forgetting costs frames silently.
7
+ # It is deliberately *not* required by `rgame` or `rgame/core`: enabling a JIT
8
+ # is a decision about the whole process, and a library has no business making
9
+ # it for a host that only wanted a Color.
10
+ #
11
+ # Guarded, so the project still runs on a Ruby built without YJIT — it just runs
12
+ # unaccelerated instead of crashing.
13
+ RubyVM::YJIT.enable if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RGame::Core::App is implemented in C — see ext/rgame_core/. It wraps the
4
+ # engine's public API (ext/rgame_core/include/rgame/core.h): an SDL window plus
5
+ # OpenGL context, and a fixed-timestep main loop that drives callbacks.
6
+ #
7
+ # App is meant to be subclassed. The engine calls back into methods on the
8
+ # object itself, so a game overrides the hooks it needs and inherits no-ops for
9
+ # the rest:
10
+ #
11
+ # class MyGame < RGame::Core::App
12
+ # def initialize = super(width: 800, height: 600, caption: 'my game')
13
+ #
14
+ # def frame_begin; end # once per frame, before that frame's ticks
15
+ # def update(dt); end # one fixed simulation tick; dt is always the
16
+ # # fixed step, never wall-clock frame time
17
+ # def needs_redraw?; end # false skips the draw (simulation still runs)
18
+ # def draw; end # render one frame
19
+ # def button_down(id); end # discrete key press (no repeats)
20
+ # def button_up(id); end
21
+ # def resize(w, h); end
22
+ # end
23
+ #
24
+ # MyGame.new.run
25
+ #
26
+ # Also inherited: #close (stops the loop; safe from inside a callback), #width,
27
+ # #height, #caption, #caption=, #ticks_ms (monotonic ms since startup) and #fps.
28
+ #
29
+ # The loop owns the fixed-timestep accumulator, so #update is called once per
30
+ # whole tick and may run zero or several times per rendered frame. #frame_begin
31
+ # is the place to sample input once and reuse it across every tick of that
32
+ # frame.
33
+ #
34
+ # If a callback raises, the exception is carried out of #run with its original
35
+ # class, message and backtrace, and the loop shuts down cleanly first. A
36
+ # non-local exit (throw/break/return) out of a callback cannot be carried
37
+ # across the C loop and is reported as a RuntimeError instead — use #close to
38
+ # stop the loop.
39
+ #
40
+ # Loading this .so pulls SDL2 and OpenGL into the process — see
41
+ # lib/rgame/core.rb for why that's kept off the default `require "rgame"`.
42
+ # It loads from lib/rgame/core_ext.so, which the build (`make ext-core`)
43
+ # copies out of ext/rgame_core/.
44
+ require 'rgame/core_ext'
45
+
46
+ module RGame
47
+ module Core
48
+ # The two things a game needs one of, owned by the app that needs them and
49
+ # built on first use.
50
+ #
51
+ # Lazily, and that is the point on both counts: an app that draws only
52
+ # primitives builds no asset manager, and one that never plays a sound never
53
+ # opens a device — which is also the right moment to open it, since asking
54
+ # for a sound is the first thing that needs one.
55
+ class App
56
+ # This app's asset manager, rooted at the `media_root:` it was made with.
57
+ #
58
+ # class MyGame < RGame::Core::App
59
+ # def initialize = super(width: 640, height: 480, caption: 'demo', media_root: MEDIA)
60
+ # end
61
+ #
62
+ # app.assets.image('space.png')
63
+ #
64
+ # A game never constructs one. An `Image` belongs to one GL context and
65
+ # has to be told which, so *something* has to hold the app — and since the
66
+ # asset manager is the only thing in the engine that loads from a path,
67
+ # that something is here, once, rather than threaded through every
68
+ # constructor that ends up owning an image.
69
+ def assets = @assets ||= AssetManager.new(root: @media_root, app: self)
70
+
71
+ # This app's sound device. Not tied to the window in any way — audio has
72
+ # no GL context and survives one being recreated — it lives here because
73
+ # a game wants exactly one, the same way it wants one asset manager.
74
+ def audio = @audio ||= Audio.new
75
+
76
+ # Where #assets resolves relative paths from. Set once, as a keyword to
77
+ # the constructor; there is deliberately no writer, because changing it
78
+ # after an asset has loaded would leave a cache keyed against two roots.
79
+ attr_reader :media_root
80
+ end
81
+ end
82
+ end