1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
# Flecs::Hot (R3): hot-reloadable systems. Register a system ONCE with a stable
# dispatcher block that looks up the current proc by name; on reload just replace
# the proc in the registry — same system id, same matched tables, same entity and
# component data, new logic. No C change to the flecs binding (it bakes the block
# into callback_ctx; we hand it a dispatcher). See .agents/knowledge/hot-reload.md.
#
# world = Flecs::World.new
# world.struct("Position", "{float x; float y;}")
# world.struct("Velocity", "{float x; float y;}")
# Flecs::Hot.world = world
# Flecs::Hot.define_system("Move", with: ["Position", "Velocity"]) do |e, p, v|
# p[:x] += v[:x]; p[:y] += v[:y]
# end
# world.progress(1.0/60)
# # ...edit + reload (over the bridge or from a file): swaps the proc, keeps state
# Flecs::Hot.reload_string('Flecs::Hot.define_system("Move", with: ["Position","Velocity"]){|e,p,v| p[:x]+=v[:x]*5 }')
module Flecs
module Hot
@world = nil
@systems = {} # name(String) => { id:, with:, phase:, proc: }
class << self
attr_accessor :world
# Register (first call) or hot-swap (reload) a system. Idempotent: re-running
# a systems file just replaces procs.
def define_system(name, with:, phase: Flecs::ON_UPDATE, &blk)
raise "Flecs::Hot.world not set" unless @world
raise ArgumentError, "define_system requires a block" unless blk
name = name.to_s
terms = Array(with)
cur = @systems[name]
# Hot path: same shape -> swap the proc only (id/tables/state preserved).
if cur && cur[:with] == terms && cur[:phase] == phase
cur[:proc] = blk
Jamstack::Log.info("system swap", tag: "hot", system: name, id: cur[:id])
return cur[:id]
end
# Shape changed (or first time): (re-)register. Deleting the old system
# entity does NOT touch entity/component data.
@world.entity_for(cur[:id]).delete if cur
rec = { with: terms, phase: phase, proc: blk }
@systems[name] = rec # set before register: the dispatcher reads it live
rec[:id] = @world.system(name, with: terms.map { |t| resolve(t) }, phase: phase) do |eid, *comps|
s = @systems[name]
s[:proc].call(eid, *comps) if s && s[:proc]
end
Jamstack::Log.info(cur ? "system reregister" : "system register",
tag: "hot", system: name, id: rec[:id])
rec[:id]
end
# Re-eval a chunk of systems Ruby (from the bridge). Never raises.
def reload_string(code)
eval(code)
true
rescue Exception => e
Jamstack::Log.exception(e, tag: "hot")
false
end
# Re-eval a systems file (the reloadable unit).
def reload_file(path)
reload_string(File.read(path))
rescue Exception => e
Jamstack::Log.exception(e, tag: "hot", path: path.to_s)
false
end
def id_for(name); (s = @systems[name.to_s]) && s[:id]; end
def systems; @systems.keys; end
def reset!; @systems = {}; end # forget registrations (does not delete systems)
private
# Accept component ids (Component/Integer) or string names (looked up live, so
# reload need not re-create components).
def resolve(t)
return t.to_i unless t.is_a?(String)
c = @world.lookup(t)
raise ArgumentError, "unknown component #{t.inspect}" unless c
c.to_i
end
end
end
end
|