ruby-sfml 3.0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +101 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +245 -0
  5. data/ext/ruby-sfml/extconf.rb +69 -0
  6. data/lib/sfml/assets/fonts/DejaVuSans.LICENSE.txt +78 -0
  7. data/lib/sfml/assets/fonts/DejaVuSans.ttf +0 -0
  8. data/lib/sfml/assets.rb +121 -0
  9. data/lib/sfml/audio/listener.rb +55 -0
  10. data/lib/sfml/audio/music.rb +88 -0
  11. data/lib/sfml/audio/sound.rb +102 -0
  12. data/lib/sfml/audio/sound_buffer.rb +38 -0
  13. data/lib/sfml/audio/sound_buffer_recorder.rb +71 -0
  14. data/lib/sfml/audio/sound_recorder.rb +30 -0
  15. data/lib/sfml/c/audio.rb +106 -0
  16. data/lib/sfml/c/graphics.rb +425 -0
  17. data/lib/sfml/c/network.rb +79 -0
  18. data/lib/sfml/c/system.rb +43 -0
  19. data/lib/sfml/c/window.rb +186 -0
  20. data/lib/sfml/c.rb +72 -0
  21. data/lib/sfml/game.rb +101 -0
  22. data/lib/sfml/graphics/blend_mode.rb +108 -0
  23. data/lib/sfml/graphics/circle_shape.rb +67 -0
  24. data/lib/sfml/graphics/color.rb +89 -0
  25. data/lib/sfml/graphics/convex_shape.rb +82 -0
  26. data/lib/sfml/graphics/font.rb +67 -0
  27. data/lib/sfml/graphics/image.rb +125 -0
  28. data/lib/sfml/graphics/rectangle_shape.rb +62 -0
  29. data/lib/sfml/graphics/render_states.rb +56 -0
  30. data/lib/sfml/graphics/render_target.rb +146 -0
  31. data/lib/sfml/graphics/render_texture.rb +72 -0
  32. data/lib/sfml/graphics/render_window.rb +154 -0
  33. data/lib/sfml/graphics/shader.rb +132 -0
  34. data/lib/sfml/graphics/sprite.rb +75 -0
  35. data/lib/sfml/graphics/text.rb +144 -0
  36. data/lib/sfml/graphics/texture.rb +79 -0
  37. data/lib/sfml/graphics/transform.rb +150 -0
  38. data/lib/sfml/graphics/transformable.rb +74 -0
  39. data/lib/sfml/graphics/vertex.rb +53 -0
  40. data/lib/sfml/graphics/vertex_array.rb +114 -0
  41. data/lib/sfml/graphics/view.rb +126 -0
  42. data/lib/sfml/network/ip_address.rb +67 -0
  43. data/lib/sfml/network/tcp_listener.rb +61 -0
  44. data/lib/sfml/network/tcp_socket.rb +74 -0
  45. data/lib/sfml/network/udp_socket.rb +71 -0
  46. data/lib/sfml/system/clock.rb +44 -0
  47. data/lib/sfml/system/rect.rb +64 -0
  48. data/lib/sfml/system/time.rb +48 -0
  49. data/lib/sfml/system/vector2.rb +66 -0
  50. data/lib/sfml/system/vector3.rb +63 -0
  51. data/lib/sfml/version.rb +19 -0
  52. data/lib/sfml/window/clipboard.rb +38 -0
  53. data/lib/sfml/window/cursor.rb +68 -0
  54. data/lib/sfml/window/event.rb +133 -0
  55. data/lib/sfml/window/joystick.rb +90 -0
  56. data/lib/sfml/window/keyboard.rb +60 -0
  57. data/lib/sfml/window/mouse.rb +71 -0
  58. data/lib/sfml/window/video_mode.rb +37 -0
  59. data/lib/sfml/window/window.rb +149 -0
  60. data/lib/sfml.rb +98 -0
  61. data/ruby-sfml.gemspec +38 -0
  62. metadata +163 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8688630fa5286e1b5c28ed9c5cda8aa635a197fe74dc422859253f45f11f111a
