summaryrefslogtreecommitdiffhomepage
path: root/mrbgems/flecs/mrblib/hot.rb
blob: b61596076c06337356a7102a462853e8f1158e4e (plain)
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