ruby-sfml 3.0.0.4 → 3.0.0.5

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.
@@ -0,0 +1,114 @@
1
+ module SFML
2
+ # Abstract callback-driven shape — for when neither CircleShape,
3
+ # RectangleShape, nor ConvexShape fits. Subclass and override
4
+ # `#point_count` (returns Integer) and `#point(i)` (returns a
5
+ # Vector2 or `[x, y]`). CSFML asks Ruby every frame for the point
6
+ # list, so you can drive geometry from live data.
7
+ #
8
+ # class Pentagon < SFML::Shape
9
+ # def point_count = 5
10
+ # def point(i)
11
+ # angle = i * 2 * Math::PI / 5 - Math::PI / 2
12
+ # [Math.cos(angle) * 50, Math.sin(angle) * 50]
13
+ # end
14
+ # end
15
+ #
16
+ # pent = Pentagon.new(fill_color: SFML::Color.red, position: [400, 300])
17
+ # window.draw(pent)
18
+ #
19
+ # When your underlying geometry changes (e.g. the data feeding
20
+ # `#point` updates), call `#update` to invalidate the cached
21
+ # outline / bounds — otherwise the next draw will still use the
22
+ # previously sampled points.
23
+ #
24
+ # CAVEATS
25
+ # * Callbacks run on whichever thread invokes the draw. With
26
+ # single-threaded rendering (the SFML default) this is fine; if
27
+ # you're driving the renderer from a different thread, make sure
28
+ # `#point_count` / `#point` are thread-safe.
29
+ # * Keep a Ruby reference to the Shape — if it's GC'd while CSFML
30
+ # still holds the function pointers, the next draw crashes.
31
+ class Shape
32
+ include Graphics::Transformable
33
+ include Graphics::ShapeInspectable
34
+ CSFML_PREFIX = :sfShape
35
+
36
+ def initialize(**opts)
37
+ # Hold strong refs so the GC doesn't disappear our FFI callbacks.
38
+ @get_point_count_cb = FFI::Function.new(:size_t, [:pointer]) do |_user|
39
+ Integer(point_count)
40
+ end
41
+ @get_point_cb = FFI::Function.new(C::System::Vector2f.by_value, [:size_t, :pointer]) do |i, _user|
42
+ p = point(i)
43
+ pt = p.is_a?(Vector2) ? p : Vector2.new(*p)
44
+ v = C::System::Vector2f.new
45
+ v[:x] = pt.x.to_f; v[:y] = pt.y.to_f
46
+ v
47
+ end
48
+
49
+ ptr = C::Graphics.sfShape_create(@get_point_count_cb, @get_point_cb, nil)
50
+ raise Error, "sfShape_create returned NULL" if ptr.null?
51
+ @handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfShape_destroy))
52
+
53
+ self.fill_color = opts[:fill_color] if opts.key?(:fill_color)
54
+ self.outline_color = opts[:outline_color] if opts.key?(:outline_color)
55
+ self.outline_thickness = opts[:outline_thickness] if opts.key?(:outline_thickness)
56
+ self.texture = opts[:texture] if opts.key?(:texture)
57
+ self.texture_rect = opts[:texture_rect] if opts.key?(:texture_rect)
58
+ self.position = opts[:position] if opts.key?(:position)
59
+ self.origin = opts[:origin] if opts.key?(:origin)
60
+ self.rotation = opts[:rotation] if opts.key?(:rotation)
61
+ self.scale = opts[:scale] if opts.key?(:scale)
62
+ end
63
+
64
+ # ---- Subclass hooks ----
65
+
66
+ def point_count
67
+ raise NoMethodError, "#{self.class} must override #point_count"
68
+ end
69
+
70
+ def point(_index)
71
+ raise NoMethodError, "#{self.class} must override #point(index)"
72
+ end
73
+
74
+ # ---- Public API ----
75
+
76
+ def fill_color = Color.from_native(C::Graphics.sfShape_getFillColor(@handle))
77
+
78
+ def fill_color=(c)
79
+ C::Graphics.sfShape_setFillColor(@handle, c.to_native)
80
+ end
81
+
82
+ def outline_color = Color.from_native(C::Graphics.sfShape_getOutlineColor(@handle))
83
+
84
+ def outline_color=(c)
85
+ C::Graphics.sfShape_setOutlineColor(@handle, c.to_native)
86
+ end
87
+
88
+ def outline_thickness = C::Graphics.sfShape_getOutlineThickness(@handle)
89
+
90
+ def outline_thickness=(t)
91
+ C::Graphics.sfShape_setOutlineThickness(@handle, t.to_f)
92
+ end
93
+
94
+ # Recompute the outline + bounds from the current `#point_count` /
95
+ # `#point(i)`. Call after the data driving your callbacks changes.
96
+ # No-op on construction (CSFML samples the points once at the
97
+ # first draw).
98
+ def update
99
+ C::Graphics.sfShape_update(@handle)
100
+ self
101
+ end
102
+
103
+ def draw_on(target, states_ptr = nil) # :nodoc:
104
+ target._draw_native(:Shape, @handle, states_ptr)
105
+ end
106
+
107
+ # Abstract Shape doesn't implement #dup — every subclass has its
108
+ # own state and copy semantics. (CSFML has no `sfShape_copy`.)
109
+ undef_method :dup
110
+ undef_method :clone
111
+
112
+ attr_reader :handle # :nodoc:
113
+ end
114
+ end
@@ -0,0 +1,92 @@
1
+ module SFML
2
+ module Graphics
3
+ # Methods shared across CircleShape / RectangleShape / ConvexShape:
4
+ # texture binding, geometry introspection, transform readout, dup.
5
+ # Including class must define CSFML_PREFIX (Symbol) and hold the FFI
6
+ # handle in @handle (same contract as Graphics::Transformable).
7
+ #
8
+ # Texture binding follows the Sprite pattern: keep a Ruby reference
9
+ # to the bound texture in @texture so the GC doesn't collect the GPU
10
+ # resource while the shape still draws with it.
11
+ module ShapeInspectable
12
+ def texture
13
+ ptr = _csfml(:getTexture, @handle)
14
+ return nil if ptr.null?
15
+ Texture.send(:_borrow, ptr)
16
+ end
17
+
18
+ # `reset_rect: true` snaps the texture-rect to the full texture size,
19
+ # which is usually what you want when binding a fresh texture.
20
+ def set_texture(tex, reset_rect: false)
21
+ raise ArgumentError, "expected SFML::Texture or nil" unless tex.nil? || tex.is_a?(Texture)
22
+ _csfml(:setTexture, @handle, tex ? tex.handle : nil, reset_rect)
23
+ @texture = tex
24
+ self
25
+ end
26
+
27
+ def texture=(tex)
28
+ set_texture(tex, reset_rect: false)
29
+ end
30
+
31
+ def texture_rect
32
+ Rect.from_native(_csfml(:getTextureRect, @handle))
33
+ end
34
+
35
+ def texture_rect=(rect)
36
+ raise ArgumentError, "texture_rect= requires a SFML::Rect" unless rect.is_a?(Rect)
37
+ native = C::Graphics::IntRect.new
38
+ native[:position][:x] = Integer(rect.x)
39
+ native[:position][:y] = Integer(rect.y)
40
+ native[:size][:x] = Integer(rect.width)
41
+ native[:size][:y] = Integer(rect.height)
42
+ _csfml(:setTextureRect, @handle, native)
43
+ end
44
+
45
+ # Single polygon vertex by index. The full list is exposed by the
46
+ # subclass-specific `#points` method on ConvexShape; CircleShape
47
+ # synthesises N points from its radius+point_count.
48
+ def point(index)
49
+ Vector2.from_native(_csfml(:getPoint, @handle, Integer(index)))
50
+ end
51
+
52
+ # Centroid of the polygon's vertex list (CSFML 3.0+).
53
+ def geometric_center
54
+ Vector2.from_native(_csfml(:getGeometricCenter, @handle))
55
+ end
56
+
57
+ # AABB before transform — the rectangle the geometry would occupy
58
+ # if the shape were drawn at the origin with no rotation/scale.
59
+ def local_bounds
60
+ Rect.from_native(_csfml(:getLocalBounds, @handle))
61
+ end
62
+
63
+ # AABB after the shape's full transform — the world-space rect the
64
+ # renderer uses for culling and hit-tests.
65
+ def global_bounds
66
+ Rect.from_native(_csfml(:getGlobalBounds, @handle))
67
+ end
68
+
69
+ def transform
70
+ _csfml(:getTransform, @handle)
71
+ end
72
+
73
+ def inverse_transform
74
+ _csfml(:getInverseTransform, @handle)
75
+ end
76
+
77
+ # Deep copy. Texture binding is shared (textures are GPU objects,
78
+ # cheap to alias); transform/colour state is independent.
79
+ def dup
80
+ ptr = _csfml(:copy, @handle)
81
+ raise Error, "#{self.class.name}#dup returned NULL" if ptr.null?
82
+
83
+ destroy_fn = C::Graphics.method(:"#{self.class::CSFML_PREFIX}_destroy")
84
+ copy = self.class.allocate
85
+ copy.instance_variable_set(:@handle, FFI::AutoPointer.new(ptr, destroy_fn))
86
+ copy.instance_variable_set(:@texture, @texture) if defined?(@texture)
87
+ copy
88
+ end
89
+ alias clone dup
90
+ end
91
+ end
92
+ end
@@ -5,8 +5,17 @@ module SFML
5
5
  # tex = SFML::Texture.load("assets/hero.png")
