rgame 0.2.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +144 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +3 -5
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +167 -12
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* locale.c — the user's preferred locales, as the operating system reports
|
|
3
|
+
* them through SDL.
|
|
4
|
+
*
|
|
5
|
+
* rgame_preferred_locales is a thin shim over SDL_GetPreferredLocales; the
|
|
6
|
+
* joining and the buffer contract are rgame_locale_append, which is pure and
|
|
7
|
+
* Check-tested. Choosing among the locales is not here at all: it is
|
|
8
|
+
* RGame::Engine::I18n.choose, specced headless.
|
|
9
|
+
*
|
|
10
|
+
* SDL answers without SDL_Init (measured on 2.0.20 under Linux, where it reads
|
|
11
|
+
* LANG and then LANGUAGE), so none of this belongs to an app.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
#include "app/locale.h"
|
|
15
|
+
|
|
16
|
+
#include <SDL2/SDL.h>
|
|
17
|
+
#include <string.h>
|
|
18
|
+
|
|
19
|
+
#include "rgame/core.h"
|
|
20
|
+
|
|
21
|
+
static size_t append_text(char *out, size_t capacity, size_t length, const char *text) {
|
|
22
|
+
for (; *text; text++, length++) {
|
|
23
|
+
if (length + 1 < capacity) {
|
|
24
|
+
out[length] = *text;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
size_t rgame_locale_append(char *out, size_t capacity, size_t length, const char *language,
|
|
31
|
+
const char *country) {
|
|
32
|
+
if (language == NULL || language[0] == '\0') {
|
|
33
|
+
return length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (length > 0) {
|
|
37
|
+
length = append_text(out, capacity, length, ",");
|
|
38
|
+
}
|
|
39
|
+
length = append_text(out, capacity, length, language);
|
|
40
|
+
if (country != NULL && country[0] != '\0') {
|
|
41
|
+
length = append_text(out, capacity, length, "-");
|
|
42
|
+
length = append_text(out, capacity, length, country);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (capacity > 0) {
|
|
46
|
+
out[length < capacity ? length : capacity - 1] = '\0';
|
|
47
|
+
}
|
|
48
|
+
return length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
size_t rgame_preferred_locales(char *out, size_t capacity) {
|
|
52
|
+
if (capacity > 0) {
|
|
53
|
+
out[0] = '\0';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
SDL_Locale *locales = SDL_GetPreferredLocales();
|
|
57
|
+
if (locales == NULL) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
size_t length = 0;
|
|
62
|
+
for (const SDL_Locale *locale = locales; locale->language != NULL; locale++) {
|
|
63
|
+
length = rgame_locale_append(out, capacity, length, locale->language, locale->country);
|
|
64
|
+
}
|
|
65
|
+
SDL_free(locales);
|
|
66
|
+
return length;
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#ifndef RGAME_LOCALE_H
|
|
2
|
+
#define RGAME_LOCALE_H
|
|
3
|
+
|
|
4
|
+
#include <stddef.h>
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The pure half of reading the user's preferred locales: joining them into one
|
|
8
|
+
* "de-AT,en" string in a caller's buffer. No SDL, so the Check suite covers the
|
|
9
|
+
* truncation contract on every platform, whatever locales the machine it runs
|
|
10
|
+
* on happens to prefer. locale.c's rgame_preferred_locales is the SDL shim
|
|
11
|
+
* that feeds it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Appends one locale to a list whose full length so far is `length`: a comma
|
|
16
|
+
* unless the list is empty, then `language`, then "-" and `country` when a
|
|
17
|
+
* country is given. Returns the list's new full length.
|
|
18
|
+
*
|
|
19
|
+
* It behaves like snprintf: `out` receives as much as fits in `capacity`,
|
|
20
|
+
* always NUL-terminated when `capacity` is non-zero, and the return value is
|
|
21
|
+
* the length the whole list needs, so a result >= `capacity` means it was
|
|
22
|
+
* truncated. `length` may already exceed `capacity`. A NULL or empty
|
|
23
|
+
* `language` appends nothing.
|
|
24
|
+
*/
|
|
25
|
+
size_t rgame_locale_append(char *out, size_t capacity, size_t length, const char *language,
|
|
26
|
+
const char *country);
|
|
27
|
+
|
|
28
|
+
#endif /* RGAME_LOCALE_H */
|
|
@@ -53,6 +53,11 @@ struct rgame_audio {
|
|
|
53
53
|
* lookup to find.
|
|
54
54
|
*/
|
|
55
55
|
int forced_no_device;
|
|
56
|
+
/*
|
|
57
|
+
* How many things still need this engine: the owner counts as one, and so
|
|
58
|
+
* does every sample and song loaded from it. See audio_release.
|
|
59
|
+
*/
|
|
60
|
+
int refs;
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
struct rgame_sample {
|
|
@@ -213,6 +218,8 @@ static rgame_audio *create_audio(int offline, unsigned int sample_rate, char *er
|
|
|
213
218
|
return NULL;
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
/* The caller's own claim. Sounds add theirs as they are loaded. */
|
|
222
|
+
audio->refs = 1;
|
|
216
223
|
return audio;
|
|
217
224
|
}
|
|
218
225
|
|
|
@@ -234,8 +241,28 @@ unsigned int rgame_audio_read(rgame_audio *audio, float *out, unsigned int frame
|
|
|
234
241
|
return (unsigned int)read;
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
|
|
238
|
-
|
|
244
|
+
/*
|
|
245
|
+
* Drops one claim on the device, and tears it down when the last one goes.
|
|
246
|
+
*
|
|
247
|
+
* The device has to outlive every sound made from it, and a refcount is what
|
|
248
|
+
* makes that true regardless of the order things are released in. That order is
|
|
249
|
+
* not ours to choose: Ruby's collector frees unreachable objects in whatever
|
|
250
|
+
* order it sweeps them, so a Song and the Audio it came from can be freed in
|
|
251
|
+
* the same pass with the Audio going first. Tearing down a *streaming* voice
|
|
252
|
+
* then asks the resource manager's job thread to acknowledge it — and that
|
|
253
|
+
* thread went with the engine, so the wait never returns and the process hangs
|
|
254
|
+
* rather than crashing.
|
|
255
|
+
*
|
|
256
|
+
* Marking the device from the sound, which audio_ext.c does, is a different
|
|
257
|
+
* guarantee: it keeps the device alive while a sound is still *reachable*. It
|
|
258
|
+
* says nothing about two objects that became garbage together.
|
|
259
|
+
*
|
|
260
|
+
* So the owner holds a reference, every sound holds one, and the last one out
|
|
261
|
+
* does the uninit. Same shape as the refcounted texture sheet in
|
|
262
|
+
* graphics/texture.c, and for the same kind of reason.
|
|
263
|
+
*/
|
|
264
|
+
static void audio_release(rgame_audio *audio) {
|
|
265
|
+
if (!audio || --audio->refs > 0) {
|
|
239
266
|
return;
|
|
240
267
|
}
|
|
241
268
|
|
|
@@ -246,6 +273,10 @@ void rgame_audio_destroy(rgame_audio *audio) {
|
|
|
246
273
|
free(audio);
|
|
247
274
|
}
|
|
248
275
|
|
|
276
|
+
void rgame_audio_destroy(rgame_audio *audio) {
|
|
277
|
+
audio_release(audio);
|
|
278
|
+
}
|
|
279
|
+
|
|
249
280
|
void rgame_audio_set_volume(rgame_audio *audio, float volume) {
|
|
250
281
|
if (audio) {
|
|
251
282
|
ma_engine_set_volume(&audio->engine, clamp_volume(volume));
|
|
@@ -367,6 +398,7 @@ rgame_sample *rgame_sample_load(rgame_audio *audio, const char *path, char *err,
|
|
|
367
398
|
strcpy(sample->path, path);
|
|
368
399
|
|
|
369
400
|
sample->audio = audio;
|
|
401
|
+
audio->refs++;
|
|
370
402
|
live_sounds++;
|
|
371
403
|
return sample;
|
|
372
404
|
}
|
|
@@ -394,8 +426,10 @@ void rgame_sample_destroy(rgame_sample *sample) {
|
|
|
394
426
|
* same file hold their own, so the last one out frees it. */
|
|
395
427
|
ma_sound_uninit(&sample->decoded);
|
|
396
428
|
free(sample->path);
|
|
429
|
+
rgame_audio *audio = sample->audio;
|
|
397
430
|
free(sample);
|
|
398
431
|
live_sounds--;
|
|
432
|
+
audio_release(audio);
|
|
399
433
|
}
|
|
400
434
|
|
|
401
435
|
void rgame_sample_play(rgame_sample *sample) {
|
|
@@ -470,6 +504,7 @@ rgame_song *rgame_song_load(rgame_audio *audio, const char *path, char *err, siz
|
|
|
470
504
|
}
|
|
471
505
|
|
|
472
506
|
song->audio = audio;
|
|
507
|
+
audio->refs++;
|
|
473
508
|
live_sounds++;
|
|
474
509
|
return song;
|
|
475
510
|
}
|
|
@@ -480,8 +515,10 @@ void rgame_song_destroy(rgame_song *song) {
|
|
|
480
515
|
}
|
|
481
516
|
|
|
482
517
|
ma_sound_uninit(&song->sound);
|
|
518
|
+
rgame_audio *audio = song->audio;
|
|
483
519
|
free(song);
|
|
484
520
|
live_sounds--;
|
|
521
|
+
audio_release(audio);
|
|
485
522
|
}
|
|
486
523
|
|
|
487
524
|
void rgame_song_play(rgame_song *song, int looping) {
|
data/ext/rgame_core/example.rb
CHANGED
|
@@ -1,30 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Manual/visual smoke test for the core extension (layer 3 — the real
|
|
4
|
-
# SDL/GL path). Build it first, then run this from the project root:
|
|
5
|
-
#
|
|
6
|
-
# make ext-core
|
|
7
|
-
# ruby ext/rgame_core/example.rb
|
|
8
|
-
#
|
|
9
|
-
# It opens a window with one of each drawing primitive in it. Arrow keys (or a
|
|
10
|
-
# controller's dpad/stick) move the marker; Escape or closing the window quits.
|
|
11
|
-
# This is the Ruby-side mirror of src/main.c, and it exists to exercise every
|
|
12
|
-
# callback and every draw call the engine offers.
|
|
13
|
-
#
|
|
14
|
-
# Pass a sound file (Ogg Vorbis or WAV) to check audio as well:
|
|
15
|
-
#
|
|
16
|
-
# ruby ext/rgame_core/example.rb assets/theme.ogg
|
|
17
|
-
#
|
|
18
|
-
# Space plays it as a one-shot sample, Return starts and stops it as looping
|
|
19
|
-
# music. There is no asset here to default to, and that is on purpose: this is
|
|
20
|
-
# the only place a *real* sound device is driven — every automated test runs
|
|
21
|
-
# against a null or offline one — so what it is worth checking is your own
|
|
22
|
-
# file, out of your own speakers.
|
|
23
|
-
#
|
|
24
|
-
# The load path points at lib/, not at this directory: `make ext-core`
|
|
25
|
-
# copies the built core_ext.so to lib/rgame/, so this runs against exactly
|
|
26
|
-
# the layout a user of the library would see.
|
|
27
|
-
|
|
28
3
|
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
29
4
|
require 'rgame'
|
|
30
5
|
require 'rgame/core'
|
|
@@ -41,8 +16,6 @@ class Example < RGame::Core::App
|
|
|
41
16
|
YELLOW = Color.new(224, 192, 64)
|
|
42
17
|
TRANSLUCENT_WHITE = Color.new(255, 255, 255, 128)
|
|
43
18
|
|
|
44
|
-
# Frozen and chosen between rather than built: #draw runs sixty times a
|
|
45
|
-
# second, and a string per frame is a garbage collection waiting to happen.
|
|
46
19
|
AUDIO_HINT_NONE = 'audio: pass a sound file on the command line to try it'
|
|
47
20
|
AUDIO_HINT_STOPPED = 'audio: Space plays a sample, Return starts the music'
|
|
48
21
|
AUDIO_HINT_PLAYING = 'audio: Space plays a sample, Return stops the music'
|
|
@@ -62,10 +35,6 @@ class Example < RGame::Core::App
|
|
|
62
35
|
@baked = nil
|
|
63
36
|
@device = Controls::KEYBOARD
|
|
64
37
|
|
|
65
|
-
# The device is opened whether or not there is anything to play through it,
|
|
66
|
-
# so that starting with no sound card takes the same path as starting with
|
|
67
|
-
# one. Nothing here is tied to the window: audio has no GL context and
|
|
68
|
-
# survives one being recreated.
|
|
69
38
|
@audio = RGame::Core::Audio.new
|
|
70
39
|
puts "audio backend: #{@audio.backend}"
|
|
71
40
|
return unless sound_path
|
|
@@ -78,8 +47,6 @@ class Example < RGame::Core::App
|
|
|
78
47
|
# here rather than per tick is the pattern the engine is shaped around — one
|
|
79
48
|
# sample per frame, reused by however many catch-up ticks follow.
|
|
80
49
|
def frame_begin
|
|
81
|
-
# Poll rather than track: Gamepad answers straight from the engine, so
|
|
82
|
-
# there is no local copy of the connection state to keep in step.
|
|
83
50
|
@device = @pads.connected?(0) ? @pads.device(0) : Controls::KEYBOARD
|
|
84
51
|
end
|
|
85
52
|
|
|
@@ -93,7 +60,6 @@ class Example < RGame::Core::App
|
|
|
93
60
|
@cursor_y -= speed if held?(Controls::KEY_UP, Controls::PAD_DPAD_UP)
|
|
94
61
|
@cursor_y += speed if held?(Controls::KEY_DOWN, Controls::PAD_DPAD_DOWN)
|
|
95
62
|
|
|
96
|
-
# Analog sticks are additive on top of the dpad; the keyboard reads 0.0.
|
|
97
63
|
@cursor_x += @input.axis(Controls::AXIS_LEFT_X, device: @device) * speed
|
|
98
64
|
@cursor_y += @input.axis(Controls::AXIS_LEFT_Y, device: @device) * speed
|
|
99
65
|
end
|
|
@@ -115,35 +81,22 @@ class Example < RGame::Core::App
|
|
|
115
81
|
def draw
|
|
116
82
|
r = @renderer
|
|
117
83
|
|
|
118
|
-
# Baked on the first frame and replayed after that: forty rectangles cost
|
|
119
|
-
# one call per frame instead of forty. A real game bakes its tile layers
|
|
120
|
-
# this way. Recording has to happen inside #draw, which is why it is here
|
|
121
|
-
# rather than in initialize.
|
|
122
84
|
@baked ||= r.record do
|
|
123
85
|
40.times { |i| r.rect(i * 18, 0, 12, 12, color: GREEN, z: 0) }
|
|
124
86
|
end
|
|
125
|
-
# Scrolls with the same value the spinning square turns by, so it is
|
|
126
|
-
# visibly the *replay* moving and not a rebake.
|
|
127
87
|
@baked.draw((@spin % 18) - 18, 560)
|
|
128
88
|
r.rect(40, 40, 160, 100, color: RED, z: 0)
|
|
129
|
-
# Higher z wins wherever they overlap, whatever order the calls came in.
|
|
130
89
|
r.rect(120, 90, 160, 100, color: TRANSLUCENT_WHITE, z: 1)
|
|
131
90
|
r.triangle(400, 40, 480, 180, 320, 180, color: GREEN, z: 0)
|
|
132
91
|
r.circle(620, 110, 70, color: BLUE, z: 0)
|
|
133
92
|
r.line(40, 240, 760, 240, thickness: 6, color: YELLOW, z: 0)
|
|
134
93
|
|
|
135
|
-
# The transform stack: a square turning about its own centre.
|
|
136
94
|
r.rotated(@spin, 400, 380) { r.rect(340, 320, 120, 120, color: GREEN, z: 0) }
|
|
137
95
|
|
|
138
|
-
# The clip stack: the rectangle is twice the size of what shows.
|
|
139
96
|
r.clipped(60, 480, 200, 80) { r.rect(60, 440, 400, 160, color: RED, z: 0) }
|
|
140
97
|
|
|
141
|
-
# The cursor the arrow keys move.
|
|
142
98
|
r.debug_box(@cursor_x - 8, @cursor_y - 8, 16, 16)
|
|
143
99
|
|
|
144
|
-
# Text, including accents and punctuation the shipped font has to cover, so
|
|
145
|
-
# a look at the window checks kerning and coverage at once. The second line
|
|
146
|
-
# is centred using text_width, which is what a UI actually does with it.
|
|
147
100
|
r.text('rgame — Grüße, œuvre, 5 €', 40, 270, color: YELLOW)
|
|
148
101
|
label = format('%d fps', fps)
|
|
149
102
|
r.text(label, 400 - (r.text_width(label) / 2), 270 + r.text_height)
|
|
@@ -155,8 +108,6 @@ class Example < RGame::Core::App
|
|
|
155
108
|
def button_down(id)
|
|
156
109
|
case id
|
|
157
110
|
when Controls::KEY_ESCAPE then close
|
|
158
|
-
# Held down, this is the overlap check: each press is another voice rather
|
|
159
|
-
# than a restart, so a fast run of them should pile up rather than stutter.
|
|
160
111
|
when Controls::KEY_SPACE then @sample&.play
|
|
161
112
|
when Controls::KEY_RETURN then toggle_music
|
|
162
113
|
end
|
data/ext/rgame_core/extconf.rb
CHANGED
|
@@ -1,117 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# extconf.rb — run by `ruby extconf.rb` to generate a Makefile via mkmf.
|
|
4
|
-
#
|
|
5
|
-
# mkmf is Ruby's "make makefile" library. It probes the system (headers,
|
|
6
|
-
# libraries, pkg-config) and writes a Makefile that knows how to compile a
|
|
7
|
-
# loadable extension (`core_ext.so`) against the running Ruby's headers.
|
|
8
|
-
# This is the same tool that `gem install` uses under the hood, which is why the
|
|
9
|
-
# root Makefile was written to look like what mkmf emits — the mental model
|
|
10
|
-
# carries straight over.
|
|
11
|
-
#
|
|
12
|
-
# This is the SDL/OpenGL half of the project: everything under RGame::Core.
|
|
13
|
-
# The graphics-free half lives in ../rgame_util (RGame::Util) and links none of
|
|
14
|
-
# these libraries.
|
|
15
|
-
|
|
16
3
|
require 'mkmf'
|
|
17
4
|
|
|
18
|
-
# The engine sources live in this directory rather than a top-level src/:
|
|
19
|
-
# `gem install` unpacks the gem and runs this script, and an extension must be
|
|
20
|
-
# buildable from its own directory.
|
|
21
|
-
#
|
|
22
|
-
# They are grouped into subdirectories by subsystem (app, graphics, text, input,
|
|
23
|
-
# audio) plus ruby/ for the Ruby-facing glue and vendor/ for third-party code.
|
|
24
|
-
# mkmf's default is to compile every .c in the extension's *own* directory and
|
|
25
|
-
# nothing deeper, so the source list has to be spelled out — which is what
|
|
26
|
-
# $srcs and $VPATH below do.
|
|
27
|
-
#
|
|
28
|
-
# $srcs — what to compile. mkmf names each object after the source's
|
|
29
|
-
# *basename*, so the .o files land flat in this directory no matter
|
|
30
|
-
# how deep the .c was. Basenames therefore have to be unique across
|
|
31
|
-
# the subdirectories; mkmf aborts with "source files duplication" if
|
|
32
|
-
# they ever aren't, so that rule enforces itself.
|
|
33
|
-
# $VPATH — where make looks for a source when a rule names it by basename.
|
|
34
|
-
# The generic `.c.o` rule mkmf emits does exactly that, so without
|
|
35
|
-
# this every object would come up as a missing prerequisite.
|
|
36
|
-
#
|
|
37
|
-
# vendor/ contributes only its <name>_impl.c translation units. The libraries
|
|
38
|
-
# themselves are #included *by* those files (stb_vorbis even ships as a .c), so
|
|
39
|
-
# compiling them separately would duplicate every symbol in them.
|
|
40
5
|
SOURCE_DIRS = %w[app graphics text input audio ruby].freeze
|
|
41
6
|
|
|
42
|
-
# Dir.glob sorts its results, so the object list is the same on every machine.
|
|
43
7
|
$srcs = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.c") } +
|
|
44
8
|
Dir.glob("#{$srcdir}/vendor/*_impl.c")
|
|
45
9
|
|
|
46
10
|
(SOURCE_DIRS + %w[vendor]).each { |dir| $VPATH << "$(srcdir)/#{dir}" }
|
|
47
11
|
|
|
48
|
-
# The extension directory itself, so a cross-subsystem include names the folder
|
|
49
|
-
# it comes from: graphics/canvas.c says `#include "graphics/clip.h"`, and text/
|
|
50
|
-
# reaching into graphics/ is visible in the source rather than hidden in a
|
|
51
|
-
# search path. It is also what resolves `#include "vendor/stb_image.h"`.
|
|
52
|
-
# $(srcdir) stays literal here — make expands it, not Ruby.
|
|
53
12
|
$INCFLAGS << ' -I$(srcdir)'
|
|
54
13
|
|
|
55
|
-
# Public API header (include/rgame/core.h), so `#include "rgame/core.h"`
|
|
56
|
-
# resolves.
|
|
57
14
|
$INCFLAGS << ' -I$(srcdir)/include'
|
|
58
15
|
|
|
59
|
-
# RGame::Util's colour header, for the RGBA byte order the renderer writes into
|
|
60
|
-
# every vertex. It is header-only (static inline), so this creates no link
|
|
61
|
-
# dependency between the two extensions — they stay separate .so files. Both
|
|
62
|
-
# directories ship together in the gem, so the relative path holds there too.
|
|
63
16
|
$INCFLAGS << ' -I$(srcdir)/../rgame_util'
|
|
64
17
|
|
|
65
|
-
# SDL2: pkg_config("sdl2") shells out to pkg-config and folds the resulting
|
|
66
|
-
# cflags and libs into mkmf's globals ($CFLAGS/$libs) for us. Returns nil if
|
|
67
|
-
# SDL2 isn't found, so we can fail with a clear message instead of a confusing
|
|
68
|
-
# later compile error.
|
|
69
18
|
abort 'SDL2 not found (pkg-config --exists sdl2 failed). Install libsdl2-dev.' unless pkg_config('sdl2')
|
|
70
19
|
|
|
71
|
-
# The GL header, probed as the sources actually spell it: SDL's own
|
|
72
|
-
# <SDL2/SDL_opengl.h>, which resolves to GL/gl.h, OpenGL/gl.h or the Windows SDK's
|
|
73
|
-
# per platform. Probing GL/gl.h directly would ask the wrong question off Linux.
|
|
74
|
-
#
|
|
75
|
-
# It needs SDL's cflags, which pkg_config folded into $CFLAGS just above — this
|
|
76
|
-
# is also what makes the probe a real check of the include path rather than of
|
|
77
|
-
# the filesystem.
|
|
78
20
|
abort 'SDL2 OpenGL header not found (SDL2/SDL_opengl.h). Install libsdl2-dev.' unless have_header('SDL2/SDL_opengl.h')
|
|
79
21
|
|
|
80
|
-
# Three platforms, three different ways to link the same OpenGL — the one place
|
|
81
|
-
# in this file that has to branch, and the mirror of the root Makefile's own
|
|
82
|
-
# branch (see its GL_LIBS). The engine calls only GL 1.1 entry points, which all
|
|
83
|
-
# three expose directly, so no extension loader (GLAD/GLEW) is involved anywhere;
|
|
84
|
-
# see CLAUDE.md's Conventions on why the legacy profile is deliberate.
|
|
85
|
-
#
|
|
86
|
-
# Probed rather than assumed, because this script is what runs on someone else's
|
|
87
|
-
# machine during `gem install` and a missing OpenGL is a routine thing to hit
|
|
88
|
-
# there. Without a probe it surfaces as a linker error about an undefined
|
|
89
|
-
# `glClear` at the end of a long build; with one, the install stops on a
|
|
90
|
-
# sentence naming what to install. Same reasoning as the SDL2 abort above — and
|
|
91
|
-
# the message is per-platform, since "install libgl1-mesa-dev" is useless advice
|
|
92
|
-
# on a Mac.
|
|
93
|
-
#
|
|
94
|
-
# `have_library` appends `-lGL`/`-lopengl32` to $libs on success; `have_framework`
|
|
95
|
-
# appends `-framework OpenGL` to $LDFLAGS. Both end up on the link line.
|
|
96
22
|
case RbConfig::CONFIG['host_os']
|
|
97
23
|
when /darwin/
|
|
98
|
-
# macOS ships OpenGL as a framework rather than a library, so there is no
|
|
99
|
-
# `-lGL` to find. It is part of the OS — a failure here means the toolchain
|
|
100
|
-
# is missing, not the graphics stack.
|
|
101
24
|
unless have_framework('OpenGL')
|
|
102
25
|
abort 'OpenGL framework not found. It ships with macOS, so this usually means ' \
|
|
103
26
|
'the command line tools are missing: xcode-select --install'
|
|
104
27
|
end
|
|
105
28
|
when /mingw|mswin|cygwin/
|
|
106
|
-
# Windows' OpenGL is an OS component: opengl32.dll is in system32 on every
|
|
107
|
-
# installation, and the import library comes with the toolchain. Nothing to
|
|
108
|
-
# install from a graphics vendor.
|
|
109
29
|
unless have_library('opengl32', 'glClear')
|
|
110
30
|
abort 'opengl32 not found. It is part of Windows itself, so this means an ' \
|
|
111
31
|
'incomplete MSYS2 toolchain: pacman -S mingw-w64-ucrt-x86_64-gcc'
|
|
112
32
|
end
|
|
113
33
|
else
|
|
114
|
-
# Linux and the BSDs.
|
|
115
34
|
unless have_library('GL', 'glClear')
|
|
116
35
|
abort 'OpenGL library not found (-lGL). Install the OpenGL development package: ' \
|
|
117
36
|
'libgl1-mesa-dev on Debian/Ubuntu, mesa-libGL-devel on Fedora.'
|
|
@@ -120,45 +39,13 @@ end
|
|
|
120
39
|
|
|
121
40
|
$libs = append_library($libs, 'm')
|
|
122
41
|
|
|
123
|
-
# miniaudio's threading, and the dlopen it uses to find ALSA or PulseAudio at
|
|
124
|
-
# runtime — the property that makes audio cost no new system dependency.
|
|
125
|
-
#
|
|
126
|
-
# Probed rather than branched, because unlike OpenGL above the *answer* differs
|
|
127
|
-
# per platform but the question does not: link them where they exist. On
|
|
128
|
-
# glibc both are system libraries nobody installs; macOS resolves `-lpthread`
|
|
129
|
-
# into libSystem and needs no `-ldl`; MSYS2 supplies winpthreads. Appending
|
|
130
|
-
# either unconditionally would break the link wherever it is absent.
|
|
131
42
|
$libs = append_library($libs, 'pthread') if have_library('pthread')
|
|
132
43
|
$libs = append_library($libs, 'dl') if have_library('dl')
|
|
133
44
|
|
|
134
|
-
# Match the project's C standard and warning flags. Note: gnu17, not plain
|
|
135
|
-
# c17 — Ruby's headers occasionally lean on GNU extensions, and gnu17 is a
|
|
136
|
-
# superset of c17 so core.c (which targets c17) still compiles fine.
|
|
137
45
|
$CFLAGS << ' -std=gnu17 -Wall -Wextra'
|
|
138
46
|
|
|
139
|
-
# Emits the Makefile. "rgame/core_ext" -> loaded via
|
|
140
|
-
# `require "rgame/core_ext"`, entry point Init_core_ext (the basename)
|
|
141
|
-
# in core_ext.c. Namespacing it under rgame/ mirrors rgame/util_ext, keeps
|
|
142
|
-
# both extensions off the top of the load path, and — importantly — leaves the
|
|
143
|
-
# bare name "rgame" to lib/rgame.rb, which is the pure-Ruby entry point.
|
|
144
47
|
create_makefile('rgame/core_ext')
|
|
145
48
|
|
|
146
|
-
# The vendored implementations, compiled with warnings off (see
|
|
147
|
-
# vendor/README.md). mkmf has no per-file flag setting, so the rules are
|
|
148
|
-
# appended to the Makefile it just wrote. An explicit rule for a specific
|
|
149
|
-
# target beats mkmf's generic .c.o suffix rule, so these are what get used for
|
|
150
|
-
# those objects and nothing else.
|
|
151
|
-
#
|
|
152
|
-
# Written from one list rather than one block per library: a second hand-copied
|
|
153
|
-
# rule is how the first one drifts. They are explicit rules rather than a `%`
|
|
154
|
-
# pattern because mkmf's Makefiles are meant to work with whatever `make` the
|
|
155
|
-
# platform has, and pattern rules are a GNU extension.
|
|
156
|
-
#
|
|
157
|
-
# `-w` comes last on the command line and switches every warning back off,
|
|
158
|
-
# which is simpler and more robust than trying to subtract -Wall -Wextra from
|
|
159
|
-
# $(CFLAGS) — everything else about how the extension compiles stays identical.
|
|
160
|
-
# Each vendored library's implementation TU (<name>_impl.c) and the file it
|
|
161
|
-
# instantiates. stb_vorbis is the odd one out: it ships as a .c, not a .h.
|
|
162
49
|
VENDORED = {
|
|
163
50
|
'stb_image' => 'stb_image.h',
|
|
164
51
|
'stb_truetype' => 'stb_truetype.h',
|
|
@@ -176,18 +63,6 @@ File.open('Makefile', 'a') do |makefile|
|
|
|
176
63
|
MAKE
|
|
177
64
|
end
|
|
178
65
|
|
|
179
|
-
# Rebuild everything when any of our headers changes.
|
|
180
|
-
#
|
|
181
|
-
# mkmf emits `$(OBJS): $(HDRS)` for this, but it builds HDRS by globbing the
|
|
182
|
-
# extension's own directory only — with the headers a level down in
|
|
183
|
-
# graphics/, text/ and the rest, HDRS comes out empty and editing a header
|
|
184
|
-
# rebuilds *nothing*. That failure is silent: the build succeeds and links
|
|
185
|
-
# objects compiled against the previous version of the struct.
|
|
186
|
-
#
|
|
187
|
-
# One coarse dependency rather than a real per-object dependency scan, for the
|
|
188
|
-
# same reason the root Makefile treats the vendored sources as one list: this
|
|
189
|
-
# is a small project where a full rebuild costs seconds, and a hand-maintained
|
|
190
|
-
# dependency graph is a thing to get quietly wrong.
|
|
191
66
|
headers = SOURCE_DIRS.flat_map { |dir| Dir.glob("#{$srcdir}/#{dir}/*.h") }
|
|
192
67
|
.map { |path| "$(srcdir)/#{path.delete_prefix("#{$srcdir}/")}" }
|
|
193
68
|
|
|
@@ -19,8 +19,19 @@ extern "C" {
|
|
|
19
19
|
|
|
20
20
|
typedef struct rgame_app rgame_app;
|
|
21
21
|
|
|
22
|
-
/*
|
|
23
|
-
|
|
22
|
+
/*
|
|
23
|
+
* Creates the window, GL context and internal state. Returns NULL on failure.
|
|
24
|
+
*
|
|
25
|
+
* `fullscreen` non-zero opens the window fullscreen straight away, rather than
|
|
26
|
+
* opening it windowed and switching after. The difference is visible: a switch
|
|
27
|
+
* made after creation shows one windowed frame first, which is the flash a
|
|
28
|
+
* player sees when a game starts up wrong.
|
|
29
|
+
*
|
|
30
|
+
* `width` and `height` still matter when starting fullscreen — they are the
|
|
31
|
+
* size the window returns to when it leaves fullscreen, and the size SDL keeps
|
|
32
|
+
* for it in the meantime.
|
|
33
|
+
*/
|
|
34
|
+
rgame_app *rgame_app_create(int width, int height, const char *title, int fullscreen);
|
|
24
35
|
|
|
25
36
|
/* Destroys the GL context/window and frees the app. Safe to call with NULL. */
|
|
26
37
|
void rgame_app_destroy(rgame_app *app);
|
|
@@ -272,6 +283,19 @@ void rgame_app_close(rgame_app *app);
|
|
|
272
283
|
int rgame_app_width(const rgame_app *app);
|
|
273
284
|
int rgame_app_height(const rgame_app *app);
|
|
274
285
|
|
|
286
|
+
/*
|
|
287
|
+
* Fullscreen, toggled at any time. This is *desktop* fullscreen: the window
|
|
288
|
+
* takes the whole screen at the screen's own resolution, rather than asking the
|
|
289
|
+
* display to change mode. That makes the switch instant and reversible, costs
|
|
290
|
+
* no mode list to choose from, and leaves other windows where they were.
|
|
291
|
+
*
|
|
292
|
+
* Switching resizes the window, so the `resize` callback fires with the new
|
|
293
|
+
* size — the same path a user dragging a window edge takes. Nothing else needs
|
|
294
|
+
* telling.
|
|
295
|
+
*/
|
|
296
|
+
void rgame_app_set_fullscreen(rgame_app *app, int fullscreen);
|
|
297
|
+
int rgame_app_fullscreen(const rgame_app *app);
|
|
298
|
+
|
|
275
299
|
/* Window title. The returned string is owned by the window, not the caller. */
|
|
276
300
|
const char *rgame_app_title(const rgame_app *app);
|
|
277
301
|
void rgame_app_set_title(rgame_app *app, const char *title);
|
|
@@ -317,6 +341,18 @@ unsigned int rgame_app_ticks_ms(const rgame_app *app);
|
|
|
317
341
|
/* Most recent frames-per-second reading (updated ~once per second). */
|
|
318
342
|
double rgame_app_fps(const rgame_app *app);
|
|
319
343
|
|
|
344
|
+
/*
|
|
345
|
+
* The user's preferred locales, most preferred first, written into `out` as
|
|
346
|
+
* "de-AT,en": a language, a hyphen and a country when the OS names one, and
|
|
347
|
+
* no trailing comma. Needs no app.
|
|
348
|
+
*
|
|
349
|
+
* Returns the length of the whole list, not counting the NUL, like snprintf:
|
|
350
|
+
* a result >= `capacity` means `out` holds a truncated prefix, and a buffer of
|
|
351
|
+
* result + 1 bytes fits it. `out` is always NUL-terminated when `capacity` is
|
|
352
|
+
* non-zero. Returns 0, with `out` empty, when the OS reports none.
|
|
353
|
+
*/
|
|
354
|
+
size_t rgame_preferred_locales(char *out, size_t capacity);
|
|
355
|
+
|
|
320
356
|
/*
|
|
321
357
|
* ---------------------------------------------------------------------------
|
|
322
358
|
* Images
|
|
@@ -114,10 +114,15 @@ static VALUE audio_s_debug_live_sounds(VALUE klass) {
|
|
|
114
114
|
* ------------------------------------------------------------------------- */
|
|
115
115
|
|
|
116
116
|
/*
|
|
117
|
-
* Both wrap a C handle plus the Ruby Audio it came from. Marking the device
|
|
118
|
-
*
|
|
119
|
-
* device's mixer, and freeing the mixer out from
|
|
120
|
-
* an unpredictable moment.
|
|
117
|
+
* Both wrap a C handle plus the Ruby Audio it came from. Marking the device
|
|
118
|
+
* keeps it alive for as long as any sound made from it is *reachable*: the
|
|
119
|
+
* sound is a voice inside that device's mixer, and freeing the mixer out from
|
|
120
|
+
* under a live sound would be a crash at an unpredictable moment.
|
|
121
|
+
*
|
|
122
|
+
* It is only half the guarantee, and the half it leaves out is worth knowing.
|
|
123
|
+
* When a sound and its device become garbage in the same collection, marking
|
|
124
|
+
* has nothing left to say and the sweep frees them in whatever order it reaches
|
|
125
|
+
* them. audio.c refcounts the device for that case; see audio_release there.
|
|
121
126
|
*/
|
|
122
127
|
typedef struct {
|
|
123
128
|
void *handle; /* rgame_sample * or rgame_song * */
|
|
@@ -312,7 +317,7 @@ void rgame_init_audio(VALUE mCore) {
|
|
|
312
317
|
rb_define_class_under(cSong, "LoadError", rb_eStandardError);
|
|
313
318
|
rb_define_alloc_func(cSong, song_alloc);
|
|
314
319
|
rb_define_method(cSong, "initialize", song_initialize, 2);
|
|
315
|
-
|
|
320
|
+
rb_define_private_method(cSong, "play_looping", song_play, 1);
|
|
316
321
|
rb_define_method(cSong, "stop", song_stop, 0);
|
|
317
322
|
rb_define_method(cSong, "playing?", song_playing_p, 0);
|
|
318
323
|
rb_define_method(cSong, "looping?", song_looping_p, 0);
|
|
@@ -61,6 +61,7 @@ static ID id_width;
|
|
|
61
61
|
static ID id_height;
|
|
62
62
|
static ID id_caption;
|
|
63
63
|
static ID id_media_root;
|
|
64
|
+
static ID id_fullscreen;
|
|
64
65
|
|
|
65
66
|
/* ------------------------------------------------------------------------- *
|
|
66
67
|
* rgame_app lifetime, wrapped as a Ruby object
|
|
@@ -110,8 +111,9 @@ static VALUE app_alloc(VALUE klass) {
|
|
|
110
111
|
return TypedData_Wrap_Struct(klass, &app_data_type, NULL);
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
/* App.new(width:, height:, caption:) — keyword
|
|
114
|
-
* subclass's own initialize will forward to via
|
|
114
|
+
/* App.new(width:, height:, caption:, media_root:, fullscreen:) — keyword
|
|
115
|
+
* arguments, matching the shape a subclass's own initialize will forward to via
|
|
116
|
+
* super. The first three are required; the last two have defaults. */
|
|
115
117
|
static VALUE app_initialize(int argc, VALUE *argv, VALUE self) {
|
|
116
118
|
VALUE opts = Qnil;
|
|
117
119
|
rb_scan_args(argc, argv, "0:", &opts); /* no positional args, keywords only */
|
|
@@ -119,14 +121,19 @@ static VALUE app_initialize(int argc, VALUE *argv, VALUE self) {
|
|
|
119
121
|
rb_raise(rb_eArgError, "missing keywords: :width, :height, :caption");
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
const ID keys[
|
|
123
|
-
VALUE values[
|
|
124
|
-
/* 3 required,
|
|
124
|
+
const ID keys[5] = { id_width, id_height, id_caption, id_media_root, id_fullscreen };
|
|
125
|
+
VALUE values[5];
|
|
126
|
+
/* 3 required, 2 optional: raises on a missing or unknown keyword. An absent
|
|
125
127
|
* optional comes back as Qundef. */
|
|
126
|
-
rb_get_kwargs(opts, keys, 3,
|
|
128
|
+
rb_get_kwargs(opts, keys, 3, 2, values);
|
|
127
129
|
|
|
130
|
+
/* Passed to create rather than set afterwards, so a game that starts
|
|
131
|
+
* fullscreen never shows a windowed frame first. RTEST treats both nil and
|
|
132
|
+
* false as off, so `fullscreen: settings[:fullscreen]` works with a key that
|
|
133
|
+
* is missing. */
|
|
128
134
|
rgame_app *app = rgame_app_create(NUM2INT(values[0]), NUM2INT(values[1]),
|
|
129
|
-
StringValueCStr(values[2])
|
|
135
|
+
StringValueCStr(values[2]),
|
|
136
|
+
values[4] != Qundef && RTEST(values[4]));
|
|
130
137
|
if (!app) {
|
|
131
138
|
rb_raise(rb_eRuntimeError, "failed to create rgame app");
|
|
132
139
|
}
|
|
@@ -363,6 +370,18 @@ static VALUE app_set_caption(VALUE self, VALUE title) {
|
|
|
363
370
|
return title;
|
|
364
371
|
}
|
|
365
372
|
|
|
373
|
+
static VALUE app_fullscreen_p(VALUE self) {
|
|
374
|
+
return rgame_app_fullscreen(rgame_app_unwrap(self)) ? Qtrue : Qfalse;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* Returns what it was given, as an assignment must in Ruby — so
|
|
378
|
+
* `app.fullscreen = settings.fullscreen?` reads back the value that was set,
|
|
379
|
+
* not the window's state. Read #fullscreen? for that. */
|
|
380
|
+
static VALUE app_set_fullscreen(VALUE self, VALUE on) {
|
|
381
|
+
rgame_app_set_fullscreen(rgame_app_unwrap(self), RTEST(on));
|
|
382
|
+
return on;
|
|
383
|
+
}
|
|
384
|
+
|
|
366
385
|
/*
|
|
367
386
|
* Raw input queries. These take the numeric device and button/axis ids from
|
|
368
387
|
* RGame::Core::Input rather than symbolic action names — turning `:fire` into
|
|
@@ -482,6 +501,7 @@ void Init_core_ext(void) {
|
|
|
482
501
|
id_height = rb_intern("height");
|
|
483
502
|
id_caption = rb_intern("caption");
|
|
484
503
|
id_media_root = rb_intern("media_root");
|
|
504
|
+
id_fullscreen = rb_intern("fullscreen");
|
|
485
505
|
|
|
486
506
|
/*
|
|
487
507
|
* rb_define_module is idempotent — it returns the existing RGame if some
|
|
@@ -501,6 +521,8 @@ void Init_core_ext(void) {
|
|
|
501
521
|
rb_define_method(cApp, "height", app_height, 0);
|
|
502
522
|
rb_define_method(cApp, "caption", app_caption, 0);
|
|
503
523
|
rb_define_method(cApp, "caption=", app_set_caption, 1);
|
|
524
|
+
rb_define_method(cApp, "fullscreen?", app_fullscreen_p, 0);
|
|
525
|
+
rb_define_method(cApp, "fullscreen=", app_set_fullscreen, 1);
|
|
504
526
|
rb_define_method(cApp, "ticks_ms", app_ticks_ms, 0);
|
|
505
527
|
rb_define_method(cApp, "fps", app_fps, 0);
|
|
506
528
|
rb_define_method(cApp, "input_down?", app_input_down_p, 2);
|
|
@@ -526,4 +548,5 @@ void Init_core_ext(void) {
|
|
|
526
548
|
rgame_init_recording(mCore);
|
|
527
549
|
rgame_init_font(mCore);
|
|
528
550
|
rgame_init_audio(mCore);
|
|
551
|
+
rgame_init_locale(mCore);
|
|
529
552
|
}
|
|
@@ -21,6 +21,9 @@ void rgame_init_renderer(VALUE mCore);
|
|
|
21
21
|
void rgame_init_recording(VALUE mCore);
|
|
22
22
|
void rgame_init_font(VALUE mCore);
|
|
23
23
|
|
|
24
|
+
/* Not a class: RGame::Core.preferred_locales, a module function. */
|
|
25
|
+
void rgame_init_locale(VALUE mCore);
|
|
26
|
+
|
|
24
27
|
/* Defines Audio, Sample and Song together — see audio_ext.c for why those three
|
|
25
28
|
* share a file when everything else here does not. */
|
|
26
29
|
void rgame_init_audio(VALUE mCore);
|