rgame 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54caa0772adc1eb3211edbd694540c196be0018d6482089447d727f5c871e19a
4
- data.tar.gz: 16b3d1de8baefc2579bbf5a89fb7b42041d174567b6c7059d5c967f2598621f5
3
+ metadata.gz: 5e39a6f27c3dea363873399361aa98c3b24c9de748f54ae635f93d9fef82dcfc
4
+ data.tar.gz: 89933482a0d7d99167ad7b725cf5eb0d8c503000bf4596893cf597e44e0ec6b4
5
5
  SHA512:
6
- metadata.gz: ab441100a4d86c34d93efed615b350a7d65ad819d3b7929e868e709c6a9790b9c78a956b9860632e62750f0880ca9656a1f7f6c98cc3df6c1b19f7d229e40919
7
- data.tar.gz: e98b7a96d21700bf0533f0155be99ffc9f480e35fda2375aaee4b8b2a7bb65b871d0a70a1f2486873b32e28a83aaa4abfc91a14991226153f57959b0d62237b4
6
+ metadata.gz: edfe6650fc2f6e36988ed687ca7685e049b2e341f685d534e4c2a535adc4549441676ebf3126857c340bb846191623e3fd94edfa6d0d132639c3f0b349eff374
7
+ data.tar.gz: 64ba08460681639d8c53923874844527348b4a1490ff85085060051e3c2f66d9a33b959e846f29a1ca5386f542c55497982bdc84936fee91e200b51eaef9e71e
data/CHANGELOG.md CHANGED
@@ -12,6 +12,27 @@ index, not the argument.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.4.0] - 2026-09-16
16
+
17
+ ### Added
18
+
19
+ - **`gem install rgame` needs no compiler and no SDL2 on a common desktop.**
20
+ Apple Silicon Macs, x86-64 Linux and 64-bit Windows get a gem whose two
21
+ extensions are already built, with SDL2 linked into them. Every other machine,
22
+ and every Ruby but 4.0, installs the source gem and compiles it as before.
23
+ - **A virtual gamepad for tests.** `RGame::Core::VirtualGamepad.new` plugs a
24
+ synthetic controller into a running `App`, which seats it and reads it like a
25
+ real pad; `set_button`, `set_axis` and `detach` drive it. See
26
+ [docs/api/input.md](docs/api/input.md#rgamecorevirtualgamepad).
27
+
28
+ ## [0.3.1] - 2026-09-15
29
+
30
+ ### Fixed
31
+
32
+ - `require "rgame"` failed under Bundler with `cannot load such file --
33
+ rexml/document`. The gem now declares `rexml` as a dependency, which Ruby 4.0
34
+ no longer loads without one.
35
+
15
36
  ## [0.3.0] - 2026-09-15
16
37
 
17
38
  ### Added
@@ -222,7 +243,9 @@ First release, and the first version that runs a game end to end.
222
243
  - **`RGame::Game`** — the entry point that wires the two halves together.
223
244
  - Examples: `14_asteroids`, `15_tiled_world`, `16_hello_world`.
224
245
 
225
- [Unreleased]: https://github.com/psuessenb/rgame/compare/v0.3.0...HEAD
246
+ [Unreleased]: https://github.com/psuessenb/rgame/compare/v0.4.0...HEAD
247
+ [0.4.0]: https://github.com/psuessenb/rgame/compare/v0.3.1...v0.4.0
248
+ [0.3.1]: https://github.com/psuessenb/rgame/compare/v0.3.0...v0.3.1
226
249
  [0.3.0]: https://github.com/psuessenb/rgame/compare/v0.2.0...v0.3.0
227
250
  [0.2.0]: https://github.com/psuessenb/rgame/compare/v0.1.0...v0.2.0
228
251
  [0.1.0]: https://github.com/psuessenb/rgame/releases/tag/v0.1.0
data/README.md CHANGED
@@ -36,9 +36,15 @@ bundle exec rspec # the game logic, headless — no window needed
36
36
  ruby main.rb # the game itself
37
37
  ```
38
38
 
39
- What it writes is small — a game class, a root node, a spec and the usual configuration — but it is laid out the way the engine wants to be used: one file loads SDL, everything else stays graphics-free and therefore testable with no display. [The `rgame` command](docs/api/cli.md) explains the layout and why it matters.
39
+ It's just a small skeleton — a game class, a root node, a spec and the usual configuration — but it is laid out the way the engine wants to be used: one file loads SDL, everything else stays graphics-free and therefore testable with no display. [The `rgame` command](docs/api/cli.md) explains the layout and the reasoning. This is also sets you up with a Rubocop configuration already geared towards RGame and game development in general.
40
40
 
41
- `gem install` compiles two C extensions, so the [requirements](#requirements) below have to be in place first. That's the one big hurdle at the moment: This gem ships source-only, so you have to compile a lot of C _on your machine_ to get this running.
41
+ **On a common desktop that first line needs nothing else** no compiler, no SDL2, no header files. `gem install rgame` fetches a gem whose two C extensions are already built, with SDL2 linked into them:
42
+
43
+ - macOS 11 or later on Apple Silicon
44
+ - x86-64 Linux with glibc 2.29 or later
45
+ - 64-bit Windows, on a RubyInstaller Ruby
46
+
47
+ All three need Ruby 4.0. Anywhere else — an Intel Mac, a Raspberry Pi, Ruby 4.1 — `gem install` falls back to the gem that ships the C and compiles it on your machine. That one needs [a compiler and SDL2](#building-from-source).
42
48
 
43
49
  ## Hello world
44
50
 
@@ -94,9 +100,11 @@ You can learn more about how it works in the [documentation](docs/api/README.md)
94
100
  | [localization](docs/api/examples.md#localization) | The same screen in two languages, switched and remembered |
95
101
  | [pathfinding](docs/api/examples.md#pathfinding) | Setting a target for an actor and let if find its way there |
96
102
 
97
- ## Requirements
103
+ ## Building from source
104
+
105
+ **This section is for two readers: anyone on a platform the binary gems miss, and anyone working on RGame itself.** Installing on one of the three platforms above needs none of it.
98
106
 
99
- At the moment this gem ships only source-code and no precompiled binaries, which unfortunately means you need to compile a bunch of C code on your locale machine. This _also_ means you need to install some system libaries and have header files present.
107
+ Compiling the C engine needs a compiler and the system libraries it links against. A source install of the gem needs the compiler, `pkg-config`, SDL2 and OpenGL from the list below. The rest is for running rgame's own suites.
100
108
 
101
109
  ### C engine
102
110
 
@@ -125,7 +133,7 @@ fixture and needs `libvorbisenc` to run.
125
133
  (Debian/Ubuntu). These are what `extconf.rb` compiles against.
126
134
  - **Bundler**, then `bundle install` for the dev/test gems (RSpec, RuboCop).
127
135
 
128
- Nothing else the engine has no runtime Ruby dependencies, and the `Gemfile` holds only development gems.
136
+ Nothing else. The engine's one runtime gem is `rexml`, which Bundler installs with it, and the `Gemfile` holds only development gems.
129
137
 
130
138
  All three platforms below are built and tested on every push by
131
139
  [CI](.github/workflows/ci.yml) - with that people have usually at home, so Apple Silicon and not Apple Intel, etc.
@@ -259,23 +267,35 @@ ruby ext/rgame_core/example.rb
259
267
  Both extensions and the Ruby layer ship as one gem, built from `rgame.gemspec` and published at [rubygems.org/gems/rgame](https://rubygems.org/gems/rgame):
260
268
 
261
269
  ```
262
- gem install rgame # from RubyGems; compiles both extensions here
270
+ gem install rgame # from RubyGems; compiles nothing on a covered platform
263
271
  ```
264
272
 
265
- Or from a checkout, which is the same gem built locally:
273
+ Or from a checkout, which builds the source gem locally:
266
274
 
267
275
  ```
268
276
  rake build # package into pkg/rgame-<version>.gem
269
277
  gem install pkg/rgame-*.gem # compiles both extensions on this machine
270
278
  ```
271
279
 
272
- `gem install` runs each `extconf.rb` and installs the resulting `.so` into the
273
- gem's own `lib/rgame/`, which is the same layout `make ext` produces in a
274
- checkout so `require "rgame"` and `require "rgame/core"` behave identically
275
- either way, including the guarantee that the first of those loads no graphics
276
- libraries. The system dependencies are the same ones the C engine needs
277
- (SDL2, OpenGL, pkg-config, a compiler); `extconf.rb` aborts with the package to
278
- install if one is missing, rather than failing later at the link step.
280
+ **RubyGems holds two kinds of gem for each version.** The source gem carries the
281
+ C and compiles on install. Three platform gems carry `core_ext` and `util_ext`
282
+ already built, with SDL2 linked statically into `core_ext`; they ship no `.c`,
283
+ no `extconf.rb` and declare no extensions, so they compile nothing. `gem
284
+ install` picks by platform and Ruby version on its own.
285
+
286
+ CI builds and publishes all four. `tools/platform_gem.rake` builds a platform
287
+ gem, `tools/check_platform_gem.rb` checks it against nine rules before it may be
288
+ published, and the `smoke` job installs it on a runner with no SDL2 and plays
289
+ the examples out of it.
290
+
291
+ Installing the source gem runs each `extconf.rb` and installs the resulting
292
+ `.so` into the gem's own `lib/rgame/`, which is the same layout `make ext`
293
+ produces in a checkout — so `require "rgame"` and `require "rgame/core"` behave
294
+ identically whichever gem you got, including the guarantee that the first of
295
+ those loads no graphics libraries. Its system dependencies are the ones the C
296
+ engine needs (SDL2, OpenGL, pkg-config, a compiler); `extconf.rb` aborts with
297
+ the package to install if one is missing, rather than failing later at the link
298
+ step.
279
299
 
280
300
  What ships is a glob over `lib/`, `ext/` and `docs/api/`, not a hand-written
281
301
  list: a new C source or a runtime asset dropped into either tree is packaged
data/docs/api/README.md CHANGED
@@ -11,7 +11,7 @@ or write C.
11
11
  | [The `rgame` command](cli.md) | `rgame new NAME` — starting a project, and the layout it gives you |
12
12
  | [App](app.md) | `RGame::Core::App` — the window and the frame loop |
13
13
  | [Game](game.md) | `RGame::Game` — the entry point that wires both halves together |
14
- | [Input](input.md) | `RGame::Core::Input`, `RGame::Util::Controls`, `RGame::Core::Gamepad` |
14
+ | [Input](input.md) | `RGame::Core::Input`, `RGame::Util::Controls`, `RGame::Core::Gamepad`, `RGame::Core::VirtualGamepad` |
15
15
  | [Drawing](drawing.md) | `RGame::Core::Renderer` — shapes, images, transforms, clipping, recordings |
16
16
  | [Images](images.md) | `RGame::Core::Image` — loading PNGs, subimages, tiles |
17
17
  | [Text](text.md) | `RGame::Core::Font` and `Renderer#text` |
@@ -67,8 +67,14 @@ The Core spec suite does this to load exactly one layer.
67
67
  `rgame/core` also loads `RGame::Util::Controls`, the input id vocabulary,
68
68
  because the input classes need it.
69
69
 
70
- `gem install rgame` compiles both extensions. In a checkout of the repository,
71
- compile them before you require anything:
70
+ **Your install either compiled both extensions or arrived with them built.** On
71
+ Apple Silicon macOS, x86-64 Linux and 64-bit Windows, `gem install rgame` fetches
72
+ a gem that already holds `core_ext` and `util_ext`, with SDL2 inside `core_ext`,
73
+ and compiles nothing. Every other machine gets the gem that ships the C and
74
+ builds both on install, against a system SDL2. Either way the two land in the
75
+ same place and the three requires above behave the same.
76
+
77
+ In a checkout of the repository, compile them before you require anything:
72
78
 
73
79
  ```
74
80
  make ext # builds both, copies them into lib/rgame/
@@ -82,7 +88,7 @@ and `Core`. `Engine` holds what a game is written in.
82
88
  | | `RGame::Util` | `RGame::Core` | `RGame::Engine` |
83
89
  |---|---|---|---|
84
90
  | Contains | shareable *values* — no window, no GPU, nothing to release | things owning a window, GPU or OS handle | game concepts: the scene graph a game is written in |
85
- | Classes | `Color`, `Tensor`, `Controls`, `Z`, `SolidGrid`, `RouteSearch`, `TileSweep`, `SaveFile` | `App`, `Input`, `Gamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `SpriteSheet`, `NineSlice`, `UiAtlas`, `TileMapRenderer`, `AssetManager` | `Node2D`, components, systems, signals, `TileMap`, `Player`, `InputMap`, `UI::Menu` |
91
+ | Classes | `Color`, `Tensor`, `Controls`, `Z`, `SolidGrid`, `RouteSearch`, `TileSweep`, `SaveFile` | `App`, `Input`, `Gamepad`, `VirtualGamepad`, `Image`, `Renderer`, `Recording`, `Font`, `Audio`, `SpriteSheet`, `NineSlice`, `UiAtlas`, `TileMapRenderer`, `AssetManager` | `Node2D`, components, systems, signals, `TileMap`, `Player`, `InputMap`, `UI::Menu` |
86
92
  | Loading it costs | nothing | SDL2 + OpenGL in your process | nothing |
87
93
 
88
94
  **A value goes in `Util`; only a handle-owner goes in `Core`.** A colour is a
data/docs/api/input.md CHANGED
@@ -436,3 +436,41 @@ end
436
436
 
437
437
  When a controller is unplugged mid-press, the engine clears its buttons and axes.
438
438
  A button held at that moment does not stay stuck down.
439
+
440
+ ## `RGame::Core::VirtualGamepad`
441
+
442
+ **`VirtualGamepad` plugs a synthetic controller into a running `App`, for tests
443
+ that need the gamepad path with no hardware.** SDL fabricates the device inside
444
+ the process. The `App` seats it and calls `gamepad_connected` on its next frame,
445
+ as it would for a real pad, and `Input` reads its buttons and axes. It serves
446
+ tests, not gameplay; rgame's own Core suite drives its gamepad and hot-plug specs
447
+ through it.
448
+
449
+ ```ruby
450
+ pad = RGame::Core::VirtualGamepad.new # raises RuntimeError unless an App is open
451
+
452
+ pad.set_button(0, true) # button 0, which Input reads as Controls::PAD_A
453
+ pad.set_axis(0, -32_768) # axis 0 is Controls::AXIS_LEFT_X, fully left
454
+ pad.button_down?(0) # the raw button, before controller mapping
455
+ pad.game_controller? # does SDL have a mapping for it?
456
+ pad.attached? # is it still a live device?
457
+ pad.detach # the App calls gamepad_disconnected
458
+
459
+ RGame::Core::VirtualGamepad.pump # apply pad state SDL has not applied yet
460
+ RGame::Core::VirtualGamepad.sdl_error # what SDL last said
461
+ ```
462
+
463
+ **Buttons and axes take SDL's own numbers.** A button number is a `Controls` pad
464
+ id minus `Controls::BUTTON_GAMEPAD_FIRST`. An axis number is the `Controls` axis
465
+ id itself. `set_axis` takes -32768 to 32767 and raises `RangeError` outside it.
466
+
467
+ `set_button` and `set_axis` return true when SDL accepts the change. SDL may
468
+ still apply a change later: outside an `App`'s frame loop a press reads back
469
+ only after one `VirtualGamepad.pump`. A test that must see a press land checks
470
+ `button_down?` and pumps until it does.
471
+
472
+ `detach` does nothing when called a second time. A pad that is never detached
473
+ stays plugged in until the last `App` is destroyed, because collecting the
474
+ object does not unplug it. SDL shuts down with the last `App` and takes every
475
+ virtual pad with it. After that every method except `detach` raises
476
+ `RuntimeError`.
data/ext/README.md CHANGED
@@ -151,6 +151,37 @@ ruby ext/rgame_core/example.rb # opens a window; Esc or close to quit
151
151
  `require "rgame/core_ext"` / `require "rgame/util_ext"` find it — mirroring
152
152
  how rake-compiler installs a compiled extension into `lib/<gem>/`.
153
153
 
154
+ `rgame_core` links the system's SDL2 by default. To link SDL2 statically
155
+ instead, the way a platform gem does, build the pinned release first:
156
+
157
+ ```
158
+ rake sdl2 # SDL2 as a static library, in build/sdl2
159
+ make ext SDL2_STATIC=build/sdl2 # extconf.rb --with-sdl2-static=<prefix>
160
+ ```
161
+
162
+ This needs CMake, and Ninja on Windows. Switching between the two rebuilds the
163
+ whole extension.
164
+
165
+ A platform gem ships both extensions precompiled, with that SDL2 linked in. It
166
+ builds from a checkout with no `make ext` objects in `ext/`:
167
+
168
+ ```
169
+ make ext-clean
170
+ rake -f tools/platform_gem.rake platform_gem # pkg/rgame-<version>-<platform>.gem
171
+ ```
172
+
173
+ On Linux it runs inside the rake-compiler-dock image, as CI does, so that the
174
+ binaries load on older glibc versions.
175
+
176
+ What the gem does once installed is a different question, and the answer only
177
+ counts on a machine that did not build it — no SDL2, no compiler, no checkout on
178
+ the load path:
179
+
180
+ ```
181
+ gem install --local pkg/rgame-<version>-<platform>.gem
182
+ ruby tools/check_installed_gem.rb # nothing compiled, and require finds the gem
183
+ ```
184
+
154
185
  ## Ruby API
155
186
 
156
187
  ```ruby
@@ -21,6 +21,7 @@
21
21
 
22
22
  #include "rgame/core.h"
23
23
  #include "app/app_gl.h"
24
+ #include "app/sdl_session.h"
24
25
  #include "graphics/canvas.h"
25
26
  #include "text/font_internal.h"
26
27
  #include "app/frame_loop.h"
@@ -52,6 +53,13 @@
52
53
  */
53
54
  static int rgame_live_apps = 0;
54
55
 
56
+ /* Counts the times SDL has started from no live apps; see sdl_session.h. */
57
+ static unsigned rgame_sdl_sessions = 0;
58
+
59
+ unsigned rgame_sdl_session(void) {
60
+ return rgame_live_apps > 0 ? rgame_sdl_sessions : 0;
61
+ }
62
+
55
63
  struct rgame_app {
56
64
  SDL_Window *window;
57
65
  SDL_GLContext gl_context;
@@ -138,7 +146,9 @@ rgame_app *rgame_app_create(int width, int height, const char *title, int fullsc
138
146
 
139
147
  SDL_GL_SetSwapInterval(1); /* vsync */
140
148
 
141
- rgame_live_apps++;
149
+ if (rgame_live_apps++ == 0) {
150
+ rgame_sdl_sessions++;
151
+ }
142
152
 
143
153
  app->running = 1;
144
154
  app->refs = 1;
@@ -0,0 +1,18 @@
1
+ #ifndef RGAME_SDL_SESSION_H
2
+ #define RGAME_SDL_SESSION_H
3
+
4
+ /*
5
+ * Which run of SDL is current. app.c starts SDL when the first app opens and
6
+ * shuts it down when the last one goes, and SDL_Quit frees every handle SDL
7
+ * gave out. A handle that can outlive every app — a virtual gamepad — records
8
+ * the session it was made in and compares, rather than handing SDL a pointer
9
+ * SDL has already freed.
10
+ *
11
+ * Private to the engine, like app_gl.h.
12
+ */
13
+
14
+ /* Non-zero while SDL is running, and different for every run; 0 when no app is
15
+ * alive. */
16
+ unsigned rgame_sdl_session(void);
17
+
18
+ #endif /* RGAME_SDL_SESSION_H */
@@ -15,7 +15,21 @@ $INCFLAGS << ' -I$(srcdir)/include'
15
15
 
16
16
  $INCFLAGS << ' -I$(srcdir)/../rgame_util'
17
17
 
18
- abort 'SDL2 not found (pkg-config --exists sdl2 failed). Install libsdl2-dev.' unless pkg_config('sdl2')
18
+ static_sdl2 = with_config('sdl2-static')
19
+
20
+ if static_sdl2
21
+ static_sdl2 = File.expand_path(static_sdl2)
22
+ pc_dir = File.join(static_sdl2, 'lib', 'pkgconfig')
23
+ abort "No SDL2 under #{static_sdl2}. Run: rake sdl2" unless File.exist?(File.join(pc_dir, 'sdl2.pc'))
24
+
25
+ ENV['PKG_CONFIG_LIBDIR'] = pc_dir
26
+ abort "pkg-config could not read #{pc_dir}/sdl2.pc" unless pkg_config('sdl2')
27
+
28
+ static_only = Shellwords.shellwords(pkg_config('sdl2', 'libs', 'static').to_s) - Shellwords.shellwords($libs)
29
+ $libs += " #{static_only.shelljoin}" unless static_only.empty?
30
+ else
31
+ abort 'SDL2 not found (pkg-config --exists sdl2 failed). Install libsdl2-dev.' unless pkg_config('sdl2')
32
+ end
19
33
 
20
34
  abort 'SDL2 OpenGL header not found (SDL2/SDL_opengl.h). Install libsdl2-dev.' unless have_header('SDL2/SDL_opengl.h')
21
35
 
@@ -44,6 +58,18 @@ $libs = append_library($libs, 'dl') if have_library('dl')
44
58
 
45
59
  $CFLAGS << ' -std=gnu17 -Wall -Wextra'
46
60
 
61
+ if static_sdl2
62
+ case RbConfig::CONFIG['host_os']
63
+ when /linux/ then $LDFLAGS << ' -Wl,--exclude-libs,ALL'
64
+ when /darwin/ then $LDFLAGS << ' -Wl,-exported_symbol,_Init_core_ext'
65
+ end
66
+ end
67
+
68
+ unless enable_config('libruby-link', true) || RbConfig::CONFIG['host_os'].match?(/mingw|mswin|cygwin/)
69
+ $LIBRUBYARG = ''
70
+ $DEFLIBPATH.delete('$(libdir)')
71
+ end
72
+
47
73
  create_makefile('rgame/core_ext')
48
74
 
49
75
  VENDORED = {
@@ -335,6 +335,68 @@ const char *rgame_app_gamepad_name(const rgame_app *app, int slot);
335
335
  /* How many controllers are currently connected. */
336
336
  int rgame_app_gamepad_count(const rgame_app *app);
337
337
 
338
+ /*
339
+ * A synthetic game controller, for tests that need the real gamepad path with
340
+ * no hardware. SDL fabricates it in-process: it reports itself as a game
341
+ * controller and raises genuine device-added and device-removed events, so an
342
+ * app seats it and fires its hot-plug callbacks exactly as it would a real pad.
343
+ *
344
+ * It lives here rather than in a test helper because only engine code is
345
+ * guaranteed to call the SDL the engine runs on, whether SDL is linked
346
+ * statically or dynamically.
347
+ *
348
+ * SDL must be running, which means some app must be alive. When the last app is
349
+ * destroyed SDL shuts down and takes every virtual pad with it; a pad attached
350
+ * before that is then stale, and every call below except detach and free
351
+ * reports it rather than touching SDL.
352
+ */
353
+ typedef struct rgame_virtual_gamepad rgame_virtual_gamepad;
354
+
355
+ /* Plugs in a new virtual pad. Returns NULL when no app is alive or SDL refuses;
356
+ * rgame_virtual_gamepad_error says why. */
357
+ rgame_virtual_gamepad *rgame_virtual_gamepad_attach(void);
358
+
359
+ /* Whether SDL has shut down since this pad was attached. */
360
+ int rgame_virtual_gamepad_stale(const rgame_virtual_gamepad *pad);
361
+
362
+ /*
363
+ * Sets an SDL controller button (SDL_CONTROLLER_BUTTON_*) up or down, or an
364
+ * axis (SDL_CONTROLLER_AXIS_*) to a value in -32768..32767. Returns 0 on
365
+ * success and -1 when SDL refuses. Each runs SDL's joystick update, but SDL
366
+ * may still defer the change: outside an app's frame loop a press read back
367
+ * only after one rgame_virtual_gamepad_pump.
368
+ */
369
+ int rgame_virtual_gamepad_set_button(rgame_virtual_gamepad *pad, int button, int down);
370
+ int rgame_virtual_gamepad_set_axis(rgame_virtual_gamepad *pad, int axis, int value);
371
+
372
+ /*
373
+ * The raw joystick button as SDL last applied it — before any controller
374
+ * mapping. Differs from what an app reads only if the mapping is wrong.
375
+ */
376
+ int rgame_virtual_gamepad_button_down(const rgame_virtual_gamepad *pad, int button);
377
+
378
+ /* Whether SDL treats the pad as a game controller, i.e. has a mapping for it.
379
+ * An app seats only pads for which this is 1. */
380
+ int rgame_virtual_gamepad_is_game_controller(const rgame_virtual_gamepad *pad);
381
+
382
+ /* Whether the pad is still a live, attached device. */
383
+ int rgame_virtual_gamepad_attached(const rgame_virtual_gamepad *pad);
384
+
385
+ /* Runs SDL's event pump and applies pending joystick state — for waiting on a
386
+ * change SDL has accepted and not yet applied. */
387
+ void rgame_virtual_gamepad_pump(void);
388
+
389
+ /* Unplugs the pad, as far as SDL and every app are concerned. Safe to call
390
+ * twice, and on a stale pad, where it does nothing. */
391
+ void rgame_virtual_gamepad_detach(rgame_virtual_gamepad *pad);
392
+
393
+ /* Frees the handle without touching SDL. A pad freed while still attached stays
394
+ * plugged in until SDL shuts down. NULL-safe. */
395
+ void rgame_virtual_gamepad_free(rgame_virtual_gamepad *pad);
396
+
397
+ /* Why the last attach failed, or SDL's own last error. Never NULL. */
398
+ const char *rgame_virtual_gamepad_error(void);
399
+
338
400
  /* Monotonic milliseconds since startup. For time-based animation phase, etc. */
339
401
  unsigned int rgame_app_ticks_ms(const rgame_app *app);
340
402
 
@@ -0,0 +1,151 @@
1
+ /*
2
+ * virtual_gamepad.c — a synthetic SDL game controller, for tests.
3
+ *
4
+ * Layer 3, and thin: every function is one or two SDL calls. What earns it a
5
+ * file in the engine rather than in a spec helper is *which* SDL it calls. A
6
+ * helper reaching SDL from outside the extension has to find it by name or by
7
+ * exported symbol, and neither works on every platform once SDL is linked into
8
+ * core_ext statically: on Windows the extension exports no SDL symbol, and a
9
+ * library name resolves to some other copy of SDL. Code compiled into the
10
+ * engine can only call the SDL the engine runs on.
11
+ *
12
+ * Covered end to end by the gamepad and input specs in spec_core/, which drive
13
+ * an app's real hot-plug and button path through it.
14
+ */
15
+
16
+ #include "rgame/core.h"
17
+ #include "app/sdl_session.h"
18
+
19
+ #include <SDL2/SDL.h>
20
+ #include <stdlib.h>
21
+
22
+ struct rgame_virtual_gamepad {
23
+ SDL_Joystick *joystick;
24
+ unsigned session;
25
+ int detached;
26
+ };
27
+
28
+ static const char *attach_error = NULL;
29
+
30
+ static int usable(const rgame_virtual_gamepad *pad) {
31
+ return !pad->detached && pad->session == rgame_sdl_session();
32
+ }
33
+
34
+ /*
35
+ * The pad's device index now. SDL renumbers device indices whenever any other
36
+ * device arrives or leaves, so the index attach returned is not kept: the
37
+ * instance id is the stable identity, and the index is looked up from it.
38
+ */
39
+ static int device_index(const rgame_virtual_gamepad *pad) {
40
+ SDL_JoystickID id = SDL_JoystickInstanceID(pad->joystick);
41
+ int count = SDL_NumJoysticks();
42
+ for (int index = 0; index < count; index++) {
43
+ if (SDL_JoystickGetDeviceInstanceID(index) == id) {
44
+ return index;
45
+ }
46
+ }
47
+ return -1;
48
+ }
49
+
50
+ rgame_virtual_gamepad *rgame_virtual_gamepad_attach(void) {
51
+ attach_error = NULL;
52
+
53
+ unsigned session = rgame_sdl_session();
54
+ if (session == 0) {
55
+ attach_error = "no app is open, so SDL is not running";
56
+ return NULL;
57
+ }
58
+
59
+ int index = SDL_JoystickAttachVirtual(SDL_JOYSTICK_TYPE_GAMECONTROLLER, SDL_CONTROLLER_AXIS_MAX,
60
+ SDL_CONTROLLER_BUTTON_MAX, 0);
61
+ if (index < 0) {
62
+ return NULL;
63
+ }
64
+
65
+ SDL_Joystick *joystick = SDL_JoystickOpen(index);
66
+ if (!joystick) {
67
+ SDL_JoystickDetachVirtual(index);
68
+ return NULL;
69
+ }
70
+
71
+ rgame_virtual_gamepad *pad = calloc(1, sizeof *pad);
72
+ if (!pad) {
73
+ SDL_JoystickClose(joystick);
74
+ SDL_JoystickDetachVirtual(index);
75
+ attach_error = "out of memory";
76
+ return NULL;
77
+ }
78
+ pad->joystick = joystick;
79
+ pad->session = session;
80
+ return pad;
81
+ }
82
+
83
+ int rgame_virtual_gamepad_stale(const rgame_virtual_gamepad *pad) {
84
+ return pad->session != rgame_sdl_session();
85
+ }
86
+
87
+ int rgame_virtual_gamepad_set_button(rgame_virtual_gamepad *pad, int button, int down) {
88
+ if (!usable(pad)) {
89
+ return -1;
90
+ }
91
+ int result = SDL_JoystickSetVirtualButton(pad->joystick, button, down ? SDL_PRESSED : SDL_RELEASED);
92
+ /* Setting virtual state only queues it. An update applies it where SDL can;
93
+ * where it cannot yet, rgame_virtual_gamepad_pump is the retry. */
94
+ SDL_JoystickUpdate();
95
+ return result;
96
+ }
97
+
98
+ int rgame_virtual_gamepad_set_axis(rgame_virtual_gamepad *pad, int axis, int value) {
99
+ if (!usable(pad)) {
100
+ return -1;
101
+ }
102
+ int result = SDL_JoystickSetVirtualAxis(pad->joystick, axis, (Sint16)value);
103
+ SDL_JoystickUpdate();
104
+ return result;
105
+ }
106
+
107
+ int rgame_virtual_gamepad_button_down(const rgame_virtual_gamepad *pad, int button) {
108
+ return usable(pad) && SDL_JoystickGetButton(pad->joystick, button) == SDL_PRESSED;
109
+ }
110
+
111
+ int rgame_virtual_gamepad_is_game_controller(const rgame_virtual_gamepad *pad) {
112
+ if (!usable(pad)) {
113
+ return 0;
114
+ }
115
+ int index = device_index(pad);
116
+ return index >= 0 && SDL_IsGameController(index) == SDL_TRUE;
117
+ }
118
+
119
+ int rgame_virtual_gamepad_attached(const rgame_virtual_gamepad *pad) {
120
+ return usable(pad) && SDL_JoystickGetAttached(pad->joystick) == SDL_TRUE;
121
+ }
122
+
123
+ void rgame_virtual_gamepad_pump(void) {
124
+ if (rgame_sdl_session() == 0) {
125
+ return;
126
+ }
127
+ SDL_PumpEvents();
128
+ SDL_JoystickUpdate();
129
+ }
130
+
131
+ void rgame_virtual_gamepad_detach(rgame_virtual_gamepad *pad) {
132
+ if (!usable(pad)) {
133
+ pad->detached = 1;
134
+ return;
135
+ }
136
+ int index = device_index(pad);
137
+ SDL_JoystickClose(pad->joystick);
138
+ if (index >= 0) {
139
+ SDL_JoystickDetachVirtual(index);
140
+ }
141
+ pad->joystick = NULL;
142
+ pad->detached = 1;
143
+ }
144
+
145
+ void rgame_virtual_gamepad_free(rgame_virtual_gamepad *pad) {
146
+ free(pad);
147
+ }
148
+
149
+ const char *rgame_virtual_gamepad_error(void) {
150
+ return attach_error ? attach_error : SDL_GetError();
151
+ }
@@ -549,4 +549,5 @@ void Init_core_ext(void) {
549
549
  rgame_init_font(mCore);
550
550
  rgame_init_audio(mCore);
551
551
  rgame_init_locale(mCore);
552
+ rgame_init_virtual_gamepad(mCore);
552
553
  }
@@ -28,6 +28,9 @@ void rgame_init_locale(VALUE mCore);
28
28
  * share a file when everything else here does not. */
29
29
  void rgame_init_audio(VALUE mCore);
30
30
 
31
+ /* A synthetic game controller for the Core specs; see virtual_gamepad_ext.c. */
32
+ void rgame_init_virtual_gamepad(VALUE mCore);
33
+
31
34
  /*
32
35
  * Wraps a baked recording in a Ruby object. Only Renderer#record calls this:
33
36
  * `images` is the Array of Images drawn while recording, which the new object
@@ -0,0 +1,162 @@
1
+ /*
2
+ * virtual_gamepad_ext.c — the Ruby binding for RGame::Core::VirtualGamepad.
3
+ *
4
+ * pad = RGame::Core::VirtualGamepad.new # needs an open App
5
+ * pad.set_button(0, true) # SDL_CONTROLLER_BUTTON_A down
6
+ * pad.button_down?(0) # => true
7
+ * pad.detach
8
+ *
9
+ * Test-only, and named so, like Audio.debug_live_sounds. The engine side is
10
+ * input/virtual_gamepad.c, which says why this belongs in the extension. This
11
+ * file checks arguments and turns the engine's "stale" and "refused" answers
12
+ * into exceptions; how long to wait for a press to land, and whether presses
13
+ * land at all on a given machine, is the spec harness's business
14
+ * (spec_core/support/virtual_gamepad.rb).
15
+ */
16
+
17
+ #include "ruby/core_ext.h"
18
+
19
+ #include "rgame/core.h"
20
+
21
+ typedef struct {
22
+ rgame_virtual_gamepad *pad;
23
+ } rgame_virtual_gamepad_ref;
24
+
25
+ /* Frees the handle only. Detaching from inside the collector would raise a
26
+ * device-removed event at whatever moment GC happened to run, and after the
27
+ * last App has gone SDL would be touched after SDL_Quit. */
28
+ static void virtual_gamepad_ref_free(void *ptr) {
29
+ rgame_virtual_gamepad_ref *ref = ptr;
30
+ rgame_virtual_gamepad_free(ref->pad);
31
+ xfree(ref);
32
+ }
33
+
34
+ static size_t virtual_gamepad_ref_size(const void *ptr) {
35
+ (void)ptr;
36
+ return sizeof(rgame_virtual_gamepad_ref);
37
+ }
38
+
39
+ static const rb_data_type_t virtual_gamepad_data_type = {
40
+ .wrap_struct_name = "rgame_virtual_gamepad",
41
+ .function = {
42
+ .dfree = virtual_gamepad_ref_free,
43
+ .dsize = virtual_gamepad_ref_size,
44
+ },
45
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
46
+ };
47
+
48
+ /* The pad behind `self`, raising when it can no longer be used: never
49
+ * initialized, or attached during a run of SDL that has since ended. */
50
+ static rgame_virtual_gamepad *virtual_gamepad_live(VALUE self) {
51
+ rgame_virtual_gamepad_ref *ref;
52
+ TypedData_Get_Struct(self, rgame_virtual_gamepad_ref, &virtual_gamepad_data_type, ref);
53
+ if (!ref->pad) {
54
+ rb_raise(rb_eRuntimeError, "virtual gamepad is not initialized");
55
+ }
56
+ if (rgame_virtual_gamepad_stale(ref->pad)) {
57
+ rb_raise(rb_eRuntimeError,
58
+ "virtual gamepad outlived SDL: every App was destroyed after it was attached");
59
+ }
60
+ return ref->pad;
61
+ }
62
+
63
+ static VALUE virtual_gamepad_alloc(VALUE klass) {
64
+ rgame_virtual_gamepad_ref *ref;
65
+ VALUE object = TypedData_Make_Struct(klass, rgame_virtual_gamepad_ref,
66
+ &virtual_gamepad_data_type, ref);
67
+ ref->pad = NULL;
68
+ return object;
69
+ }
70
+
71
+ /* VirtualGamepad.new — plugs in a pad. Raises when no App is open. */
72
+ static VALUE virtual_gamepad_initialize(VALUE self) {
73
+ rgame_virtual_gamepad_ref *ref;
74
+ TypedData_Get_Struct(self, rgame_virtual_gamepad_ref, &virtual_gamepad_data_type, ref);
75
+ if (ref->pad) {
76
+ rb_raise(rb_eRuntimeError, "virtual gamepad is already attached");
77
+ }
78
+
79
+ rgame_virtual_gamepad *pad = rgame_virtual_gamepad_attach();
80
+ if (!pad) {
81
+ rb_raise(rb_eRuntimeError, "cannot attach a virtual gamepad: %s",
82
+ rgame_virtual_gamepad_error());
83
+ }
84
+ ref->pad = pad;
85
+ return self;
86
+ }
87
+
88
+ /*
89
+ * #set_button(button, down) — `button` is SDL's controller button number, the
90
+ * same number RGame::Util::Controls' PAD_ ids add to the gamepad range. True
91
+ * when SDL accepted the change, which SDL may apply only on a later pump.
92
+ */
93
+ static VALUE virtual_gamepad_set_button(VALUE self, VALUE button, VALUE down) {
94
+ rgame_virtual_gamepad *pad = virtual_gamepad_live(self);
95
+ int result = rgame_virtual_gamepad_set_button(pad, NUM2INT(button), RTEST(down));
96
+ return result == 0 ? Qtrue : Qfalse;
97
+ }
98
+
99
+ /* #set_axis(axis, value) — `value` in -32768..32767, SDL's own axis range. */
100
+ static VALUE virtual_gamepad_set_axis(VALUE self, VALUE axis, VALUE value) {
101
+ rgame_virtual_gamepad *pad = virtual_gamepad_live(self);
102
+ int raw = NUM2INT(value);
103
+ if (raw < -32768 || raw > 32767) {
104
+ rb_raise(rb_eRangeError, "axis value %d is outside -32768..32767", raw);
105
+ }
106
+ return rgame_virtual_gamepad_set_axis(pad, NUM2INT(axis), raw) == 0 ? Qtrue : Qfalse;
107
+ }
108
+
109
+ /* #button_down?(button) — the raw joystick button, before controller mapping. */
110
+ static VALUE virtual_gamepad_button_down_p(VALUE self, VALUE button) {
111
+ rgame_virtual_gamepad *pad = virtual_gamepad_live(self);
112
+ return rgame_virtual_gamepad_button_down(pad, NUM2INT(button)) ? Qtrue : Qfalse;
113
+ }
114
+
115
+ /* #game_controller? — whether SDL has a mapping for the pad, which an App needs
116
+ * before it seats one. */
117
+ static VALUE virtual_gamepad_game_controller_p(VALUE self) {
118
+ return rgame_virtual_gamepad_is_game_controller(virtual_gamepad_live(self)) ? Qtrue : Qfalse;
119
+ }
120
+
121
+ static VALUE virtual_gamepad_attached_p(VALUE self) {
122
+ return rgame_virtual_gamepad_attached(virtual_gamepad_live(self)) ? Qtrue : Qfalse;
123
+ }
124
+
125
+ /* #detach — unplugs the pad. Does nothing the second time, or once SDL has shut
126
+ * down, since the pad went with it. */
127
+ static VALUE virtual_gamepad_detach(VALUE self) {
128
+ rgame_virtual_gamepad_ref *ref;
129
+ TypedData_Get_Struct(self, rgame_virtual_gamepad_ref, &virtual_gamepad_data_type, ref);
130
+ if (ref->pad) {
131
+ rgame_virtual_gamepad_detach(ref->pad);
132
+ }
133
+ return Qnil;
134
+ }
135
+
136
+ /* VirtualGamepad.pump — runs SDL's event pump and applies pending pad state. */
137
+ static VALUE virtual_gamepad_s_pump(VALUE klass) {
138
+ (void)klass;
139
+ rgame_virtual_gamepad_pump();
140
+ return Qnil;
141
+ }
142
+
143
+ /* VirtualGamepad.sdl_error — what SDL last said when it refused something. */
144
+ static VALUE virtual_gamepad_s_sdl_error(VALUE klass) {
145
+ (void)klass;
146
+ return rb_utf8_str_new_cstr(rgame_virtual_gamepad_error());
147
+ }
148
+
149
+ void rgame_init_virtual_gamepad(VALUE mCore) {
150
+ VALUE cVirtualGamepad = rb_define_class_under(mCore, "VirtualGamepad", rb_cObject);
151
+
152
+ rb_define_alloc_func(cVirtualGamepad, virtual_gamepad_alloc);
153
+ rb_define_method(cVirtualGamepad, "initialize", virtual_gamepad_initialize, 0);
154
+ rb_define_method(cVirtualGamepad, "set_button", virtual_gamepad_set_button, 2);
155
+ rb_define_method(cVirtualGamepad, "set_axis", virtual_gamepad_set_axis, 2);
156
+ rb_define_method(cVirtualGamepad, "button_down?", virtual_gamepad_button_down_p, 1);
157
+ rb_define_method(cVirtualGamepad, "game_controller?", virtual_gamepad_game_controller_p, 0);
158
+ rb_define_method(cVirtualGamepad, "attached?", virtual_gamepad_attached_p, 0);
159
+ rb_define_method(cVirtualGamepad, "detach", virtual_gamepad_detach, 0);
160
+ rb_define_singleton_method(cVirtualGamepad, "pump", virtual_gamepad_s_pump, 0);
161
+ rb_define_singleton_method(cVirtualGamepad, "sdl_error", virtual_gamepad_s_sdl_error, 0);
162
+ }
@@ -6,4 +6,9 @@ $CFLAGS << ' -std=gnu17 -Wall -Wextra'
6
6
 
7
7
  $CFLAGS << ' -ffp-contract=off'
8
8
 
9
+ unless enable_config('libruby-link', true) || RbConfig::CONFIG['host_os'].match?(/mingw|mswin|cygwin/)
10
+ $LIBRUBYARG = ''
11
+ $DEFLIBPATH.delete('$(libdir)')
12
+ end
13
+
9
14
  create_makefile('rgame/util_ext')
@@ -8,10 +8,10 @@ source 'https://rubygems.org'
8
8
  # replace this line with something like `ruby '~> <%= ruby_version.split('.').first(2).join('.') %>'`.
9
9
  ruby file: '.ruby-version'
10
10
 
11
- # The engine. It has no runtime dependencies of its own SDL2 and OpenGL are
12
- # system libraries rather than gems but installing it compiles two C
13
- # extensions, so a C compiler, `make`, `pkg-config` and the SDL2/OpenGL
14
- # development headers have to be present. See the rgame README.
11
+ # The engine. Its one gem dependency is rexml, which Bundler installs with it.
12
+ # SDL2 and OpenGL are system libraries rather than gems, and installing rgame
13
+ # compiles two C extensions, so a C compiler, `make`, `pkg-config` and the
14
+ # SDL2/OpenGL development headers have to be present. See the rgame README.
15
15
  gem 'rgame', '<%= rgame_requirement %>'
16
16
 
17
17
  group :development, :test do
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rgame/core_ext'
4
+
5
+ module RGame
6
+ module Core
7
+ # A synthetic game controller, for tests that need the real gamepad path
8
+ # with no hardware. It is C (`ext/rgame_core/input/virtual_gamepad.c`, bound
9
+ # in `ruby/virtual_gamepad_ext.c`), and it serves tests, not gameplay.
10
+ #
11
+ # pad = RGame::Core::VirtualGamepad.new # an App must be open
12
+ # pad.set_button(0, true) # SDL_CONTROLLER_BUTTON_A
13
+ # pad.set_axis(0, -32_768) # left stick fully left
14
+ # pad.detach # the App sees it unplugged
15
+ #
16
+ # SDL fabricates the device in-process, so the App seats it and raises its
17
+ # `gamepad_connected` and `gamepad_disconnected` callbacks as it would for a
18
+ # real pad. Buttons and axes take SDL's own numbers.
19
+ #
20
+ # It is part of the extension rather than a spec helper because only the
21
+ # extension is guaranteed to reach the SDL the App runs on, whether SDL is
22
+ # linked statically or dynamically.
23
+ class VirtualGamepad
24
+ end
25
+ end
26
+ end
data/lib/rgame/core.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'core/app'
5
5
  require_relative 'core/locale'
6
6
  require_relative 'core/input'
7
7
  require_relative 'core/gamepad'
8
+ require_relative 'core/virtual_gamepad'
8
9
  require_relative 'core/image'
9
10
  require_relative 'core/renderer'
10
11
  require_relative 'core/recording'
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rexml/document'
4
- require 'base64'
5
4
  require 'zlib'
6
5
 
7
6
  require_relative '../util'
@@ -52,7 +51,7 @@ module RGame
52
51
  layers = []
53
52
  above = []
54
53
  root.each_element('layer') do |layer_el|
55
- raw = Base64.decode64(layer_el.elements['data'].text.strip)
54
+ raw = layer_el.elements['data'].text.strip.unpack1('m')
56
55
  gids = Zlib::Inflate.inflate(raw).unpack('V*')
57
56
  gids.map! { |g| g & FLIP_MASK }
58
57
  layers << gids
data/lib/rgame/version.rb CHANGED
@@ -8,5 +8,5 @@
8
8
  # module here is enough; the extensions define `RGame` too, and doing so twice
9
9
  # is harmless.
10
10
  module RGame
11
- VERSION = '0.3.0'
11
+ VERSION = '0.4.0'
12
12
  end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rgame
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Süßenbach
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rexml
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.4'
12
26
  description: |
13
27
  RGame is a small 2D game engine for Ruby, written in Ruby and C. It's build
14
28
  on top of SDL2, OpenGL and miniaudio. It's built with testability and
@@ -125,6 +139,7 @@ files:
125
139
  - ext/rgame_core/app/frame_loop.h
126
140
  - ext/rgame_core/app/locale.c
127
141
  - ext/rgame_core/app/locale.h
142
+ - ext/rgame_core/app/sdl_session.h
128
143
  - ext/rgame_core/audio/audio.c
129
144
  - ext/rgame_core/audio/audio_internal.h
130
145
  - ext/rgame_core/audio/vorbis_decoder.c
@@ -158,6 +173,7 @@ files:
158
173
  - ext/rgame_core/input/gamepad.h
159
174
  - ext/rgame_core/input/input.c
160
175
  - ext/rgame_core/input/input.h
176
+ - ext/rgame_core/input/virtual_gamepad.c
161
177
  - ext/rgame_core/ruby/audio_ext.c
162
178
  - ext/rgame_core/ruby/core_ext.c
163
179
  - ext/rgame_core/ruby/core_ext.h
@@ -166,6 +182,7 @@ files:
166
182
  - ext/rgame_core/ruby/locale_ext.c
167
183
  - ext/rgame_core/ruby/recording_ext.c
168
184
  - ext/rgame_core/ruby/renderer_ext.c
185
+ - ext/rgame_core/ruby/virtual_gamepad_ext.c
169
186
  - ext/rgame_core/text/atlas.c
170
187
  - ext/rgame_core/text/atlas.h
171
188
  - ext/rgame_core/text/font.c
@@ -232,6 +249,7 @@ files:
232
249
  - lib/rgame/core/sprite_sheet.rb
233
250
  - lib/rgame/core/tile_map_renderer.rb
234
251
  - lib/rgame/core/ui_atlas.rb
252
+ - lib/rgame/core/virtual_gamepad.rb
235
253
  - lib/rgame/engine.rb
236
254
  - lib/rgame/engine/actor_blockers.rb
237
255
  - lib/rgame/engine/animation_set.rb