6
6
  # tex = SFML::Texture.load("assets/tile.png", smooth: true, repeated: true)
7
7
  class Texture
8
- def self.load(path, smooth: false, repeated: false)
9
- ptr = C::Graphics.sfTexture_createFromFile(path.to_s, nil)
8
+ # Load a texture from a file on disk.
9
+ #
10
+ # Pass `srgb: true` if the source pixels are in sRGB-encoded
11
+ # gamma space (most photo/PNG art assets) and you want the GPU
12
+ # to gamma-decode on sample so blending happens in linear space.
13
+ # Pair with `RenderWindow#new(..., srgb_capable: true)` and your
14
+ # final framebuffer will gamma-encode on present.
15
+ def self.load(path, smooth: false, repeated: false, srgb: false)
16
+ ptr = srgb \
17
+ ? C::Graphics.sfTexture_createSrgbFromFile(path.to_s, nil) \
18
+ : C::Graphics.sfTexture_createFromFile(path.to_s, nil)
10
19
  raise Error, "Could not load texture from #{path.inspect}" if ptr.null?
11
20
 
12
21
  tex = allocate
@@ -20,10 +29,10 @@ module SFML
20
29
  # `update(image)` afterwards to upload pixels. Useful when
21
30
  # you'll be filling the texture from a procedurally-generated
