Class: Demiurge::Engine
- Inherits:
-
Object
- Object
- Demiurge::Engine
- Includes:
- Util
- Defined in:
- lib/demiurge.rb,
lib/demiurge/dsl.rb
Overview
The Engine class encapsulates one "world" of simulation. This includes state and code for all items, subscriptions to various events and the ability to reload state or code at a later point. It is entirely possible to have multiple Engine objects containing different objects and subscriptions which are unrelated to each other. If Engines share references in common, that sharing is ordinarily a bug. Currently only registered DSL Object Types should be shared.
Instance Attribute Summary collapse
-
#execution_context ⇒ Hash{String=>String}
readonly
The current execution context for notifications and logging.
-
#ticks ⇒ Integer
readonly
The number of ticks that have occurred since the beginning of this Engine's history.
Instance Method Summary collapse
-
#action_for_item(item_name, action_name) ⇒ Object
private
Fetch an action for an ActionItem that's stored in the engine.
-
#actions_for_item(item_name) ⇒ Object
private
Fetch actions for an ActionItem that's stored in the engine.
-
#admin_warning(message, info = {}) ⇒ void
Send a warning that something unfortunate but not continuity-threatening has occurred.
-
#advance_one_tick ⇒ void
Perform all necessary operations and phases for one "tick" of virtual time to pass in the Engine.
-
#all_actions_for_all_items ⇒ Object
private
Fetch all actions for all items in an internal format.
-
#all_item_names ⇒ Array<String>
Get an array of all registered names for all items.
-
#finished_init ⇒ void
The "finished_init" callback on Demiurge items exists to allow items to finalize their structure relative to other items.
- #flush_intentions ⇒ void
-
#flush_notifications ⇒ void
Send out any pending notifications that have been queued.
-
#get_intention_id ⇒ Integer
private
Get an intention ID which is guaranteed to never be returned by this method again.
-
#get_type(t) ⇒ Class
Get a StateItem type that is registered with this Engine, using the registered name for that type.
-
#initialize(types: {}, state: []) ⇒ void
constructor
This is the constructor for a new Engine object.
-
#instantiate_new_item(name, parent, extra_state = {}) ⇒ Demiurge::StateItem
This method creates a new StateItem based on an existing parent StateItem, and will retain some level of linkage to that parent StateItem afterward as well.
-
#item_by_name(name) ⇒ StateItem?
Get a StateItem by its registered unique name.
-
#load_state_from_dump(arr) ⇒ void
This loads the Engine's state from structured StateItem state that has been serialized.
-
#next_step_intentions ⇒ void
Calculate the intentions for the next round of the Intention phase of a tick.
-
#push_context(context) { ... } ⇒ void
private
Certain context can be important to notifications, errors, logging and other admin-available information.
-
#queue_intention(intention) ⇒ void
Add an intention to the Engine's Intention queue.
-
#queue_item_intentions ⇒ void
Queue all Intentions for all registered items for the current tick.
-
#register_actions_by_item_and_action_name(item_actions) ⇒ Object
StateItems are transient and can be created, recreated or destroyed without warning.
-
#register_state_item(item) ⇒ Demiurge::StateItem
Register a new StateItem.
-
#register_type(name, klass) ⇒ void
Register a new StateItem type with a name that will be used in structured StateItem dumps.
-
#reload_from_dsl_files(*filenames, options: {}) ⇒ void
This method loads new World File code into an existing engine.
-
#reload_from_dsl_text(*specs, options: { "verify_only" => false, "guessing_okay" => false, "no_addition" => false, "no_removal" => false }) ⇒ void
This method loads new World File code into an existing engine.
-
#replace_all_actions_for_all_items(item_action_hash) ⇒ void
private
Replace actions for an item with other, potentially quite different, actions.
-
#send_notification(data = {}, type:, zone:, location:, actor:, include_context: false) ⇒ void
Queue a notification to be sent later by the engine.
-
#structured_state(options = { "copy" => false }) ⇒ Array
This method dumps the Engine's state in StateItem structured array format.
-
#subscribe_to_notifications(type: :all, zone: :all, location: :all, predicate: nil, actor: :all, tracker: nil, &block) ⇒ void
This method 'subscribes' a block to various types of notifications.
-
#unregister_state_item(item) ⇒ void
This method unregisters a StateItem from the engine.
-
#unsubscribe_from_notifications(tracker) ⇒ void
When you subscribe to a notification with #subscribe_to_notifications, you may optionally pass a non-nil tracker with the subscription.
-
#valid_item_name?(name) ⇒ Boolean
Determine whether the item name is basically allowable.
-
#zones ⇒ Array<Demiurge::StateItem>
Get an Array of StateItems that are top-level Zone items.
Methods included from Util
Constructor Details
#initialize(types: {}, state: []) ⇒ void
This is the constructor for a new Engine object. Most frequently this will be called by DSL or another external source which will also supply item types and initial state.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/demiurge.rb', line 56 def initialize(types: {}, state: []) @klasses = {} if types types.each do |tname, tval| register_type(tname, tval) end end @finished_init = false @state_items = {} @zones = [] state_from_structured_array(state || []) @item_actions = {} @subscriptions_by_tracker = {} @queued_notifications = [] @queued_intentions = [] @execution_context = [] nil end |
Instance Attribute Details
#execution_context ⇒ Hash{String=>String} (readonly)
Returns The current execution context for notifications and logging.
46 47 48 |
# File 'lib/demiurge.rb', line 46 def execution_context @execution_context end |
#ticks ⇒ Integer (readonly)
Returns The number of ticks that have occurred since the beginning of this Engine's history.
43 44 45 |
# File 'lib/demiurge.rb', line 43 def ticks @ticks end |
Instance Method Details
#action_for_item(item_name, action_name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Fetch an action for an ActionItem that's stored in the engine.
322 323 324 |
# File 'lib/demiurge.rb', line 322 def action_for_item(item_name, action_name) @item_actions[item_name] ? @item_actions[item_name][action_name] : nil end |
#actions_for_item(item_name) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Fetch actions for an ActionItem that's stored in the engine.
330 331 332 |
# File 'lib/demiurge.rb', line 330 def actions_for_item(item_name) @item_actions[item_name] end |
#admin_warning(message, info = {}) ⇒ void
This method returns an undefined value.
Send a warning that something unfortunate but not continuity-threatening has occurred. The problem isn't bad enough to warrant raising an exception, but it's bad enough that we should collect data about the problem. The warning normally indicates a problem in user-supplied code, current state, or the Demiurge gem itself. These warnings can be subscribed to with the notification type Notifications::AdminWarning.
208 209 210 211 |
# File 'lib/demiurge.rb', line 208 def admin_warning(, info = {}) send_notification({"message" => , "info" => info}, type: ::Demiurge::Notifications::AdminWarning, zone: "admin", location: nil, actor: nil, include_context: true) end |
#advance_one_tick ⇒ void
This method returns an undefined value.
Perform all necessary operations and phases for one "tick" of virtual time to pass in the Engine.
255 256 257 258 259 260 261 |
# File 'lib/demiurge.rb', line 255 def advance_one_tick() queue_item_intentions flush_intentions send_notification({}, type: Demiurge::Notifications::TickFinished, location: "", zone: "", actor: nil) flush_notifications nil end |
#all_actions_for_all_items ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Fetch all actions for all items in an internal format.
338 339 340 |
# File 'lib/demiurge.rb', line 338 def all_actions_for_all_items @item_actions end |
#all_item_names ⇒ Array<String>
Get an array of all registered names for all items.
141 142 143 |
# File 'lib/demiurge.rb', line 141 def all_item_names @state_items.keys end |
#finished_init ⇒ void
This method returns an undefined value.
The "finished_init" callback on Demiurge items exists to allow items to finalize their structure relative to other items. For instance, containers can ensure that their list of contents is identical to the set of items that list the container as their location. This cannot be done until the container is certain that all items have been added to the engine. The #finished_init engine method calls StateItem#finished_init on any items that respond to that callback. Normally #finished_init should be called when a new Engine is created, but not when restoring one from a state dump. This method should not be called multiple times, and the Engine will try not to allow multiple calls.
96 97 98 99 100 |
# File 'lib/demiurge.rb', line 96 def finished_init raise("Duplicate finished_init call to engine!") if @finished_init @state_items.values.each { |obj| obj.finished_init() if obj.respond_to?(:finished_init) } @finished_init = true end |
#flush_intentions ⇒ void
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/demiurge.rb', line 220 def flush_intentions state_backup = structured_state("copy" => true) infinite_loop_detector = 0 until @queued_intentions.empty? infinite_loop_detector += 1 if infinite_loop_detector > 20 raise ::Demiurge::Errors::TooManyIntentionLoopsError.new("Over 20 batches of intentions were dispatched in the same call! Error and die!", "final_batch" => @queued_intentions.map { |i| i.class.to_s }) end intentions = @queued_intentions @queued_intentions = [] begin intentions.each do |a| if a.cancelled? admin_warning("Trying to apply a cancelled intention of type #{a.class}!", "inspect" => a.inspect) else a.try_apply end end rescue ::Demiurge::Errors::RetryableError admin_warning("Exception when updating! Throwing away speculative state!", "exception" => $_.jsonable) load_state_from_dump(state_backup) end end @state_items["admin"].state["ticks"] += 1 nil end |
#flush_notifications ⇒ void
This method returns an undefined value.
Send out any pending notifications that have been queued. This will normally happen at least once per tick in any case, but may happen more often. If this occurs during the Engine's tick, certain ordering issues may occur. Normally it's best to let the Engine call this method during the tick, and to only call it manually when no tick is occurring.
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 |
# File 'lib/demiurge.rb', line 653 def flush_notifications infinite_loop_detector = 0 # Dispatch the queued notifications. Then, dispatch any # notifications that resulted from them. Then, keep doing that # until the queue is empty. until @queued_notifications.empty? infinite_loop_detector += 1 if infinite_loop_detector > 20 raise ::Demiurge::Errors::TooManyNotificationLoopsError.new("Over 20 batches of notifications were dispatched in the same call! Error and die!", "last batch" => @queued_notifications.map { |n| n.class.to_s }) end current_notifications = @queued_notifications @queued_notifications = [] current_notifications.each do |cleaned_data| @subscriptions_by_tracker.each do |tracker, sub_structures| sub_structures.each do |sub_structure| next unless sub_structure[:type] == :all || sub_structure[:type].include?(cleaned_data["type"]) next unless sub_structure[:zone] == :all || sub_structure[:zone].include?(cleaned_data["zone"]) next unless sub_structure[:location] == :all || sub_structure[:location].include?(cleaned_data["location"]) next unless sub_structure[:actor] == :all || sub_structure[:actor].include?(cleaned_data["actor"]) next unless sub_structure[:predicate] == nil || sub_structure[:predicate] == :all || sub_structure[:predicate].call(cleaned_data) sub_structure[:block].call(cleaned_data) end end end end end |
#get_intention_id ⇒ Integer
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Get an intention ID which is guaranteed to never be returned by this method again. It's not important that it be consecutive or otherwise special, just that it be unique.
152 153 154 155 |
# File 'lib/demiurge.rb', line 152 def get_intention_id @state_items["admin"].state["intention_id"] += 1 return @state_items["admin"].state["intention_id"] end |
#get_type(t) ⇒ Class
Get a StateItem type that is registered with this Engine, using the registered name for that type.
268 269 270 271 |
# File 'lib/demiurge.rb', line 268 def get_type(t) raise("Not a valid type: #{t.inspect}!") unless @klasses[t] @klasses[t] end |
#instantiate_new_item(name, parent, extra_state = {}) ⇒ Demiurge::StateItem
This method creates a new StateItem based on an existing parent StateItem, and will retain some level of linkage to that parent StateItem afterward as well. This provides a simple form of action inheritance and data inheritance.
The new child item will begin with a copy of the parent's state which can then be overridden. There will not be a longer-term link to the parent's state, and any later state changes in the parent or child will not affect each other.
There will be a longer-term link to the parent's actions, and any action not overridden in the child item will fall back to the parent's action of the same name.
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/demiurge.rb', line 391 def instantiate_new_item(name, parent, extra_state = {}) parent = item_by_name(parent) unless parent.is_a?(StateItem) ss = parent.get_structure # The new instantiated item is different from the parent because # it has its own name, and because it can get named actions from # the parent as well as itself. The latter is important because # we can't easily make new action procs without an associated # World File of some kind. ss[1] = name ss[2] = deepcopy(ss[2]) ss[2].merge!(extra_state) ss[2]["parent"] = parent.name child = register_state_item(StateItem.from_name_type(self, *ss)) if @finished_init && child.respond_to?(:finished_init) child.finished_init end child end |
#item_by_name(name) ⇒ StateItem?
Get a StateItem by its registered unique name.
125 126 127 |
# File 'lib/demiurge.rb', line 125 def item_by_name(name) @state_items[name] end |
#load_state_from_dump(arr) ⇒ void
This method returns an undefined value.
This loads the Engine's state from structured StateItem state that has been serialized. This method handles reinitializing, signaling and whatnot. Use this method to restore state from a JSON dump or a hypothetical scenario that didn't work out.
Note that this does not update code from Ruby files or otherwise handle any changes in the World Files. For that, use #reload_from_dsl_files or #reload_from_dsl_text.
You can only reload state or World Files for a running engine, never both. If you want to reload both, make a new engine and use it, or reload the two in an order of your choice. Keep in mind that some World File changes can rename state items.
504 505 506 507 508 509 510 |
# File 'lib/demiurge.rb', line 504 def load_state_from_dump(arr) send_notification(type: Demiurge::Notifications::LoadStateStart, actor: nil, location: nil, zone: "admin", include_context: true) state_from_structured_array(arr) finished_init send_notification(type: Demiurge::Notifications::LoadStateEnd, actor: nil, location: nil, zone: "admin", include_context: true) flush_notifications end |
#next_step_intentions ⇒ void
190 191 192 |
# File 'lib/demiurge.rb', line 190 def next_step_intentions() @zones.flat_map { |item| item.intentions_for_next_step || [] } end |
#push_context(context) { ... } ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Certain context can be important to notifications, errors, logging and other admin-available information. For instance: the current zone-in-tick (if any), a current item taking an action and so on. This context can be attached to notifications, admin warnings, system logging and similar.
365 366 367 368 369 370 |
# File 'lib/demiurge.rb', line 365 def push_context(context) @execution_context.push(context) yield ensure @execution_context.pop end |
#queue_intention(intention) ⇒ void
This method returns an undefined value.
Add an intention to the Engine's Intention queue. If this method is called during the Intention phase of a tick, the intention should be excecuted during this tick in standard order. If the method is called outside the Intention phase of a tick, the Intention will normally be executed during the soonest upcoming Intention phase. Queued intentions are subject to approval, cancellation and other normal operations that Intentions undergo before being executed.
169 170 171 172 |
# File 'lib/demiurge.rb', line 169 def queue_intention(intention) @queued_intentions.push intention nil end |
#queue_item_intentions ⇒ void
This method returns an undefined value.
Queue all Intentions for all registered items for the current tick.
178 179 180 |
# File 'lib/demiurge.rb', line 178 def queue_item_intentions() next_step_intentions.each { |i| queue_intention(i) } end |
#register_actions_by_item_and_action_name(item_actions) ⇒ Object
StateItems are transient and can be created, recreated or destroyed without warning. They need to be hooked up to the various Ruby code for their actions. The code for actions isn't serialized. Instead, each action is referred to by name, and the engine loads up all the item-name/action-name combinations when it reads the Ruby World Files. This means an action can be referred to by its name when serialized, but the actual code changes any time the world files are reloaded.
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/demiurge.rb', line 296 def register_actions_by_item_and_action_name(item_actions) item_actions.each do |item_name, act_hash| if @item_actions[item_name] act_hash.each do |action_name, opts| existing = @item_actions[item_name][action_name] if existing ActionItem::ACTION_LEGAL_KEYS.each do |key| if existing[key] && opts[key] && existing[key] != opts[key] raise "Can't register action #{action_name.inspect} for item #{item_name.inspect}, conflict for key #{key.inspect}!" end end existing.merge!(opts) else @item_actions[item_name][action_name] = opts end end else @item_actions[item_name] = act_hash end end end |
#register_state_item(item) ⇒ Demiurge::StateItem
Register a new StateItem
426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/demiurge.rb', line 426 def register_state_item(item) name = item.name if @state_items[name] raise "Duplicate item name: #{name}! Failing!" end @state_items[name] = item if item.zone? @zones.push(item) end if @finished_init send_notification(type: ::Demiurge::Notifications::NewItem, zone: item.zone_name, location: item.location_name, actor: name) end item end |
#register_type(name, klass) ⇒ void
This method returns an undefined value.
Register a new StateItem type with a name that will be used in structured StateItem dumps.
279 280 281 282 283 284 285 |
# File 'lib/demiurge.rb', line 279 def register_type(name, klass) if @klasses[name] && @klasses[name] != klass raise "Re-registering name with different type! Name: #{name.inspect} Class: #{klass.inspect} OldClass: #{@klasses[name].inspect}!" end @klasses[name] ||= klass nil end |
#reload_from_dsl_files(*filenames, options: {}) ⇒ void
This method returns an undefined value.
This method loads new World File code into an existing engine. It should be passed a list of filenames, normally roughly the same list that was passed to Demiurge::DSL.engine_from_dsl_files to create the engine initially.
See #reload_from_dsl_text for more details and allowed options. It is identical in operation except for taking code as parameters instead of filenames.
25 26 27 28 |
# File 'lib/demiurge/dsl.rb', line 25 def reload_from_dsl_files(*filenames, options: {}) filename_string_pairs = filenames.map { |fn| [fn, File.read(fn)] } engine_from_dsl_text(*filename_string_pairs) end |
#reload_from_dsl_text(*specs, options: { "verify_only" => false, "guessing_okay" => false, "no_addition" => false, "no_removal" => false }) ⇒ void
This method returns an undefined value.
This method loads new World File code into an existing engine. It should be passed a list of "specs", normally roughly the same list that was passed to Demiurge::DSL.engine_from_dsl_files to create the engine initially. A "spec" is either a single string of World File DSL code, or a two-element array of the form: ["label", "code"]. Each is a string. "Code" is World File DSL Ruby code, while "label" is the name that will be used in stack traces.
Options:
- verify_only - only check for errors, don't load the new code into the engine, aka "dry run mode"
- guessing_okay - attempt to notice renames and modify old state to new state accordingly
- no_addition - if any new StateItem would be created, raise a NonMatchingStateError
- no_removal - if any StateItem would be removed, raise a NonMatchingStateError
Note that if "guessing_okay" is turned on, certain cases where same-type or (in the future) similar-type items are added and removed may be "guessed" as renames rather than treated as addition or removal.
Where the files and world objects are essentially the same, this should reload any code changes into the engine. Where they are different, the method will try to determine the best match between the new and old state, but may fail at doing so.
Ordinarily, this method should be called on a fully-configured, fully-initialized engine with its full state loaded, in between ticks.
When in doubt, it's better to save state and reload the engine from nothing. This gives far better opportunities for a human to determine what changes have occurred and manually fix any errors. A game's current state is a complex thing and a computer simply cannot correctly determine all possible changes to it in a useful way.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/demiurge/dsl.rb', line 79 def reload_from_dsl_text(*specs, options: { "verify_only" => false, "guessing_okay" => false, "no_addition" => false, "no_removal" => false }) old_engine = self new_engine = nil send_notification type: Demiurge::Notifications::LoadWorldVerify, zone: "admin", location: nil, actor: nil, include_context: true begin new_engine = Demiurge::DSL.engine_from_dsl_text(*specs) rescue # Didn't work? Leave the existing engine intact, but raise. raise Demiurge::Errors::CannotLoadWorldFiles.new("Error reloading World File text") end # Match up old and new state items, but the "admin" InertStateItem doesn't get messed with old_item_names = old_engine.all_item_names - ["admin"] new_item_names = new_engine.all_item_names - ["admin"] shared_item_names = old_item_names & new_item_names added_item_names = new_item_names - shared_item_names removed_item_names = old_item_names - shared_item_names renamed_pairs = {} if ["guessing_okay"] # For right this second, don't guess. When guessing happens, # this will populate the renamed_pairs hash. end if ["no_addition"] && !added_item_names.empty? raise NonMatchingStateError.new "StateItems added when they weren't allowed: #{added_item_names.inspect}!" end if ["no_removal"] && !removed_item_names.empty? raise NonMatchingStateError.new "StateItems removed when they weren't allowed: #{removed_item_names.inspect}!" end # Okay, finished with error checking - the dry-run is over return if ["verify_only"] # Now, replace the engine code send_notification type: Demiurge::Notifications::LoadWorldStart, zone: "admin", location: nil, actor: nil, include_context: true # Replace all actions performed by ActionItems old_engine.replace_all_actions_for_all_items(new_engine.all_actions_for_all_items) # For removed items, delete the StateItem from the old engine removed_item_names.each do |removed_item_name| old_engine.unregister_state_item(old_engine.item_by_name removed_item_name) end # For added items, use the state data from the new engine and add the item to the old engine added_item_names.each do |added_item_name| new_item = new_engine.item_by_name(added_item_name) ss = new_item.structured_state old_engine.register_state_item(StateItem.from_name_type(old_engine, *ss)) end # A rename is basically an add and a remove... But not necessarily # with the same name. And the newly-created item uses the state # from the older item. A rename is also permitted to choose a new # type, so create the new item in the old engine with the new name # and type, but the old state. renamed_pairs.each do |old_name, new_name| old_item = old_engine.item_by_name(old_name) new_item = new_engine.item_by_name(new_name) old_type, _, old_state = *old_item.structured_state new_type, _, new_state = *new_item.structured_state old_engine.unregister_state_item(old_item) old_engine.register_state_item(StateItem.from_name_type(old_engine, new_type, new_name, old_state)) end send_notification type: Demiurge::Notifications::LoadWorldEnd, zone: "admin", location: nil, actor: nil, include_context: true nil end |
#replace_all_actions_for_all_items(item_action_hash) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Replace actions for an item with other, potentially quite different, actions. This is normally done to reload engine state from World Files.
349 350 351 352 |
# File 'lib/demiurge.rb', line 349 def replace_all_actions_for_all_items(item_action_hash) @item_actions = item_action_hash nil end |
#send_notification(data = {}, type:, zone:, location:, actor:, include_context: false) ⇒ void
This method returns an undefined value.
Queue a notification to be sent later by the engine. The notification must include at least type, zone, location and actor and may include a hash of additional data, which should be serializable to JSON (i.e. use only basic data types.)
624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 |
# File 'lib/demiurge.rb', line 624 def send_notification(data = {}, type:, zone:, location:, actor:, include_context:false) raise "Notification type must be a String, not #{type.class}!" unless type.is_a?(String) raise "Location must be a String, not #{location.class}!" unless location.is_a?(String) || location.nil? raise "Zone must be a String, not #{zone.class}!" unless zone.is_a?(String) raise "Acting item must be a String or nil, not #{actor.class}!" unless actor.is_a?(String) || actor.nil? @state_items["admin"].state["notification_id"] += 1 cleaned_data = {} cleaned_data["context"] = @execution_context if include_context data.each do |key, val| # TODO: verify somehow that this is JSON-serializable? cleaned_data[key.to_s] = val end cleaned_data.merge!("type" => type, "zone" => zone, "location" => location, "actor" => actor, "notification_id" => @state_items["admin"].state["notification_id"]) @queued_notifications.push(cleaned_data) end |
#structured_state(options = { "copy" => false }) ⇒ Array
This method dumps the Engine's state in StateItem structured array format. This method is how one would normally collect a full state dump of the engine suitable for later restoration.
112 113 114 115 116 117 118 |
# File 'lib/demiurge.rb', line 112 def structured_state( = { "copy" => false }) dump = @state_items.values.map { |item| item.get_structure } if ["copy"] dump = deepcopy(dump) # Make sure it doesn't share state... end dump end |
#subscribe_to_notifications(type: :all, zone: :all, location: :all, predicate: nil, actor: :all, tracker: nil, &block) ⇒ void
This method returns an undefined value.
This method 'subscribes' a block to various types of notifications. The block will be called with the notifications when they occur. A "specifier" for this method means either the special symbol +:all+ or an Array of Symbols or Strings to show what values a notification may have for the given field. For fields that might indicate Demiurge items such as "zone" or "actor" the value should be the Demiurge item name, not the item itself.
When a notification occurs that matches the subscription, the given block will be called with a hash of data about that notification.
The tracker is supplied to allow later unsubscribes. Pass a unique tracker object (usually a String or Symbol) when subscribing, then pass in the same one when unsubscribing.
At a minimum, subscriptions should include a zone to avoid subscribing to all such notifications everywhere in the engine. Engine-wide subscriptions become very inefficient very quickly.
Notifications only have a few mandatory fields - type, actor, zone and location. The location and/or actor can be nil in some cases, and the zone can be "admin" for engine-wide events. If you wish to subscribe based on other properties of the notification then you'll need to pass a custom predicate to check each notification. A "predicate" just means it's a proc that returns true or false, depending whether a notification matches.
subscribe_to_notifications(zone: "my zone name", location: "my location") { |h| puts "Got #Demiurge::Engine.hh.inspect!" }
subscribe_to_notifications(zone: "my zone", type: "say") { |h| puts "Somebody said something!" }
subscribe_to_notifications(zone: ["one", "two", "three"], type: "move_to", actor: "bozo the clown", tracker: "bozo move tracker") { |h| bozo_move(h) }
subscribe_to_notifications(zone: "green field", type: "joyous dance", predicate: proc { |h| h["info"]["subtype"] == "butterfly wiggle" }) { |h| process(h) }
580 581 582 583 584 585 586 587 588 589 590 591 592 593 |
# File 'lib/demiurge.rb', line 580 def subscribe_to_notifications(type: :all, zone: :all, location: :all, predicate: nil, actor: :all, tracker: nil, &block) sub_structure = { type: notification_spec(type), zone: notification_spec(zone), location: notification_spec(location), actor: notification_spec(actor), predicate: predicate, tracker: tracker, block: block, } @subscriptions_by_tracker[tracker] ||= [] @subscriptions_by_tracker[tracker].push(sub_structure) nil end |
#unregister_state_item(item) ⇒ void
This method returns an undefined value.
This method unregisters a StateItem from the engine. The method assumes other items don't refer to the item being unregistered. #unregister_state_item will try to perform basic cleanup, but calling it can leave dangling references.
449 450 451 452 453 454 455 456 457 |
# File 'lib/demiurge.rb', line 449 def unregister_state_item(item) loc = item.location loc.ensure_does_not_contain(item.name) zone = item.zone zone.ensure_does_not_contain(item.name) @state_items.delete(item.name) @zones -= [item] nil end |
#unsubscribe_from_notifications(tracker) ⇒ void
This method returns an undefined value.
When you subscribe to a notification with #subscribe_to_notifications, you may optionally pass a non-nil tracker with the subscription. If you pass that tracker to this method, it will unsubscribe you from that notification. Multiple subscriptions can use the same tracker and they will all be unsubscribed at once. For that reason, you should use a unique tracker if you do not want other code to be able to unsubscribe you from notifications.
607 608 609 610 |
# File 'lib/demiurge.rb', line 607 def unsubscribe_from_notifications(tracker) raise "Tracker must be non-nil!" if tracker.nil? @subscriptions_by_tracker.delete(tracker) end |
#valid_item_name?(name) ⇒ Boolean
Determine whether the item name is basically allowable.
417 418 419 |
# File 'lib/demiurge.rb', line 417 def valid_item_name?(name) !!(name =~ /\A[-_ 0-9a-zA-Z]+\Z/) end |
#zones ⇒ Array<Demiurge::StateItem>
Get an Array of StateItems that are top-level Zone items.
133 134 135 |
# File 'lib/demiurge.rb', line 133 def zones @zones end |