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.
- checksums.yaml +7 -0
- data/LICENSE +26 -0
- data/README.md +406 -0
- data/docs/api/README.md +167 -0
- data/docs/api/app.md +192 -0
- data/docs/api/assets.md +426 -0
- data/docs/api/audio.md +208 -0
- data/docs/api/components.md +321 -0
- data/docs/api/drawing.md +330 -0
- data/docs/api/game.md +99 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +179 -0
- data/docs/api/internals.md +110 -0
- data/docs/api/scene_graph.md +159 -0
- data/docs/api/signals.md +142 -0
- data/docs/api/systems.md +98 -0
- data/docs/api/text.md +116 -0
- data/docs/api/toolbox.md +240 -0
- data/docs/api/values.md +101 -0
- data/ext/README.md +225 -0
- data/ext/rgame_core/app/app.c +721 -0
- data/ext/rgame_core/app/app_gl.h +64 -0
- data/ext/rgame_core/app/frame_loop.c +42 -0
- data/ext/rgame_core/app/frame_loop.h +54 -0
- data/ext/rgame_core/audio/audio.c +466 -0
- data/ext/rgame_core/audio/audio_internal.h +45 -0
- data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
- data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
- data/ext/rgame_core/example.rb +188 -0
- data/ext/rgame_core/extconf.rb +167 -0
- data/ext/rgame_core/graphics/backend.c +52 -0
- data/ext/rgame_core/graphics/backend.h +64 -0
- data/ext/rgame_core/graphics/canvas.c +247 -0
- data/ext/rgame_core/graphics/canvas.h +143 -0
- data/ext/rgame_core/graphics/clip.c +87 -0
- data/ext/rgame_core/graphics/clip.h +89 -0
- data/ext/rgame_core/graphics/draw_queue.c +216 -0
- data/ext/rgame_core/graphics/draw_queue.h +174 -0
- data/ext/rgame_core/graphics/gl_backend.c +122 -0
- data/ext/rgame_core/graphics/gl_backend.h +43 -0
- data/ext/rgame_core/graphics/image.c +304 -0
- data/ext/rgame_core/graphics/image_internal.h +30 -0
- data/ext/rgame_core/graphics/primitives.c +189 -0
- data/ext/rgame_core/graphics/primitives.h +111 -0
- data/ext/rgame_core/graphics/recording.c +119 -0
- data/ext/rgame_core/graphics/recording.h +88 -0
- data/ext/rgame_core/graphics/texture.c +181 -0
- data/ext/rgame_core/graphics/texture.h +165 -0
- data/ext/rgame_core/graphics/transform.c +128 -0
- data/ext/rgame_core/graphics/transform.h +106 -0
- data/ext/rgame_core/include/rgame/core.h +577 -0
- data/ext/rgame_core/input/device_slots.c +103 -0
- data/ext/rgame_core/input/device_slots.h +93 -0
- data/ext/rgame_core/input/gamepad.c +145 -0
- data/ext/rgame_core/input/gamepad.h +63 -0
- data/ext/rgame_core/input/input.c +109 -0
- data/ext/rgame_core/input/input.h +99 -0
- data/ext/rgame_core/ruby/audio_ext.c +321 -0
- data/ext/rgame_core/ruby/core_ext.c +513 -0
- data/ext/rgame_core/ruby/core_ext.h +51 -0
- data/ext/rgame_core/ruby/font_ext.c +168 -0
- data/ext/rgame_core/ruby/image_ext.c +230 -0
- data/ext/rgame_core/ruby/recording_ext.c +186 -0
- data/ext/rgame_core/ruby/renderer_ext.c +376 -0
- data/ext/rgame_core/text/atlas.c +59 -0
- data/ext/rgame_core/text/atlas.h +85 -0
- data/ext/rgame_core/text/font.c +281 -0
- data/ext/rgame_core/text/font.h +139 -0
- data/ext/rgame_core/text/font_atlas.c +385 -0
- data/ext/rgame_core/text/font_internal.h +47 -0
- data/ext/rgame_core/text/glyph_cache.c +142 -0
- data/ext/rgame_core/text/glyph_cache.h +89 -0
- data/ext/rgame_core/vendor/README.md +159 -0
- data/ext/rgame_core/vendor/miniaudio.h +95864 -0
- data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
- data/ext/rgame_core/vendor/stb_image.h +7988 -0
- data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
- data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
- data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
- data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
- data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
- data/ext/rgame_util/color.c +19 -0
- data/ext/rgame_util/color.h +60 -0
- data/ext/rgame_util/color_ext.c +156 -0
- data/ext/rgame_util/extconf.rb +27 -0
- data/ext/rgame_util/tensor.c +186 -0
- data/ext/rgame_util/util_ext.c +27 -0
- data/ext/rgame_util/util_ext.h +16 -0
- data/lib/rgame/boot.rb +13 -0
- data/lib/rgame/core/app.rb +82 -0
- data/lib/rgame/core/asset_manager.rb +224 -0
- data/lib/rgame/core/audio.rb +124 -0
- data/lib/rgame/core/font.rb +49 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +77 -0
- data/lib/rgame/core/nine_slice.rb +163 -0
- data/lib/rgame/core/recording.rb +52 -0
- data/lib/rgame/core/renderer.rb +363 -0
- data/lib/rgame/core/sprite_sheet.rb +108 -0
- data/lib/rgame/core/tile_map_renderer.rb +160 -0
- data/lib/rgame/core/ui_atlas.rb +86 -0
- data/lib/rgame/core.rb +24 -0
- data/lib/rgame/engine/actor.rb +53 -0
- data/lib/rgame/engine/animation_set.rb +49 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +29 -0
- data/lib/rgame/engine/body.rb +49 -0
- data/lib/rgame/engine/cached_label.rb +33 -0
- data/lib/rgame/engine/camera.rb +33 -0
- data/lib/rgame/engine/camera_view.rb +28 -0
- data/lib/rgame/engine/circle_collider.rb +32 -0
- data/lib/rgame/engine/collision_box.rb +34 -0
- data/lib/rgame/engine/collision_system.rb +44 -0
- data/lib/rgame/engine/component.rb +30 -0
- data/lib/rgame/engine/components/action_trigger.rb +41 -0
- data/lib/rgame/engine/components/animated_sprite.rb +63 -0
- data/lib/rgame/engine/components/character_body.rb +70 -0
- data/lib/rgame/engine/components/circle_collider.rb +44 -0
- data/lib/rgame/engine/components/collision_world.rb +103 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
- data/lib/rgame/engine/components/path_follow.rb +84 -0
- data/lib/rgame/engine/components/player_controller.rb +24 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +27 -0
- data/lib/rgame/engine/components/sprite.rb +31 -0
- data/lib/rgame/engine/components/targeting.rb +54 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +68 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +27 -0
- data/lib/rgame/engine/components/wander_controller.rb +60 -0
- data/lib/rgame/engine/debug_overlay.rb +106 -0
- data/lib/rgame/engine/i18n.rb +97 -0
- data/lib/rgame/engine/input/action_mapper.rb +46 -0
- data/lib/rgame/engine/input/actions.rb +41 -0
- data/lib/rgame/engine/input/player_controller.rb +14 -0
- data/lib/rgame/engine/matrix.rb +32 -0
- data/lib/rgame/engine/node2d.rb +271 -0
- data/lib/rgame/engine/path.rb +78 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/resettable.rb +67 -0
- data/lib/rgame/engine/scene/scene_stack.rb +65 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +71 -0
- data/lib/rgame/engine/tile_collision.rb +78 -0
- data/lib/rgame/engine/tile_map.rb +149 -0
- data/lib/rgame/engine/tileset.rb +101 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine.rb +68 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +129 -0
- data/lib/rgame/util/color.rb +27 -0
- data/lib/rgame/util/controls.rb +107 -0
- data/lib/rgame/util/tensor.rb +12 -0
- data/lib/rgame/util.rb +8 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +20 -0
- 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
|