22
31
  # Image or repeatedly streaming pixel data into it.
23
- def self.create(width, height)
32
+ def self.create(width, height, srgb: false)
24
33
  size = C::System::Vector2u.new
25
34
  size[:x] = Integer(width); size[:y] = Integer(height)
26
- ptr = C::Graphics.sfTexture_create(size)
35
+ ptr = srgb ? C::Graphics.sfTexture_createSrgb(size) : C::Graphics.sfTexture_create(size)
27
36
  raise Error, "sfTexture_create returned NULL — out of GPU memory?" if ptr.null?
28
37
 
29
38
  tex = allocate
@@ -34,12 +43,14 @@ module SFML
34
43
  # Decode + upload a Ruby String of bytes (PNG, JPG, BMP, …) as
35
44
  # a texture. Useful for embedded assets / network responses
36
45
  # that bypass the disk.
37
- def self.from_memory(bytes, smooth: false, repeated: false)
46
+ def self.from_memory(bytes, smooth: false, repeated: false, srgb: false)
38
47
  raise ArgumentError, "expected a String, got #{bytes.class}" unless bytes.is_a?(String)
39
48
 
40
49
  buf = FFI::MemoryPointer.new(:uint8, bytes.bytesize)
41
50
  buf.write_bytes(bytes)