4
+ data.tar.gz: bcb83c0e0667fcae607fa3d3b0ca32527d25dec96fd8726ae697b1b5340c68f9
5
+ SHA512:
6
+ metadata.gz: d2a0d250ee6f2a7ca62d7da0305f3345bed8170be02f7700074ae1dbb76a24379687c13e64488ab6cbea5eed78821aa9554ecac6501a76ca16eb3febcbe8ca47
7
+ data.tar.gz: f9b7399e3e2b6168e217292573ef54a15a6217c4675f21f3718a4786787590fda2900d6795b68cd422ca1a5859962aa75d51371305f3ca1584914d5d4e0d13e2
data/CHANGELOG.md ADDED
@@ -0,0 +1,101 @@
1
+ # Changelog
2
+
3
+ All notable changes are documented here. The format roughly follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the gem
5
+ versioning is [described in the README](README.md#versioning) — first
6
+ three segments mirror the targeted CSFML release, fourth segment is
7
+ ruby-sfml's own patch level.
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [3.0.0.0] — initial release
12
+
13
+ First public cut. Targets **CSFML 3.0.0** (released March 2025) and
14
+ **Ruby ≥ 3.2**. API surface complete for the SFML 3.0 spec; some
15
+ engineering polish still pending (`gem build` end-to-end verification,
16
+ RBS signatures, hosted RDoc).
17
+
18
+ ### System
19
+ - `Vector2`, `Vector3` — operator-friendly value classes with `coerce`
20
+ (`2 * vec`), pattern-match `deconstruct`, `length`, `normalize`,
21
+ `dot`/`cross`, conversion helpers
22
+ - `Rect` — single class for float / int rectangles with `contains?`,
23
+ `intersects?`, deconstruction
24
+ - `Time`, `Clock` — monotonic timer, immutable Time arithmetic
25
+
26
+ ### Window
27
+ - `RenderWindow` — main 2D drawing surface
28
+ - `Window` — bare GL-only window for raw-OpenGL apps
29
+ - `Event` — pattern-matchable hash-like value (`case event in {type: :key_pressed, code: :escape}`)
30
+ - `Keyboard`, `Mouse`, `Joystick` — polling APIs with symbol-named keys / buttons / axes
31
+ - `Cursor` — 21 system cursor types + `from_pixels` for custom shapes
32
+ - `Clipboard` — UTF-8 in / UTF-8 out via the unicode-string CSFML path
33
+ - `VideoMode`
34
+
35
+ ### Graphics
36
+ - `Color`, `Image`, `Texture`, `RenderTexture`
37
+ - `Sprite`, `CircleShape`, `RectangleShape`, `ConvexShape` — share a
38
+ `Transformable` mixin (position, rotation, scale, origin, move)
39
+ - `Vertex`, `VertexArray` — batched geometry; six primitive types
40
+ - `Font`, `Text` — UTF-8 strings, `local_bounds` / `global_bounds`,
41
+ Font.find searches common system paths plus a bundled DejaVu Sans
42
+ - `View` — 2D camera, including `from_rect` and viewport for split-screen / minimap
43
+ - `RenderStates`, `BlendMode` — full blend mode catalogue, kwargs
44
+ shortcut on `window.draw(thing, blend_mode: SFML::BlendMode::ADD)`
45
+ - `Shader` — load from file or memory, uniform setter that dispatches by
46
+ Ruby type (`shader[:time] = 1.5`, `shader[:tex] = my_texture`,
47
+ `shader[:tint] = SFML::Color.red`)
48
+ - `Transform` — standalone 4×3 matrix value class, chainable
49
+ - `RenderTarget#draw` accepts `texture:`, `blend_mode:`, `shader:`,
50
+ `coordinate_type:`, `render_states:` kwargs
51
+ - `RenderTarget#draw_primitives(vertices, type)` — one-shot batch
52
+ rendering without a `VertexArray` object
53
+
54
+ ### Audio
55
+ - `SoundBuffer`, `Sound`, `Music`
56
+ - 3D positional audio on Sound and Music (`#position=`, `#attenuation=`,
57
+ `#min_distance=`, `#relative_to_listener=`)
58
+ - `Listener` — global "ear" with `position`, `direction`, `up_vector`,
59
+ `global_volume`
60
+ - `SoundBufferRecorder` + `SoundRecorder` static helpers — record audio
61
+ from the system mic into a SoundBuffer
62
+
63
+ ### Network
64
+ - `Network::IpAddress` — value class with `from_string`, `from_bytes`,
65
+ `LOCALHOST` / `ANY` / `BROADCAST` constants, `local`, `public`
66
+ - `Network::TcpSocket`, `Network::TcpListener` — TCP client + server,
67
+ blocking and non-blocking modes
68
+ - `Network::UdpSocket` — connectionless datagrams
69
+ - `SocketStatus` returned as symbols (`:done`, `:not_ready`,
70
+ `:disconnected`, `:error`, `:partial`)
71
+ - Intentionally not wrapped: `Http`, `Ftp` (use Ruby stdlib's
72
+ `Net::HTTP` / `Net::FTP` — they're nicer)
73
+
74
+ ### Helpers
75
+ - `Game` — subclass-friendly main loop with `setup` / `update(dt)` /
76
+ `draw` / `on_event` hooks. Auto-quit on Esc + close button.
77
+ - `Assets` — cached, search-path-driven loader.
78
+ `SFML::Assets.font("DejaVuSans")`, `.texture(name)`, `.sound(name)`,
79
+ `.music(name)`. Default search root is `<dir of $0>/assets/`.
80
+ - Bundled DejaVu Sans (Bitstream Vera license) — `SFML::Font.default`
81
+ works without a system font install
82
+
83
+ ### Engineering
84
+ - Two-tier API: `SFML::C::*` thin FFI bindings, `SFML::*` idiomatic
85
+ Ruby on top. Most user-facing classes are ~50–150 LOC each
86
+ - `RenderTarget` mixin — `RenderWindow` and `RenderTexture` share
87
+ every drawing method through CSFML_PREFIX dispatch
88
+ - 287 RSpec examples, all hitting real CSFML
89
+ - 20 self-contained example folders under [examples/](examples/)
90
+ - CI matrix: Ubuntu + macOS × Ruby 3.2 / 3.3 / 3.4
91
+ - Linux CI builds CSFML 3 from source (cached) and runs specs under
92
+ `xvfb-run`
93
+ - `extconf.rb` checks every required `libcsfml-*` plus probes
94
+ `sfClock_isRunning` (CSFML 3.0+ only) — `gem install` aborts with a
95
+ helpful message on CSFML 2.x
96
+ - Same probe re-runs at `require "sfml"` time as a runtime sanity
97
+ check
98
+ - `at_exit` hook stops live `Sound`/`Music` and bypasses Ruby's
99
+ finalizer pass via `exit!` — eliminates a class of GL/OpenAL-teardown
100
+ segfaults that plagued every non-trivial example
101
+ - RDoc 7 (Aliki theme) generated via `rake rdoc`
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ruby-sfml contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # ruby-sfml
2
+
3
+ Modern, idiomatic Ruby bindings for [SFML 3.x](https://www.sfml-dev.org/) via [CSFML](https://github.com/SFML/CSFML) and [Ruby FFI](https://github.com/ffi/ffi).
4
+
5
+ > **Status:** the API surface is complete for SFML 3.0 — system, window, graphics, audio, network, plus the higher-level `Game` and `Assets` helpers. 287 RSpec examples, 20 runnable example folders. Some details (gem-build verification, RBS signatures, hosted docs) are still pending.
6
+
7
+ ## Why
8
+
9
+ The original [rbSFML](https://github.com/Groogy/rbSFML) is unmaintained and only works against SFML 2 and Ruby 2.2. `ruby-sfml` targets the current SFML 3.x line, modern Ruby (3.2+), and a Ruby-first API — blocks instead of polling loops, symbols instead of enums, operators on vectors, automatic resource cleanup via GC.
10
+
11
+ ## Requirements
12
+
13
+ - Ruby `>= 3.2`
14
+ - CSFML **3.0** or compatible 3.x at the system level
15
+
16
+ ### Install CSFML
17
+
18
+ | OS | Command | Notes |
19
+ | ------------------------ | ---------------------------------------- | ----- |
20
+ | Ubuntu 25.04+ / Debian | `sudo apt install libcsfml-dev` | Ships CSFML 3 |
21
+ | Ubuntu 22.04 / 24.04 | repo too old (CSFML 2.5) | Build from [3.0.0 release](https://github.com/SFML/CSFML/releases/tag/3.0.0) |
22
+ | macOS (brew) | `brew install csfml` | Currently 3.x |
23
+ | Arch Linux | `sudo pacman -S csfml` | Currently 3.x |
24
+ | Windows | https://www.sfml-dev.org/download/csfml/ | Pick the 3.0 tarball |
25
+
26
+ ruby-sfml verifies the linked CSFML twice:
27
+
28
+ - **At `gem install`** — `extconf.rb` checks for the five `libcsfml-*` libraries plus a CSFML 3.0+ symbol (`sfClock_isRunning`). Aborts with a clear message if the system has CSFML 2.x.
29
+ - **At `require "sfml"`** — same probe runs as a runtime sanity check, in case libraries were swapped between install and use.
30
+
31
+ You'll see a useful error either way; nothing falls through to a cryptic CSFML segfault.
32
+
33
+ ## A 12-line game
34
+
35
+ ```ruby
36
+ require "sfml"
37
+
38
+ class Hello < SFML::Game
39
+ def setup
40
+ @ball = SFML::CircleShape.new(radius: 30, fill_color: SFML::Color.white,
41
+ position: [200, 200])
42
+ end
43
+
44
+ def update(dt) = @ball.move(60 * dt.as_seconds * SFML::Vector2[1, 0])
45
+ def draw = window.draw(@ball)
46
+ end
47
+
48
+ Hello.new(title: "Hello", background: SFML::Color.cornflower_blue).run
49
+ ```
50
+
51
+ `SFML::Game` handles window creation, the main loop, event pumping, dt, and the Esc/close-button quit. Override `setup` / `update` / `draw` / `on_event`. Drop into the manual loop style any time you want full control.
52
+
53
+ ## A 5-line manual loop
54
+
55
+ ```ruby
56
+ require "sfml"
57
+
58
+ window = SFML::RenderWindow.new(800, 600, "Hello", framerate: 60)
59
+
60
+ while window.open?
61
+ window.each_event do |event|
62
+ case event
63
+ in {type: :closed} then window.close
64
+ in {type: :key_pressed, code: :escape} then window.close
65
+ else # always include `else` — case/in raises on unmatched events.
66
+ end
67
+ end
68
+
69
+ window.clear(SFML::Color.cornflower_blue)
70
+ window.display
71
+ end
72
+ ```
73
+
74
+ ## Available modules
75
+
76
+ | Area | Classes |
77
+ | -------- | ------------------------------------------------------------ |
78
+ | System | `Vector2`, `Vector3`, `Rect`, `Time`, `Clock` |
79
+ | Window | `RenderWindow`, `Window` (bare, GL-only), `VideoMode`, `Event`, `Keyboard`, `Mouse`, `Joystick`, `Cursor`, `Clipboard` |
80
+ | Graphics | `Color`, `Image`, `Texture`, `RenderTexture`, `Sprite`, `CircleShape`, `RectangleShape`, `ConvexShape`, `Vertex`, `VertexArray`, `Font`, `Text`, `View`, `BlendMode`, `RenderStates`, `Shader`, `Transform` |
81
+ | Audio | `SoundBuffer`, `Sound`, `Music`, `Listener`, `SoundRecorder`, `SoundBufferRecorder` (3D positional audio supported on Sound and Music) |
82
+ | Helpers | `Assets` (search-path + cache), `Game` (lifecycle main loop) |
83
+
84
+ **Network**: `IpAddress`, `TcpSocket`, `TcpListener`, `UdpSocket` for stream / datagram networking.
85
+
86
+ ## What's intentionally *not* wrapped
87
+
88
+ CSFML 3 has a few corners we deliberately don't expose. Each is either
89
+ (a) niche enough not to justify the surface area, (b) better served by
90
+ a Ruby standard library, or (c) requires patterns that don't translate
91
+ cleanly to FFI.
92
+
93
+ **Use Ruby stdlib instead**
94
+ - `sf::Http` — `Net::HTTP` is a better Ruby fit
95
+ - `sf::Ftp` — `Net::FTP` likewise
96
+ - `sf::SocketSelector` — `IO.select` or [Async](https://github.com/socketry/async)
97
+
98
+ **Callback-based APIs that fight FFI / the GVL**
99
+ - Raw `sf::SoundRecorder` (per-buffer callbacks on the audio thread) —
100
+ use `SFML::SoundBufferRecorder` for "record into memory, save on stop"
101
+ - `sf::SoundStream` (custom audio source via inheritance) — niche; if
102
+ you need it, generate samples to a file and play via `Music`
103
+
104
+ **Mobile / niche inputs** (SFML 3 itself treats these as experimental)
105
+ - `sf::Touch`, `sf::Sensor` (accelerometer, gyro, etc.)
106
+
107
+ **Advanced graphics features**
108
+ - `sf::VertexBuffer` (static GPU vertex buffer) — `VertexArray` covers
109
+ the common case; if you need static-mesh perf, open an issue
110
+ - Geometry shaders — only vertex and fragment stages on `SFML::Shader`
111
+ - `sf::Shader#setUniformArray` (bulk uniforms) — set elements one by one
112
+ - Stencil buffer ops (`clearStencil`, custom `StencilMode`) — accept
113
+ CSFML defaults
114
+ - `sf::Image#saveToMemory` — only `Image#save(path)` is wrapped
115
+
116
+ **Advanced audio features**
117
+ - Sound / Music cones, velocity, Doppler factor, custom DSP via
118
+ `setEffectProcessor` — basic 3D positional + attenuation is in;
119
+ the rest is rarely used in 2D gamedev
120
+ - `sf::Listener` cone — same reasoning
121
+
122
+ **Embedding / integration corners**
123
+ - `RenderWindow.createFromHandle` (embed in another framework's window)
124
+ - Custom `sf::InputStream` for loading assets from non-file sources
125
+ - Window icon, min/max size, native handle accessors on `SFML::Window`
126
+
127
+ **Other Ruby bindings worth knowing about**
128
+ - SFML 2.x is *not* covered. The previous-generation gem
129
+ [rbSFML](https://github.com/Groogy/rbSFML) targets SFML 2; it's
130
+ unmaintained and only works with Ruby ≤ 2.2.
131
+
132
+ If anything in the list above is blocking you, **open an issue** —
133
+ "niche" is just a default, not a closed door.
134
+
135
+ ## Examples
136
+
137
+ Each example is a self-contained folder under [examples/](examples/),
138
+ numbered roughly in learning order. Assets each example needs sit next
139
+ to its script. Run from the gem root:
140
+
141
+ ```sh
142
+ bundle exec ruby examples/<NN_name>/<name>.rb
143
+ ```
144
+
145
+ | # | Example | What it shows |
146
+ | --- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
147
+ | 01 | [hello_window](examples/01_hello_window/hello_window.rb) | Empty window, manual event loop |
148
+ | 02 | [events_demo](examples/02_events_demo/events_demo.rb) | Pattern matching on input events |
149
+ | 03 | [bouncing_ball](examples/03_bouncing_ball/bouncing_ball.rb) | dt-based physics, `CircleShape` + `RectangleShape` |
150
+ | 04 | [game_class](examples/04_game_class/game_class.rb) | Same idea on top of `SFML::Game` |
151
+ | 05 | [mouse_demo](examples/05_mouse_demo/mouse_demo.rb) | Polling vs. events; paint with the mouse |
152
+ | 06 | [pong](examples/06_pong/pong.rb) | Two-player Pong with in-window score (`Text`) and bounce `Sound` |
153
+ | 07 | [scrolling_world](examples/07_scrolling_world/scrolling_world.rb) | `View` as a 2D camera: drag-pan, wheel-zoom around cursor, FPS HUD |
154
+ | 08 | [joystick_demo](examples/08_joystick_demo/joystick_demo.rb) | Live gamepad inspector (axes, buttons, connect/disconnect) |
155
+ | 09 | [image_viewer](examples/09_image_viewer/image_viewer.rb) | Load a PNG, mutate the `Image`, re-upload to `Texture` on a key |
156
+ | 10 | [pixel_paint](examples/10_pixel_paint/pixel_paint.rb) | Paint into a CPU `Image`, blit to GPU `Texture` each dirty frame |
157
+ | 11 | [particles](examples/11_particles/particles.rb) | Thousands of points in one draw call via `VertexArray` + `ConvexShape` ground |
158
+ | 12 | [render_texture](examples/12_render_texture/render_texture.rb) | Off-screen `RenderTexture` for trail / motion-blur effects |
159
+ | 13 | [tilemap](examples/13_tilemap/tilemap.rb) | Textured `VertexArray` tilemap + additive `BlendMode` torch |
160
+ | 14 | [shader_wave](examples/14_shader_wave/shader_wave.rb) | Pure GLSL fragment `Shader` — procedural ripple + plasma |
161
+ | 15 | [cursors_clipboard](examples/15_cursors_clipboard/cursors_clipboard.rb) | All 21 system `Cursor` shapes + `Clipboard` copy/paste |
162
+ | 16 | [spatial_audio](examples/16_spatial_audio/spatial_audio.rb) | 3D positional `Sound` + `Listener` — three drones around the cursor |
163
+ | 17 | [voice_memo](examples/17_voice_memo/voice_memo.rb) | Record from microphone via `SoundBufferRecorder`, save + play back |
164
+ | 18 | [draw_primitives](examples/18_draw_primitives/draw_primitives.rb) | Raw `draw_primitives` — line burst rebuilt every frame |
165
+ | 19 | [udp_loopback](examples/19_udp_loopback/udp_loopback.rb) | UDP send/receive on localhost via `Network::UdpSocket` |
166
+ | 20 | [bare_window](examples/20_bare_window/bare_window.rb) | `SFML::Window` (no 2D batcher) — events for raw-OpenGL apps |
167
+
168
+ ## Idioms baked in
169
+
170
+ - **Symbols, not enums:** `Keyboard.key_pressed?(:escape)`, not `Keyboard::Key::Escape`.
171
+ - **Pattern matching for events:**
172
+ ```ruby
173
+ case event
174
+ in {type: :key_pressed, code: :escape}
175
+ in {type: :resized, size: {x:, y:}}
176
+ in {type: :mouse_button_pressed, button: :left, position: {x:, y:}}
177
+ end
178
+ ```
179
+ - **Vectors with operators:** `pos + velocity * dt`, `2 * vec`, `vec.length`, deconstruction in `case/in`.
180
+ - **Kwargs constructors:** `Sprite.new(texture, position: [0, 0], color: SFML::Color.red)`, `CircleShape.new(radius: 10, fill_color: ...)` — no setter chains.
181
+ - **Asset manager with cache:** `SFML::Assets.font("DejaVuSans")`, `SFML::Assets.sound("blip")` — load each thing once, refer by name.
182
+ - **GC-managed resources:** every CSFML pointer goes through `FFI::AutoPointer`, so `sfXxx_destroy` is called automatically.
183
+
184
+ ## Versioning
185
+
186
+ The gem version is `MAJOR.MINOR.PATCH.GEM_PATCH` — the first three segments mirror the CSFML release the gem was built against; the fourth is our own patch level for fixes / additions on top of the same upstream:
187
+
188
+ | gem version | targets CSFML | meaning |
189
+ | ----------- | ------------- | ----------------------------------------------- |
190
+ | `3.0.0.0` | 3.0.0 | First cut against CSFML 3.0.0 |
191
+ | `3.0.0.1` | 3.0.0 | Bug fix on top of CSFML 3.0.0 |
192
+ | `3.0.1.0` | 3.0.1 | CSFML 3.0.1 ships, we re-cut |
193
+ | `3.1.0.0` | 3.1.0 | New CSFML minor — added bindings for new APIs |
194
+
195
+ `SFML::CSFML_VERSION` exposes the upstream string at runtime.
196
+
197
+ Bundler-pinning patterns:
198
+
199
+ ```ruby
200
+ gem "ruby-sfml", "~> 3.0" # any 3.x.x.x — typical
201
+ gem "ruby-sfml", "~> 3.0.0" # only 3.0.0.x — hold across a CSFML minor
202
+ gem "ruby-sfml", "~> 3.0.0.0" # only our patches on CSFML 3.0.0 — paranoid pin
203
+ ```
204
+
205
+ ## Process exit
206
+
207
+ ruby-sfml installs a single `at_exit` hook that:
208
+
209
+ 1. Stops every live `SFML::Sound` / `SFML::Music` so the audio thread quiets before anything is freed.
210
+ 2. Calls `Kernel#exit!` with the appropriate status, bypassing Ruby's natural finalizer pass.
211
+
212
+ This is intentional. CSFML's GL context, font glyph atlases, and OpenAL state are reclaimed by the OS on process exit; running each `FFI::AutoPointer` finalizer in a non-deterministic order tends to crash inside libGL/libopenal. The OS doesn't care, and now neither do we.
213
+
214
+ The trade-off: any user `at_exit` hook registered **before** `require "sfml"` will be skipped. Hooks registered after the require run first (Ruby's at_exit is LIFO) and are unaffected. Put your `require` at the top of the file (the normal place for it) and there's nothing to think about.
215
+
216
+ ## Architecture
217
+
218
+ Two layers. Users only touch the top one.
219
+
220
+ ```
221
+ SFML::C # thin FFI wrapper around CSFML, 1:1 with the C API
222
+ SFML # idiomatic Ruby on top
223
+ ```
224
+
225
+ Each render target (RenderWindow + RenderTexture) includes a `Graphics::RenderTarget` mixin that dispatches `clear`, `display`, `draw`, `view=`, `map_pixel_to_coords` etc. through the includer's `CSFML_PREFIX`. Adding a new target (say a future `RenderImage`) is ~30 lines.
226
+
227
+ When SFML 3.1 / CSFML 3.1 ships, only the bottom layer typically needs to move.
228
+
229
+ ## Development
230
+
231
+ ```sh
232
+ bundle install
233
+ bundle exec rspec # 287 examples
234
+ bundle exec rake rdoc # generate HTML docs in doc/ (Aliki theme via RDoc 7)
235
+ ```
236
+
237
+ The spec suite hits real CSFML for everything that isn't pure Ruby — `Clock` reads the real monotonic clock, `Text#local_bounds` measures real glyphs, audio loads a WAV — so a green run also confirms the FFI bindings line up. `spec/fixtures/` holds the only assets the suite touches (a font and a tiny WAV) so tests are independent of `examples/`.
238
+
239
+ CI runs the full suite on Ubuntu and macOS × Ruby 3.2 / 3.3 / 3.4. Linux builds CSFML 3 from source (cached), then runs specs under `xvfb-run` so the headless runner has an X server for `RenderWindow`.
240
+
241
+ ## License
242
+
243
+ MIT. See [LICENSE.txt](LICENSE.txt).
244
+
245
+ The gem also bundles [DejaVu Sans](https://dejavu-fonts.github.io/) under its [permissive license](lib/sfml/assets/fonts/DejaVuSans.LICENSE.txt) — used as the default font when you don't supply your own.
@@ -0,0 +1,69 @@
1
+ require "mkmf"
2
+
3
+ # This is not a real native extension. We use the extconf.rb hook only as a
4
+ # pre-flight check: if CSFML 3.x is not on the system, fail `gem install` here
5
+ # with a clear message instead of letting the user discover the problem at
6
+ # runtime.
7
+
8
+ REQUIRED_LIBS = %w[csfml-system csfml-window csfml-graphics csfml-audio csfml-network]
9
+
10
+ missing = REQUIRED_LIBS.reject { |lib| have_library(lib) }
11
+
12
+ unless missing.empty?
13
+ abort <<~MSG
14
+
15
+ ============================================================================
16
+ ruby-sfml requires CSFML 3.x to be installed on your system.
17
+
18
+ Missing libraries: #{missing.join(', ')}
19
+
20
+ Install CSFML, then re-run `gem install ruby-sfml`:
21
+
22
+ Ubuntu / Debian: sudo apt install libcsfml-dev
23
+ macOS (brew): brew install csfml
24
+ Arch Linux: sudo pacman -S csfml
25
+ Windows: https://www.sfml-dev.org/download/csfml/
26
+
27
+ See https://github.com/m1kh41l/ruby-sfml#requirements for full instructions.
28
+ ============================================================================
29
+
30
+ MSG
31
+ end
32
+
33
+ # All libcsfml-* are present, but they might be 2.x (Ubuntu 22.04 / 24.04
34
+ # ship 2.5 in their repos). Probe a CSFML 3.0+ symbol — sfClock_isRunning
35
+ # is part of the SFML 3 sf::Clock rewrite and isn't in any 2.x release.
36
+ unless have_func("sfClock_isRunning")
37
+ abort <<~MSG
38
+
39
+ ============================================================================
40
+ ruby-sfml requires CSFML 3.0 or newer.
41
+
42
+ The libcsfml on your system is older than 3.0 — sfClock_isRunning,
43
+ introduced in the SFML 3.0 / CSFML 3.0 release (March 2025), is not
44
+ exported by the linked library.
45
+
46
+ Upgrade options:
47
+
48
+ Ubuntu 25.04+ / Debian: sudo apt install libcsfml-dev
49
+ Ubuntu 22.04 / 24.04: repo is too old; build from source:
50
+ https://github.com/SFML/CSFML/releases/tag/3.0.0
51
+ macOS (brew): brew upgrade csfml
52
+ Arch Linux: sudo pacman -S csfml
53
+
54
+ Or grab a prebuilt 3.x release from
55
+ https://github.com/SFML/CSFML/releases.
56
+ ============================================================================
57
+
58
+ MSG
59
+ end
60
+
61
+ # Emit a no-op Makefile so RubyGems considers the extension "built".
62
+ File.write("Makefile", <<~MAKE)
63
+ all:
64
+ \t@true
65
+ install:
66
+ \t@true
67
+ clean:
68
+ \t@true
69
+ MAKE
@@ -0,0 +1,78 @@
1
+ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
2
+ Upstream-Name: DejaVu fonts
3
+ Upstream-Author: Stepan Roh <src@users.sourceforge.net> (original author),
4
+ see /usr/share/doc/fonts-dejavu-core/AUTHORS for full list
5
+ Source: https://dejavu-fonts.github.io/
6
+
7
+ Files: *
8
+ Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
9
+ Bitstream Vera is a trademark of Bitstream, Inc.
10
+ DejaVu changes are in public domain.
11
+ License: bitstream-vera
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of the fonts accompanying this license ("Fonts") and associated
14
+ documentation files (the "Font Software"), to reproduce and distribute the
15
+ Font Software, including without limitation the rights to use, copy, merge,
16
+ publish, distribute, and/or sell copies of the Font Software, and to permit
17
+ persons to whom the Font Software is furnished to do so, subject to the
18
+ following conditions:
19
+ .
20
+ The above copyright and trademark notices and this permission notice shall
21
+ be included in all copies of one or more of the Font Software typefaces.
22
+ .
23
+ The Font Software may be modified, altered, or added to, and in particular
24
+ the designs of glyphs or characters in the Fonts may be modified and
25
+ additional glyphs or characters may be added to the Fonts, only if the fonts
26
+ are renamed to names not containing either the words "Bitstream" or the word
27
+ "Vera".
28
+ .
29
+ This License becomes null and void to the extent applicable to Fonts or Font
30
+ Software that has been modified and is distributed under the "Bitstream
31
+ Vera" names.
32
+ .
33
+ The Font Software may be sold as part of a larger software package but no
34
+ copy of one or more of the Font Software typefaces may be sold by itself.
35
+ .
36
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
37
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
38
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
39
+ TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
40
+ FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
41
+ ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
42
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
43
+ THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
44
+ FONT SOFTWARE.
45
+ .
46
+ Except as contained in this notice, the names of Gnome, the Gnome
47
+ Foundation, and Bitstream Inc., shall not be used in advertising or
48
+ otherwise to promote the sale, use or other dealings in this Font Software
49
+ without prior written authorization from the Gnome Foundation or Bitstream
50
+ Inc., respectively. For further information, contact: fonts at gnome dot
51
+ org.
52
+
53
+ Files: debian/*
54
+ Copyright: (C) 2005-2006 Peter Cernak <pce@users.sourceforge.net>
55
+ (C) 2006-2011 Davide Viti <zinosat@tiscali.it>
56
+ (C) 2011-2013 Christian Perrier <bubulle@debian.org>
57
+ (C) 2013 Fabian Greffrath <fabian+debian@greffrath.com>
58
+ License: GPL-2+
59
+ This program is free software; you can redistribute it
60
+ and/or modify it under the terms of the GNU General Public
61
+ License as published by the Free Software Foundation; either
62
+ version 2 of the License, or (at your option) any later
63
+ version.
64
+ .
65
+ This program is distributed in the hope that it will be
66
+ useful, but WITHOUT ANY WARRANTY; without even the implied
67
+ warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
68
+ PURPOSE. See the GNU General Public License for more
69
+ details.
70
+ .
71
+ You should have received a copy of the GNU General Public
72
+ License along with this package; if not, write to the Free
73
+ Software Foundation, Inc., 51 Franklin St, Fifth Floor,
74
+ Boston, MA 02110-1301 USA
75
+ .
76
+ On Debian systems, the full text of the GNU General Public
77
+ License version 2 can be found in the file
78
+ /usr/share/common-licenses/GPL-2'.
@@ -0,0 +1,121 @@
1
+ module SFML
2
+ # Cached, search-path-driven asset loader. Use it to avoid repeating file
3
+ # paths and to load each asset exactly once.
4
+ #
5
+ # font = SFML::Assets.font("DejaVuSans")
6
+ # tex = SFML::Assets.texture("hero") # finds hero.png/.jpg/.bmp
7
+ # blip = SFML::Assets.sound("blip") # finds blip.wav/.ogg/...
8
+ # music = SFML::Assets.music("track") # NOT cached (stateful)
9
+ #
10
+ # By default the search root is `<dir of $0>/assets/`. Override:
11
+ #
12
+ # SFML::Assets.root = File.expand_path("data", __dir__)
13
+ # SFML::Assets.add_search_path("/usr/local/share/mygame")
14
+ #
15
+ # Cache survives until you call .clear or the process exits.
16
+ module Assets
17
+ TEXTURE_EXTS = %w[.png .jpg .jpeg .bmp .gif .tga].freeze
18
+ SOUND_EXTS = %w[.wav .ogg .flac .mp3].freeze
19
+ MUSIC_EXTS = SOUND_EXTS
20
+ FONT_EXTS = %w[.ttf .otf].freeze
21
+
22
+ class NotFound < SFML::Error; end
23
+
24
+ class << self
25
+ def search_paths
26
+ @search_paths ||= [default_root]
27
+ end
28
+
29
+ def search_paths=(paths)
30
+ @search_paths = Array(paths).map { |p| File.expand_path(p) }
31
+ @cache&.clear
32
+ end
33
+
34
+ def root=(path)
35
+ self.search_paths = [path]
36
+ end
37
+
38
+ def add_search_path(path)
39
+ search_paths << File.expand_path(path) unless search_paths.include?(File.expand_path(path))
40
+ end
41
+
42
+ # Drop everything from the in-memory cache. New loads will re-read
43
+ # from disk and recreate textures, fonts, sound buffers.
44
+ def clear
45
+ @cache&.clear
46
+ self
47
+ end
48
+
49
+ def font(name)
50
+ cache[[:font, name]] ||= load_font(name)
51
+ end
52
+
53
+ def texture(name)
54
+ cache[[:texture, name]] ||= load_texture(name)
55
+ end
56
+
57
+ def sound(name)
58
+ cache[[:sound, name]] ||= load_sound_buffer(name)
59
+ end
60
+
61
+ # Music is intentionally NOT cached — it owns a streaming position and
62
+ # play state, so each caller wants its own instance.
63
+ def music(name)
64
+ path = locate(name, MUSIC_EXTS) or raise NotFound,
65
+ "Music #{name.inspect} not found. Searched: #{search_paths.inspect}"
66
+ Music.load(path)
67
+ end
68
+
69
+ private
70
+
71
+ def cache
72
+ @cache ||= {}
73
+ end
74
+
75
+ def default_root
76
+ File.expand_path("assets", File.dirname($PROGRAM_NAME || "."))
77
+ end
78
+
79
+ def load_font(name)
80
+ path = locate(name, FONT_EXTS)
81
+ return Font.load(path) if path
82
+ # Fall back to a system-font search — fonts are large and rarely
83
+ # shipped with games, so this often does the right thing.
84
+ Font.find(name) or raise NotFound,
85
+ "Font #{name.inspect} not found in #{search_paths.inspect} or system fonts"
86
+ end
87
+
88
+ def load_texture(name)
89
+ path = locate(name, TEXTURE_EXTS) or raise NotFound,
90
+ "Texture #{name.inspect} not found. Searched: #{search_paths.inspect}"
91
+ Texture.load(path)
92
+ end
93
+
94
+ def load_sound_buffer(name)
95
+ path = locate(name, SOUND_EXTS) or raise NotFound,
96
+ "Sound #{name.inspect} not found. Searched: #{search_paths.inspect}"
97
+ SoundBuffer.load(path)
98
+ end
99
+
100
+ # Finds an asset on disk. If `name` already has a matching extension,
101
+ # uses it as-is; otherwise tries each extension in order.
102
+ def locate(name, extensions)
103
+ name_str = name.to_s
104
+ candidates =
105
+ if extensions.any? { |ext| name_str.end_with?(ext) }
106
+ [name_str]
107
+ else
108
+ extensions.map { |ext| "#{name_str}#{ext}" }
109
+ end
110
+
111
+ search_paths.each do |dir|
112
+ candidates.each do |cand|
113
+ full = File.join(dir, cand)
114
+ return full if File.file?(full)
115
+ end
116
+ end
117
+ nil
118
+ end
119
+ end
120
+ end
121
+ end