dama 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +227 -0
- data/dama-logo.svg +91 -0
- data/exe/dama +4 -0
- data/ext/dama_native/.cargo/config.toml +3 -0
- data/ext/dama_native/Cargo.lock +3575 -0
- data/ext/dama_native/Cargo.toml +39 -0
- data/ext/dama_native/extconf.rb +72 -0
- data/ext/dama_native/src/audio.rs +134 -0
- data/ext/dama_native/src/engine.rs +339 -0
- data/ext/dama_native/src/lib.rs +396 -0
- data/ext/dama_native/src/renderer/screenshot.rs +84 -0
- data/ext/dama_native/src/renderer/shape_renderer.rs +507 -0
- data/ext/dama_native/src/renderer/text_renderer.rs +192 -0
- data/ext/dama_native/src/renderer.rs +563 -0
- data/ext/dama_native/src/window.rs +255 -0
- data/lib/dama/animation.rb +66 -0
- data/lib/dama/asset_cache.rb +56 -0
- data/lib/dama/audio.rb +47 -0
- data/lib/dama/auto_loader.rb +54 -0
- data/lib/dama/backend/base.rb +137 -0
- data/lib/dama/backend/native/ffi_bindings.rb +122 -0
- data/lib/dama/backend/native.rb +191 -0
- data/lib/dama/backend/web.rb +199 -0
- data/lib/dama/backend.rb +13 -0
- data/lib/dama/camera.rb +68 -0
- data/lib/dama/cli/new_project.rb +112 -0
- data/lib/dama/cli/release.rb +45 -0
- data/lib/dama/cli.rb +22 -0
- data/lib/dama/colors.rb +30 -0
- data/lib/dama/command_buffer.rb +83 -0
- data/lib/dama/component/attribute_definition.rb +13 -0
- data/lib/dama/component/attribute_set.rb +32 -0
- data/lib/dama/component.rb +28 -0
- data/lib/dama/configuration.rb +18 -0
- data/lib/dama/debug/frame_controller.rb +35 -0
- data/lib/dama/debug/screenshot_tool.rb +19 -0
- data/lib/dama/debug.rb +4 -0
- data/lib/dama/event_bus.rb +47 -0
- data/lib/dama/game/builder.rb +31 -0
- data/lib/dama/game/loop.rb +44 -0
- data/lib/dama/game.rb +88 -0
- data/lib/dama/geometry/circle.rb +28 -0
- data/lib/dama/geometry/rect.rb +16 -0
- data/lib/dama/geometry/sprite.rb +18 -0
- data/lib/dama/geometry/triangle.rb +13 -0
- data/lib/dama/geometry.rb +4 -0
- data/lib/dama/input/keyboard_state.rb +44 -0
- data/lib/dama/input/mouse_state.rb +45 -0
- data/lib/dama/input.rb +38 -0
- data/lib/dama/keys.rb +67 -0
- data/lib/dama/node/component_slot.rb +18 -0
- data/lib/dama/node/draw_context.rb +96 -0
- data/lib/dama/node.rb +139 -0
- data/lib/dama/physics/body.rb +57 -0
- data/lib/dama/physics/collider.rb +152 -0
- data/lib/dama/physics/collision.rb +15 -0
- data/lib/dama/physics/world.rb +125 -0
- data/lib/dama/physics.rb +4 -0
- data/lib/dama/registry/class_resolver.rb +48 -0
- data/lib/dama/registry.rb +21 -0
- data/lib/dama/release/archiver.rb +100 -0
- data/lib/dama/release/defaults/icon.icns +0 -0
- data/lib/dama/release/defaults/icon.ico +0 -0
- data/lib/dama/release/defaults/icon.png +0 -0
- data/lib/dama/release/dylib_relinker.rb +95 -0
- data/lib/dama/release/game_file_copier.rb +35 -0
- data/lib/dama/release/game_metadata.rb +61 -0
- data/lib/dama/release/icon_provider.rb +36 -0
- data/lib/dama/release/native_builder.rb +44 -0
- data/lib/dama/release/packager/linux.rb +62 -0
- data/lib/dama/release/packager/macos.rb +99 -0
- data/lib/dama/release/packager/web.rb +32 -0
- data/lib/dama/release/packager/windows.rb +61 -0
- data/lib/dama/release/packager.rb +9 -0
- data/lib/dama/release/platform_detector.rb +23 -0
- data/lib/dama/release/ruby_bundler.rb +163 -0
- data/lib/dama/release/stdlib_trimmer.rb +133 -0
- data/lib/dama/release/template_renderer.rb +40 -0
- data/lib/dama/release/templates/info_plist.xml.erb +19 -0
- data/lib/dama/release/templates/launcher_linux.sh.erb +10 -0
- data/lib/dama/release/templates/launcher_macos.sh.erb +10 -0
- data/lib/dama/release/templates/launcher_windows.bat.erb +11 -0
- data/lib/dama/release.rb +7 -0
- data/lib/dama/scene/composer.rb +65 -0
- data/lib/dama/scene.rb +233 -0
- data/lib/dama/scene_graph/class_index.rb +26 -0
- data/lib/dama/scene_graph/group_node.rb +27 -0
- data/lib/dama/scene_graph/instance_node.rb +30 -0
- data/lib/dama/scene_graph/path_selector.rb +25 -0
- data/lib/dama/scene_graph/query.rb +34 -0
- data/lib/dama/scene_graph/tag_index.rb +26 -0
- data/lib/dama/scene_graph/tree.rb +65 -0
- data/lib/dama/scene_graph.rb +4 -0
- data/lib/dama/sprite_sheet.rb +36 -0
- data/lib/dama/tween/easing.rb +31 -0
- data/lib/dama/tween/lerp.rb +35 -0
- data/lib/dama/tween/manager.rb +28 -0
- data/lib/dama/tween.rb +4 -0
- data/lib/dama/version.rb +3 -0
- data/lib/dama/vertex_batch.rb +35 -0
- data/lib/dama/web/entry.rb +79 -0
- data/lib/dama/web/static/index.html +142 -0
- data/lib/dama/web_builder.rb +232 -0
- data/lib/dama.rb +42 -0
- metadata +198 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Physics
|
|
3
|
+
# Manages physics bodies and steps the simulation each frame.
|
|
4
|
+
# Integrates velocity, detects AABB/circle collisions, resolves overlaps,
|
|
5
|
+
# and emits collision events via EventBus.
|
|
6
|
+
class World
|
|
7
|
+
attr_reader :gravity_x, :gravity_y
|
|
8
|
+
|
|
9
|
+
def initialize(gravity_x: 0.0, gravity_y: 0.0, event_bus: nil)
|
|
10
|
+
@gravity_x = gravity_x.to_f
|
|
11
|
+
@gravity_y = gravity_y.to_f
|
|
12
|
+
@event_bus = event_bus
|
|
13
|
+
@bodies = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add(body)
|
|
17
|
+
bodies << body
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def remove(body)
|
|
21
|
+
bodies.delete(body)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Step the simulation: integrate velocities, detect and resolve collisions.
|
|
25
|
+
def step(delta_time:)
|
|
26
|
+
integrate_bodies(delta_time:)
|
|
27
|
+
detect_and_resolve_collisions
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :bodies, :event_bus
|
|
33
|
+
|
|
34
|
+
def integrate_bodies(delta_time:)
|
|
35
|
+
bodies.each do |body|
|
|
36
|
+
body.integrate(delta_time:, gravity_x:, gravity_y:)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def detect_and_resolve_collisions
|
|
41
|
+
bodies.combination(2).each do |body_a, body_b|
|
|
42
|
+
next if body_a.static? && body_b.static?
|
|
43
|
+
|
|
44
|
+
sep = body_a.collider.separation(
|
|
45
|
+
other: body_b.collider,
|
|
46
|
+
ax: body_a.x, ay: body_a.y,
|
|
47
|
+
bx: body_b.x, by: body_b.y
|
|
48
|
+
)
|
|
49
|
+
next unless sep
|
|
50
|
+
|
|
51
|
+
resolve_collision(body_a, body_b, sep)
|
|
52
|
+
emit_collision(body_a, body_b, sep)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def resolve_collision(body_a, body_b, sep)
|
|
57
|
+
dx = sep.fetch(:dx)
|
|
58
|
+
dy = sep.fetch(:dy)
|
|
59
|
+
|
|
60
|
+
resolve_positions(body_a, body_b, dx, dy)
|
|
61
|
+
resolve_velocities(body_a, body_b, dx, dy)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
POSITION_RESOLVERS = {
|
|
65
|
+
# dynamic vs static: push dynamic body away (opposite of separation)
|
|
66
|
+
%i[dynamic static] => lambda { |a, _b, dx, dy|
|
|
67
|
+
a.x -= dx
|
|
68
|
+
a.y -= dy
|
|
69
|
+
},
|
|
70
|
+
# static vs dynamic: push dynamic body along separation
|
|
71
|
+
%i[static dynamic] => lambda { |_a, b, dx, dy|
|
|
72
|
+
b.x += dx
|
|
73
|
+
b.y += dy
|
|
74
|
+
},
|
|
75
|
+
# dynamic vs dynamic: split the separation equally
|
|
76
|
+
%i[dynamic dynamic] => lambda { |a, b, dx, dy|
|
|
77
|
+
half_dx = dx / 2.0
|
|
78
|
+
half_dy = dy / 2.0
|
|
79
|
+
a.x -= half_dx
|
|
80
|
+
a.y -= half_dy
|
|
81
|
+
b.x += half_dx
|
|
82
|
+
b.y += half_dy
|
|
83
|
+
},
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
86
|
+
def resolve_positions(body_a, body_b, dx, dy)
|
|
87
|
+
key = [body_a.type, body_b.type]
|
|
88
|
+
resolver = POSITION_RESOLVERS.fetch(key, nil)
|
|
89
|
+
resolver&.call(body_a, body_b, dx, dy)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolve_velocities(body_a, body_b, dx, dy)
|
|
93
|
+
# Separation points from a toward b. Normal for a points away (negative).
|
|
94
|
+
normal_x = dx.zero? ? 0.0 : (dx / dx.abs)
|
|
95
|
+
normal_y = dy.zero? ? 0.0 : (dy / dy.abs)
|
|
96
|
+
|
|
97
|
+
# body_a bounces against normal pointing away from b (negative).
|
|
98
|
+
bounce_body(body_a, -normal_x, -normal_y) if body_a.dynamic?
|
|
99
|
+
# body_b bounces against normal pointing away from a (positive).
|
|
100
|
+
bounce_body(body_b, normal_x, normal_y) if body_b.dynamic?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def bounce_body(body, normal_x, normal_y)
|
|
104
|
+
restitution = body.restitution
|
|
105
|
+
# Reflect velocity along the collision normal.
|
|
106
|
+
dot = (body.velocity_x * normal_x) + (body.velocity_y * normal_y)
|
|
107
|
+
return unless dot.negative? # Only bounce if moving toward the surface.
|
|
108
|
+
|
|
109
|
+
body.velocity_x -= (1.0 + restitution) * dot * normal_x
|
|
110
|
+
body.velocity_y -= (1.0 + restitution) * dot * normal_y
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def emit_collision(body_a, body_b, sep)
|
|
114
|
+
return unless event_bus
|
|
115
|
+
|
|
116
|
+
collision = Collision.new(
|
|
117
|
+
body_a:, body_b:,
|
|
118
|
+
separation_x: sep.fetch(:dx),
|
|
119
|
+
separation_y: sep.fetch(:dy)
|
|
120
|
+
)
|
|
121
|
+
event_bus.emit(:collision, collision:)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/dama/physics.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
class Registry
|
|
3
|
+
# Resolves symbol/string names to registered classes.
|
|
4
|
+
# Classes are registered by category (:component, :node, :scene)
|
|
5
|
+
# and looked up via a snake_case key derived from the class name.
|
|
6
|
+
class ClassResolver
|
|
7
|
+
CATEGORIES = %i[component node scene].freeze
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@registrations = CATEGORIES.to_h { |cat| [cat, {}] }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(klass:, category:)
|
|
14
|
+
key = derive_key(klass:)
|
|
15
|
+
registrations.fetch(category)[key] = klass
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def resolve(name:, category:)
|
|
19
|
+
key = normalize_name(name:)
|
|
20
|
+
registrations.fetch(category).fetch(key)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :registrations
|
|
26
|
+
|
|
27
|
+
# Derives a snake_case symbol key from a class name.
|
|
28
|
+
# Only uses the last segment (after ::) to avoid namespace prefixes.
|
|
29
|
+
# Uses a regex with named groups to split CamelCase boundaries.
|
|
30
|
+
CAMEL_BOUNDARY = /(?<upper>[A-Z]+)(?<next_upper>[A-Z][a-z])/
|
|
31
|
+
CAMEL_LOWER = /(?<lower>[a-z\d])(?<upper>[A-Z])/
|
|
32
|
+
|
|
33
|
+
def derive_key(klass:)
|
|
34
|
+
# Extract the class basename (last segment after ::).
|
|
35
|
+
basename = klass.name&.match(/(?<basename>[^:]+)\z/)&.[](:basename) || klass.to_s
|
|
36
|
+
basename
|
|
37
|
+
.gsub(CAMEL_BOUNDARY, '\k<upper>_\k<next_upper>')
|
|
38
|
+
.gsub(CAMEL_LOWER, '\k<lower>_\k<upper>')
|
|
39
|
+
.downcase
|
|
40
|
+
.to_sym
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_name(name:)
|
|
44
|
+
name.to_s.downcase.to_sym
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
# Global class registry for auto-discovery of Component, Node, and Scene
|
|
3
|
+
# subclasses. Supports symbol/string-to-class resolution for the Composer DSL.
|
|
4
|
+
class Registry
|
|
5
|
+
def initialize
|
|
6
|
+
@resolver = Registry::ClassResolver.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def register(klass:, category:)
|
|
10
|
+
resolver.register(klass:, category:)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(name:, category:)
|
|
14
|
+
resolver.resolve(name:, category:)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :resolver
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "pathname"
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "zip"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
# :nocov:
|
|
9
|
+
raise LoadError,
|
|
10
|
+
"rubyzip gem is required for release packaging. Install it with: gem install rubyzip"
|
|
11
|
+
# :nocov:
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Dama
|
|
15
|
+
module Release
|
|
16
|
+
# Creates distributable archives from release directories.
|
|
17
|
+
# Uses pure Ruby for tar.gz and zip formats. The macOS .app
|
|
18
|
+
# variant uses ditto to preserve extended attributes and
|
|
19
|
+
# resource forks -- no Ruby equivalent exists for this.
|
|
20
|
+
class Archiver
|
|
21
|
+
def initialize(source_path:)
|
|
22
|
+
@source_path = source_path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Creates a .zip using ditto, which preserves macOS extended
|
|
26
|
+
# attributes and resource forks required by .app bundles.
|
|
27
|
+
def create_macos_zip
|
|
28
|
+
archive_path = "#{source_path}.zip"
|
|
29
|
+
FileUtils.rm_f(archive_path)
|
|
30
|
+
success = system("ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", source_path, archive_path)
|
|
31
|
+
raise "ditto failed creating #{archive_path}" unless success
|
|
32
|
+
|
|
33
|
+
archive_path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create_tar_gz
|
|
37
|
+
require "rubygems/package"
|
|
38
|
+
|
|
39
|
+
archive_path = "#{source_path}.tar.gz"
|
|
40
|
+
FileUtils.rm_f(archive_path)
|
|
41
|
+
|
|
42
|
+
File.open(archive_path, "wb") do |file|
|
|
43
|
+
Zlib::GzipWriter.wrap(file) do |gzip|
|
|
44
|
+
Gem::Package::TarWriter.new(gzip) do |tar|
|
|
45
|
+
write_directory_to_tar(tar:, dir: source_path, prefix: source_name)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
archive_path
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_zip
|
|
54
|
+
archive_path = "#{source_path}.zip"
|
|
55
|
+
FileUtils.rm_f(archive_path)
|
|
56
|
+
|
|
57
|
+
source_pathname = Pathname.new(source_path)
|
|
58
|
+
Zip::File.open(archive_path, create: true) do |zipfile|
|
|
59
|
+
collect_files.each do |file_path|
|
|
60
|
+
relative = Pathname.new(file_path).relative_path_from(source_pathname.parent).to_s
|
|
61
|
+
zipfile.add(relative, file_path)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
archive_path
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
attr_reader :source_path
|
|
71
|
+
|
|
72
|
+
def source_name
|
|
73
|
+
File.basename(source_path)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def collect_files
|
|
77
|
+
Dir.glob(File.join(source_path, "**", "*"))
|
|
78
|
+
.reject { |f| File.directory?(f) }
|
|
79
|
+
.sort
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write_directory_to_tar(tar:, dir:, prefix:)
|
|
83
|
+
dir_pathname = Pathname.new(dir)
|
|
84
|
+
|
|
85
|
+
Dir.glob(File.join(dir, "**", "*"), File::FNM_DOTMATCH).sort.each do |entry|
|
|
86
|
+
next if File.basename(entry).match?(/\A\.\.?\z/)
|
|
87
|
+
|
|
88
|
+
relative = "#{prefix}/#{Pathname.new(entry).relative_path_from(dir_pathname)}"
|
|
89
|
+
stat = File.stat(entry)
|
|
90
|
+
|
|
91
|
+
next if stat.directory?
|
|
92
|
+
|
|
93
|
+
tar.add_file_simple(relative, stat.mode, stat.size) do |io|
|
|
94
|
+
File.open(entry, "rb") { |f| IO.copy_stream(f, io) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "pathname"
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require "macho"
|
|
6
|
+
rescue LoadError
|
|
7
|
+
# :nocov:
|
|
8
|
+
raise LoadError,
|
|
9
|
+
"ruby-macho gem is required for macOS release packaging. Install it with: gem install ruby-macho"
|
|
10
|
+
# :nocov:
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Dama
|
|
14
|
+
module Release
|
|
15
|
+
# Rewrites dynamic library load paths in macOS Mach-O binaries so they
|
|
16
|
+
# reference bundled copies via @loader_path instead of absolute paths
|
|
17
|
+
# from the build machine. This makes the app bundle portable —
|
|
18
|
+
# it can run on any Mac without the original build environment.
|
|
19
|
+
#
|
|
20
|
+
# Uses the ruby-macho gem (maintained by Homebrew) for pure-Ruby
|
|
21
|
+
# Mach-O manipulation, removing the dependency on Xcode command-line tools.
|
|
22
|
+
class DylibRelinker
|
|
23
|
+
SYSTEM_LIB_PATTERN = %r{\A(/usr/lib/|/System/)}
|
|
24
|
+
|
|
25
|
+
def initialize(binary_path:, lib_destination:)
|
|
26
|
+
@binary_path = binary_path
|
|
27
|
+
@lib_destination = lib_destination
|
|
28
|
+
@processed = Set.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def relink
|
|
32
|
+
FileUtils.mkdir_p(lib_destination)
|
|
33
|
+
relink_binary(path: binary_path)
|
|
34
|
+
# ruby-macho modifies Mach-O data directly, invalidating the
|
|
35
|
+
# original code signature. Re-sign every modified binary with
|
|
36
|
+
# an ad-hoc signature so macOS doesn't kill the process.
|
|
37
|
+
codesign_modified_binaries
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :binary_path, :lib_destination, :processed
|
|
43
|
+
|
|
44
|
+
def relink_binary(path:)
|
|
45
|
+
return if processed.include?(path)
|
|
46
|
+
|
|
47
|
+
processed.add(path)
|
|
48
|
+
|
|
49
|
+
non_system_libraries(path:).each do |original_path|
|
|
50
|
+
lib_name = File.basename(original_path)
|
|
51
|
+
dest_lib = File.join(lib_destination, lib_name)
|
|
52
|
+
|
|
53
|
+
copy_library(source: original_path, destination: dest_lib)
|
|
54
|
+
change_load_path(binary: path, old_path: original_path, lib_name:)
|
|
55
|
+
relink_binary(path: dest_lib)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def non_system_libraries(path:)
|
|
60
|
+
binary_name = File.basename(path)
|
|
61
|
+
|
|
62
|
+
linked_dylibs(path:).reject do |lib_path|
|
|
63
|
+
SYSTEM_LIB_PATTERN.match?(lib_path) || File.basename(lib_path) == binary_name
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def linked_dylibs(path:)
|
|
68
|
+
MachO::Tools.dylibs(path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def copy_library(source:, destination:)
|
|
72
|
+
return if File.exist?(destination)
|
|
73
|
+
|
|
74
|
+
FileUtils.cp(source, destination)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def change_load_path(binary:, old_path:, lib_name:)
|
|
78
|
+
binary_dir = Pathname.new(File.dirname(binary))
|
|
79
|
+
lib_dir = Pathname.new(lib_destination)
|
|
80
|
+
relative = lib_dir.relative_path_from(binary_dir)
|
|
81
|
+
new_path = "@loader_path/#{relative}/#{lib_name}"
|
|
82
|
+
|
|
83
|
+
MachO::Tools.change_install_name(binary, old_path, new_path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def codesign_modified_binaries
|
|
87
|
+
processed.each { |path| codesign(path:) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def codesign(path:)
|
|
91
|
+
system("codesign", "--sign", "-", "--force", path)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Dama
|
|
4
|
+
module Release
|
|
5
|
+
# Copies the game's source files (game/, config.rb, assets/) from
|
|
6
|
+
# the project directory into the release destination. Shared across
|
|
7
|
+
# all native packagers to avoid duplicating the same copy logic.
|
|
8
|
+
class GameFileCopier
|
|
9
|
+
def initialize(project_root:, destination:)
|
|
10
|
+
@project_root = project_root
|
|
11
|
+
@destination = destination
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def copy
|
|
15
|
+
copy_directory("game")
|
|
16
|
+
copy_file("config.rb")
|
|
17
|
+
copy_directory("assets")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :project_root, :destination
|
|
23
|
+
|
|
24
|
+
def copy_directory(name)
|
|
25
|
+
source = File.join(project_root, name)
|
|
26
|
+
FileUtils.cp_r(source, File.join(destination, name)) if File.directory?(source)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def copy_file(name)
|
|
30
|
+
source = File.join(project_root, name)
|
|
31
|
+
FileUtils.cp(source, destination) if File.exist?(source)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Release
|
|
3
|
+
# Extracts game title and resolution from config.rb
|
|
4
|
+
# without loading the engine. Uses regex with named groups
|
|
5
|
+
# to parse the DSL statically.
|
|
6
|
+
class GameMetadata
|
|
7
|
+
TITLE_PATTERN = /title:\s*"(?<title>[^"]+)"/
|
|
8
|
+
RESOLUTION_PATTERN = /resolution:\s*\[(?<width>\d+),\s*(?<height>\d+)\]/
|
|
9
|
+
|
|
10
|
+
# Characters that are unsafe in filenames across macOS, Linux, and Windows.
|
|
11
|
+
UNSAFE_FILENAME_CHARS = %r{[/\\:*?"<>|]}
|
|
12
|
+
|
|
13
|
+
DEFAULT_RESOLUTION = [800, 600].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(project_root:)
|
|
16
|
+
@project_root = project_root
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def title
|
|
20
|
+
extracted_title || directory_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Filesystem-safe version of title for use in release
|
|
24
|
+
# directory names and .app bundle names.
|
|
25
|
+
def release_name
|
|
26
|
+
title.gsub(UNSAFE_FILENAME_CHARS, " ").squeeze(" ").strip
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def resolution
|
|
30
|
+
match = config_content.match(RESOLUTION_PATTERN)
|
|
31
|
+
return DEFAULT_RESOLUTION unless match
|
|
32
|
+
|
|
33
|
+
[Integer(match[:width]), Integer(match[:height])]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :project_root
|
|
39
|
+
|
|
40
|
+
def extracted_title
|
|
41
|
+
match = config_content.match(TITLE_PATTERN)
|
|
42
|
+
match&.[](:title)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def directory_name
|
|
46
|
+
File.basename(project_root)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def config_content
|
|
50
|
+
@config_content ||= read_config
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def read_config
|
|
54
|
+
config_path = File.join(project_root, "config.rb")
|
|
55
|
+
return "" unless File.exist?(config_path)
|
|
56
|
+
|
|
57
|
+
File.read(config_path)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Release
|
|
3
|
+
# Resolves the icon file for a release build.
|
|
4
|
+
# Checks for a user-provided icon in assets/ first,
|
|
5
|
+
# then falls back to the default icon shipped with the gem.
|
|
6
|
+
class IconProvider
|
|
7
|
+
ICON_EXTENSIONS = {
|
|
8
|
+
macos: "icns",
|
|
9
|
+
linux: "png",
|
|
10
|
+
windows: "ico",
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
DEFAULT_ICONS_PATH = ->(ext) { File.expand_path("defaults/icon.#{ext}", __dir__) }
|
|
14
|
+
|
|
15
|
+
def initialize(project_root:, platform:)
|
|
16
|
+
@project_root = project_root
|
|
17
|
+
@platform = platform
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def icon_path
|
|
21
|
+
user_icon_path = File.join(project_root, "assets", "icon.#{extension}")
|
|
22
|
+
return user_icon_path if File.exist?(user_icon_path)
|
|
23
|
+
|
|
24
|
+
DEFAULT_ICONS_PATH.call(extension)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :project_root, :platform
|
|
30
|
+
|
|
31
|
+
def extension
|
|
32
|
+
ICON_EXTENSIONS.fetch(platform)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Dama
|
|
2
|
+
module Release
|
|
3
|
+
# Compiles the Rust native library in release mode.
|
|
4
|
+
# Returns the path to the compiled shared library,
|
|
5
|
+
# using the platform-appropriate extension from FfiBindings.
|
|
6
|
+
class NativeBuilder
|
|
7
|
+
RUST_CRATE_PATH = File.expand_path("../../../ext/dama_native", __dir__)
|
|
8
|
+
|
|
9
|
+
def build
|
|
10
|
+
puts "=== Building native library (release) ==="
|
|
11
|
+
run_command("cargo build --release", dir: RUST_CRATE_PATH)
|
|
12
|
+
library_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def library_path
|
|
16
|
+
platform_key = Backend::Native::FfiBindings::LIBRARY_EXTENSIONS.keys.detect do |k|
|
|
17
|
+
RUBY_PLATFORM.include?(k)
|
|
18
|
+
end
|
|
19
|
+
raise PlatformDetector::UnsupportedPlatformError, "Unsupported platform: #{RUBY_PLATFORM}" unless platform_key
|
|
20
|
+
|
|
21
|
+
extension = Backend::Native::FfiBindings::LIBRARY_EXTENSIONS.fetch(platform_key)
|
|
22
|
+
File.join(RUST_CRATE_PATH, "target", "release", "libdama_native.#{extension}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def run_command(cmd, dir:)
|
|
28
|
+
full_env = ENV.to_h
|
|
29
|
+
full_env["PATH"] = rust_enhanced_path(full_env.fetch("PATH", ""))
|
|
30
|
+
success = system(full_env, cmd, chdir: dir)
|
|
31
|
+
raise "Command failed: #{cmd}" unless success
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def rust_enhanced_path(existing_path)
|
|
35
|
+
home = Dir.home
|
|
36
|
+
rust_dirs = [
|
|
37
|
+
File.join(home, ".cargo", "bin"),
|
|
38
|
+
].select { |d| File.directory?(d) }
|
|
39
|
+
|
|
40
|
+
(rust_dirs + existing_path.split(File::PATH_SEPARATOR)).join(File::PATH_SEPARATOR)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Dama
|
|
4
|
+
module Release
|
|
5
|
+
module Packager
|
|
6
|
+
# Creates a self-contained Linux directory with a launcher script,
|
|
7
|
+
# bundled Ruby, native .so, game files, and assets.
|
|
8
|
+
class Linux
|
|
9
|
+
def initialize(project_root:)
|
|
10
|
+
@project_root = project_root
|
|
11
|
+
@metadata = GameMetadata.new(project_root:)
|
|
12
|
+
@icon_provider = IconProvider.new(project_root:, platform: :linux)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def package
|
|
16
|
+
native_library_path = NativeBuilder.new.build
|
|
17
|
+
prepare_structure
|
|
18
|
+
RubyBundler.new(destination: release_path, project_root:).bundle
|
|
19
|
+
FileUtils.cp(native_library_path, release_path)
|
|
20
|
+
GameFileCopier.new(project_root:, destination: release_path).copy
|
|
21
|
+
copy_icon
|
|
22
|
+
write_launcher_script(native_library_path:)
|
|
23
|
+
|
|
24
|
+
archive_path = Archiver.new(source_path: release_path).create_tar_gz
|
|
25
|
+
puts "Linux release created: #{archive_path}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :project_root, :metadata, :icon_provider
|
|
31
|
+
|
|
32
|
+
def release_path
|
|
33
|
+
File.join(project_root, "release", metadata.release_name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prepare_structure
|
|
37
|
+
FileUtils.rm_rf(release_path)
|
|
38
|
+
FileUtils.mkdir_p(release_path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def copy_icon
|
|
42
|
+
icon_source = icon_provider.icon_path
|
|
43
|
+
FileUtils.cp(icon_source, File.join(release_path, "icon.png")) if File.exist?(icon_source)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def write_launcher_script(native_library_path:)
|
|
47
|
+
launcher_path = File.join(release_path, metadata.release_name.downcase.gsub(/\s+/, "-"))
|
|
48
|
+
content = TemplateRenderer.new(
|
|
49
|
+
template_name: "launcher_linux.sh.erb",
|
|
50
|
+
variables: {
|
|
51
|
+
native_lib_name: File.basename(native_library_path),
|
|
52
|
+
ruby_version: RbConfig::CONFIG.fetch("ruby_version"),
|
|
53
|
+
ruby_arch: RbConfig::CONFIG.fetch("arch"),
|
|
54
|
+
},
|
|
55
|
+
).render
|
|
56
|
+
File.write(launcher_path, content)
|
|
57
|
+
FileUtils.chmod(0o755, launcher_path)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|