42
- ptr = C::Graphics.sfTexture_createFromMemory(buf, bytes.bytesize, nil)
51
+ ptr = srgb \
52
+ ? C::Graphics.sfTexture_createSrgbFromMemory(buf, bytes.bytesize, nil) \
53
+ : C::Graphics.sfTexture_createFromMemory(buf, bytes.bytesize, nil)
43
54
  raise Error, "sfTexture_createFromMemory returned NULL — unsupported format?" if ptr.null?
44
55
 
45
56
  tex = allocate
@@ -49,12 +60,32 @@ module SFML
49
60
  tex
50
61
  end
51
62
 
63
+ # Load a texture from any Ruby IO-like object (File, StringIO,
64
+ # or anything answering read/seek/pos/size). Useful for assets
65
+ # inside a zip archive, served over a socket, or generated on
66
+ # the fly.
67
+ def self.from_stream(io, smooth: false, repeated: false, srgb: false)
68
+ stream = SFML::InputStream.new(io)
69
+ ptr = srgb \
70
+ ? C::Graphics.sfTexture_createSrgbFromStream(stream.to_ptr, nil) \
71
+ : C::Graphics.sfTexture_createFromStream(stream.to_ptr, nil)
72
+ raise Error, "sfTexture_createFromStream returned NULL — unsupported format?" if ptr.null?
73
+
74
+ tex = allocate
75
+ tex.send(:_take_ownership, ptr)
76
+ tex.smooth = smooth
77
+ tex.repeated = repeated
78
+ tex
79
+ end
80
+
52
81
  # Upload a CPU-side SFML::Image to the GPU as a new Texture. Keeps
53
82
  # the RGBA byte order and dimensions of the source image.
54
- def self.from_image(image, smooth: false, repeated: false)
83
+ def self.from_image(image, smooth: false, repeated: false, srgb: false)
55
84
  raise ArgumentError, "Texture.from_image needs a SFML::Image" unless image.is_a?(Image)
56
85
 
57
- ptr = C::Graphics.sfTexture_createFromImage(image.handle, nil)
86
+ ptr = srgb \
87
+ ? C::Graphics.sfTexture_createSrgbFromImage(image.handle, nil) \
88
+ : C::Graphics.sfTexture_createFromImage(image.handle, nil)
58
89
  raise Error, "sfTexture_createFromImage returned NULL" if ptr.null?
59
90
 
60
91
  tex = allocate
@@ -144,10 +175,15 @@ module SFML
144
175
  # Reallocate this texture's GPU memory at a new size. Returns
145
176
  # `false` if the GPU rejects the size (driver limit / OOM);
146
177
  # the texture's contents become undefined on success.
147
- def resize(width, height)
178
+ # Pass `srgb: true` to re-allocate as an sRGB-encoded texture.
179
+ def resize(width, height, srgb: false)
148
180
  size = C::System::Vector2u.new
149
181
  size[:x] = Integer(width); size[:y] = Integer(height)
150
- C::Graphics.sfTexture_resize(@handle, size)
182
+ if srgb
183
+ C::Graphics.sfTexture_resizeSrgb(@handle, size)
184
+ else
185
+ C::Graphics.sfTexture_resize(@handle, size)
186
+ end
151
187
  end
152
188
 
153
189
  # Atomically swap the GPU memory between two textures —
@@ -0,0 +1,48 @@
1
+ module SFML
2
+ # Standalone transformable — a pure CSFML transform container (no
3
+ # geometry attached). Useful as a base for custom drawables that
4
+ # combine a transform with their own rendering: parent the child's
5
+ # vertices to this object's `#transform` and you get position /
6
+ # rotation / scale / origin for free.
7
+ #
8
+ # Most users want the `Graphics::Transformable` mixin instead — this
9
+ # standalone class exists for symmetry with the C++ API.
10
+ #
11
+ # t = SFML::TransformableObject.new(position: [400, 300], rotation: 45)
12
+ # t.transform #=> SFML::C::Graphics::Transform (use via RenderStates)
13
+ class TransformableObject
14
+ include Graphics::Transformable
15
+ CSFML_PREFIX = :sfTransformable
16
+
17
+ def initialize(**opts)
18
+ ptr = C::Graphics.sfTransformable_create
19
+ raise Error, "sfTransformable_create returned NULL" if ptr.null?
20
+ @handle = FFI::AutoPointer.new(ptr, C::Graphics.method(:sfTransformable_destroy))
21
+
22
+ self.position = opts[:position] if opts.key?(:position)
23
+ self.origin = opts[:origin] if opts.key?(:origin)
24
+ self.rotation = opts[:rotation] if opts.key?(:rotation)
25
+ self.scale = opts[:scale] if opts.key?(:scale)
26
+ end
27
+
28
+ def transform
29
+ C::Graphics.sfTransformable_getTransform(@handle)
30
+ end
31
+
32
+ def inverse_transform
33
+ C::Graphics.sfTransformable_getInverseTransform(@handle)
34
+ end
35
+
36
+ def dup
37
+ ptr = C::Graphics.sfTransformable_copy(@handle)
38
+ raise Error, "sfTransformable_copy returned NULL" if ptr.null?
39
+ copy = self.class.allocate
40
+ copy.instance_variable_set(:@handle,
41
+ FFI::AutoPointer.new(ptr, C::Graphics.method(:sfTransformable_destroy)))
42
+ copy
43
+ end
44
+ alias clone dup
45
+
46
+ attr_reader :handle # :nodoc:
47
+ end
48
+ end
@@ -109,6 +109,18 @@ module SFML
109
109
  target._draw_native(:VertexArray, @handle, states_ptr)
110
110
  end
111
111
 
112
+ # Independent copy — vertices duplicated, future appends to one
113
+ # don't affect the other.
114
+ def dup
115
+ ptr = C::Graphics.sfVertexArray_copy(@handle)
116
+ raise Error, "sfVertexArray_copy returned NULL" if ptr.null?
117
+ copy = self.class.allocate
118
+ copy.instance_variable_set(:@handle,
119
+ FFI::AutoPointer.new(ptr, C::Graphics.method(:sfVertexArray_destroy)))
120
+ copy
121
+ end
122
+ alias clone dup
123
+
112
124
  attr_reader :handle # :nodoc:
113
125
  end
114
126
  end
@@ -108,6 +108,18 @@ module SFML
108
108
 
109
109
  def native_handle = C::Graphics.sfVertexBuffer_getNativeHandle(@handle)
110
110
 
111
+ # Bind this VBO as the active vertex buffer for the GL pipeline.
112
+ # Useful when mixing raw GL with SFML rendering — pair with
113
+ # `SFML::VertexBuffer.unbind` (or any other draw) to release.
114
+ def bind
115
+ C::Graphics.sfVertexBuffer_bind(@handle)
116
+ self
117
+ end
118
+
119
+ def self.unbind
120
+ C::Graphics.sfVertexBuffer_bind(nil)
121
+ end
122
+
111
123
  def draw_on(target, states_ptr = nil) # :nodoc:
112
124
  target._draw_native(:VertexBuffer, @handle, states_ptr)
113
125
  end
@@ -0,0 +1,123 @@
1
+ module SFML
2
+ module Network
3
+ # Binary serializer with explicit, typed read/write — wire-compatible
4
+ # with SFML's `sf::Packet`. Used by Tcp/UdpSocket's `send_packet`
5
+ # /`receive_packet` (the network layer prepends a 4-byte length header
6
+ # for TCP framing, so the receiver always gets a whole packet).
7
+ #
8
+ # All read calls consume bytes in FIFO order. After a write you can
9
+ # always re-read by sending the packet over the wire; the local
10
+ # read position only advances on the receive side. To rewind a
11
+ # locally-built packet, just construct a new one.
12
+ #
13
+ # pkt = SFML::Network::Packet.new
14
+ # pkt.write_int32(42)
15
+ # pkt.write_string("hello")
16
+ # pkt.write_float(0.5)
17
+ #
18
+ # # … send over a TcpSocket, receive on the other end …
19
+ #
20
+ # incoming.read_int32 #=> 42
21
+ # incoming.read_string #=> "hello"
22
+ # incoming.read_float #=> 0.5
23
+ class Packet
24
+ def initialize
25
+ ptr = C::Network.sfPacket_create
26
+ raise Error, "sfPacket_create returned NULL" if ptr.null?
27
+ @handle = FFI::AutoPointer.new(ptr, C::Network.method(:sfPacket_destroy))
28
+ end
29
+
30
+ def clear
31
+ C::Network.sfPacket_clear(@handle)
32
+ self
33
+ end
34
+
35
+ # Append a raw byte buffer (no length prefix, no type tag) — for
36
+ # interop with hand-crafted binary protocols. Most callers want
37
+ # `write_string` / `write_int32` etc. instead.
38
+ def append(bytes)
39
+ s = bytes.to_s
40
+ buf = FFI::MemoryPointer.new(:uint8, s.bytesize)
41
+ buf.write_bytes(s)
42
+ C::Network.sfPacket_append(@handle, buf, s.bytesize)
43
+ self
44
+ end
45
+
46
+ # Returns the full packet as a binary String. Includes any length
47
+ # prefixes / type encoding sfPacket added on write.
48
+ def data
49
+ size = C::Network.sfPacket_getDataSize(@handle)
50
+ return "".b if size.zero?
51
+ ptr = C::Network.sfPacket_getData(@handle)
52
+ ptr.read_bytes(size).force_encoding(Encoding::ASCII_8BIT)
53
+ end
54
+
55
+ def size = C::Network.sfPacket_getDataSize(@handle)
56
+ def read_position = C::Network.sfPacket_getReadPosition(@handle)
57
+ def end_of_packet? = C::Network.sfPacket_endOfPacket(@handle)
58
+
59
+ # `false` if the last read overran the packet — sfPacket "fails"
60
+ # quietly rather than raising, and subsequent reads return zero.
61
+ # Check this after a read sequence to validate the frame.
62
+ def ok? = C::Network.sfPacket_canRead(@handle)
63
+
64
+ # ---- typed writers ----
65
+ def write_bool(v)
66
+ C::Network.sfPacket_writeBool(@handle, v ? true : false); self
67
+ end
68
+ def write_int8(v) = (C::Network.sfPacket_writeInt8(@handle, Integer(v)); self)
69
+ def write_uint8(v) = (C::Network.sfPacket_writeUint8(@handle, Integer(v)); self)
70
+ def write_int16(v) = (C::Network.sfPacket_writeInt16(@handle, Integer(v)); self)
71
+ def write_uint16(v) = (C::Network.sfPacket_writeUint16(@handle, Integer(v)); self)
72
+ def write_int32(v) = (C::Network.sfPacket_writeInt32(@handle, Integer(v)); self)
73
+ def write_uint32(v) = (C::Network.sfPacket_writeUint32(@handle, Integer(v)); self)
74
+ def write_int64(v) = (C::Network.sfPacket_writeInt64(@handle, Integer(v)); self)
75
+ def write_uint64(v) = (C::Network.sfPacket_writeUint64(@handle, Integer(v)); self)
76
+ def write_float(v) = (C::Network.sfPacket_writeFloat(@handle, Float(v)); self)
77
+ def write_double(v) = (C::Network.sfPacket_writeDouble(@handle, Float(v)); self)
78
+
79
+ def write_string(str)
80
+ C::Network.sfPacket_writeString(@handle, str.to_s)
81
+ self
82
+ end
83
+
84
+ # ---- typed readers ----
85
+ def read_bool = C::Network.sfPacket_readBool(@handle)
86
+ def read_int8 = C::Network.sfPacket_readInt8(@handle)
87
+ def read_uint8 = C::Network.sfPacket_readUint8(@handle)
88
+ def read_int16 = C::Network.sfPacket_readInt16(@handle)
89
+ def read_uint16 = C::Network.sfPacket_readUint16(@handle)
90
+ def read_int32 = C::Network.sfPacket_readInt32(@handle)
91
+ def read_uint32 = C::Network.sfPacket_readUint32(@handle)
92
+ def read_int64 = C::Network.sfPacket_readInt64(@handle)
93
+ def read_uint64 = C::Network.sfPacket_readUint64(@handle)
94
+ def read_float = C::Network.sfPacket_readFloat(@handle)
95
+ def read_double = C::Network.sfPacket_readDouble(@handle)
96
+
97
+ # Read a length-prefixed string. CSFML expects a caller-allocated
98
+ # C buffer of "string length + 1 NUL" bytes — we size it from the
99
+ # remaining packet bytes, which is always an upper bound.
100
+ def read_string
101
+ remaining = size - read_position
102
+ return "" if remaining <= 0
103
+
104
+ buf = FFI::MemoryPointer.new(:char, remaining + 1)
105
+ C::Network.sfPacket_readString(@handle, buf)
106
+ buf.read_string.force_encoding(Encoding::UTF_8)
107
+ end
108
+
109
+ def dup
110
+ ptr = C::Network.sfPacket_copy(@handle)
111
+ raise Error, "sfPacket_copy returned NULL" if ptr.null?
112
+
113
+ copy = self.class.allocate
114
+ copy.instance_variable_set(:@handle,
115
+ FFI::AutoPointer.new(ptr, C::Network.method(:sfPacket_destroy)))
116
+ copy
117
+ end
118
+ alias clone dup
119
+
120
+ attr_reader :handle # :nodoc:
121
+ end
122
+ end
123
+ end
@@ -55,6 +55,25 @@ module SFML
55
55
  [status, buf.read_bytes(n)]
56
56
  end
57
57
 
58
+ # Send a structured SFML::Network::Packet. CSFML frames the wire
59
+ # bytes with a length prefix so the peer's receive_packet always
60
+ # gets a whole packet (no need to handle TCP boundary fragments
61
+ # at the Ruby layer).
62
+ def send_packet(packet)
63
+ raise ArgumentError, "expected SFML::Network::Packet" unless packet.is_a?(Packet)
64
+ code = C::Network.sfTcpSocket_sendPacket(@handle, packet.handle)
65
+ C::Network::STATUSES[code]
66
+ end
67
+
68
+ # Receive into a fresh Packet. Returns [status, packet]; the
69
+ # packet is nil for non-:done statuses.
70
+ def receive_packet
71
+ pkt = Packet.new
72
+ code = C::Network.sfTcpSocket_receivePacket(@handle, pkt.handle)
73
+ status = C::Network::STATUSES[code]
74
+ [status, status == :done ? pkt : nil]
75
+ end
76
+
58
77
  def blocking? = C::Network.sfTcpSocket_isBlocking(@handle)
59
78
 
60
79
  def blocking=(value)
@@ -57,6 +57,29 @@ module SFML
57
57
  [status, buf.read_bytes(n), sip, sport]
58
58
  end
59
59
 
60
+ # Send a structured SFML::Network::Packet to (to, port).
61
+ def send_packet(packet, to:, port:)
62
+ raise ArgumentError, "expected SFML::Network::Packet" unless packet.is_a?(Packet)
63
+ addr = to.is_a?(IpAddress) ? to : IpAddress.from_string(to)
64
+ code = C::Network.sfUdpSocket_sendPacket(@handle, packet.handle, addr.struct, Integer(port))
65
+ C::Network::STATUSES[code]
66
+ end
67
+
68
+ # Returns [status, packet, sender_ip, sender_port]. Packet is nil
69
+ # for non-:done statuses.
70
+ def receive_packet
71
+ pkt = Packet.new
72
+ sender_addr = C::Network::IpAddress.new
73
+ sender_port = FFI::MemoryPointer.new(:uint16)
74
+
75
+ code = C::Network.sfUdpSocket_receivePacket(
76
+ @handle, pkt.handle, sender_addr.pointer, sender_port,
77
+ )
78
+ status = C::Network::STATUSES[code]
79
+ return [status, nil, nil, nil] unless status == :done
80
+ [status, pkt, IpAddress.wrap(sender_addr), sender_port.read(:uint16)]
81
+ end
82
+
60
83
  def blocking? = C::Network.sfUdpSocket_isBlocking(@handle)
61
84
 
62
85
  def blocking=(value)
@@ -0,0 +1,88 @@
1
+ module SFML
2
+ # Bridge between a Ruby IO-like object (anything with `#read(n)`,
3
+ # `#seek(pos)`, `#pos` / `#tell`, and `#size`) and CSFML's
4
+ # `sfInputStream*` parameter on `*_createFromStream` loader
5
+ # functions.
6
+ #
7
+ # Typical use is indirect — pass a Ruby IO to the
8
+ # `.from_stream` factory on Font, Image, Texture, Shader, Music,
9
+ # or SoundBuffer:
10
+ #
11
+ # File.open("assets/hero.png", "rb") do |io|
12
+ # tex = SFML::Texture.from_stream(io)
13
+ # end
14
+ #
15
+ # Direct use is for advanced cases (custom virtual filesystems,
16
+ # network streams, etc.). Subclass or build with `.new(io)`:
17
+ #
18
+ # wrapped = SFML::InputStream.new(my_random_access_io)
19
+ # font = SFML::Font.send(:_load_from_stream_handle, wrapped.struct)
20
+ #
21
+ # The InputStream object must outlive the loader call — keep a
22
+ # reference until the CSFML factory returns. The wrapped IO is
23
+ # not closed automatically; close it yourself.
24
+ class InputStream
25
+ # `io` must respond to: read(n), seek(pos, IO::SEEK_SET), pos/tell, size.
26
+ # Ruby's File, StringIO, and Tempfile all qualify.
27
+ def initialize(io)
28
+ @io = io
29
+
30
+ # Strong refs so the GC doesn't free our callbacks before CSFML
31
+ # is done with the struct. CSFML calls them synchronously
32
+ # during the loader function, but may also call them later
33
+ # (Music streams from disk on its audio thread).
34
+ @read_cb = FFI::Function.new(:int64, [:pointer, :size_t, :pointer]) do |buf, size, _user|
35
+ begin
36
+ bytes = @io.read(size) || ""
37
+ buf.write_bytes(bytes) unless bytes.empty?
38
+ bytes.bytesize
39
+ rescue StandardError
40
+ -1
41
+ end
42
+ end
43
+
44
+ @seek_cb = FFI::Function.new(:int64, [:size_t, :pointer]) do |pos, _user|
45
+ begin
46
+ @io.seek(pos, IO::SEEK_SET)
47
+ pos
48
+ rescue StandardError
49
+ -1
50
+ end
51
+ end
52
+
53
+ @tell_cb = FFI::Function.new(:int64, [:pointer]) do |_user|
54
+ begin
55
+ @io.respond_to?(:pos) ? @io.pos : @io.tell
56
+ rescue StandardError
57
+ -1
58
+ end
59
+ end
60
+
61
+ @size_cb = FFI::Function.new(:int64, [:pointer]) do |_user|
62
+ begin
63
+ @io.size
64
+ rescue StandardError
65
+ -1
66
+ end
67
+ end
68
+
69
+ @struct = C::System::InputStream.new
70
+ @struct[:read] = @read_cb
71
+ @struct[:seek] = @seek_cb
72
+ @struct[:tell] = @tell_cb
73
+ @struct[:get_size] = @size_cb
74
+ @struct[:user_data] = FFI::Pointer::NULL
75
+ end
76
+
77
+ # @!visibility private
78
+ def struct
79
+ @struct
80
+ end
81
+
82
+ # Pointer suitable for the `sfInputStream*` parameter on
83
+ # `*_createFromStream` loaders.
84
+ def to_ptr
85
+ @struct.pointer
86
+ end
87
+ end
88
+ end
data/lib/sfml/version.rb CHANGED
@@ -15,5 +15,5 @@ module SFML
15
15
  # "3.0.1.0" — CSFML 3.0.1 ships, we re-cut from upstream
16
16
  # "3.0.1.1" — our patch on top of CSFML 3.0.1
17
17
  # "3.1.0.0" — CSFML 3.1.0 ships, we add new bindings
18
- VERSION = "3.0.0.4"
18
+ VERSION = "3.0.0.5"
19
19
  end