Class: Demiurge::Engine

Inherits:
Object
  • Object
show all
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.

Since:

  • 0.0.1

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Util

#copyfreeze, #deepcopy

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.

Parameters:

  • types (Hash) (defaults to: {})

    A name/value hash of item types supported by this engine's serialization.

  • state (Array) (defaults to: [])

    An array of serialized Demiurge items in StateItem structured array format.

Since:

  • 0.0.1



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_contextHash{String=>String} (readonly)

Returns The current execution context for notifications and logging.

Returns:

  • (Hash{String=>String})

    The current execution context for notifications and logging.

Since:

  • 0.0.1



46
47
48
# File 'lib/demiurge.rb', line 46

def execution_context
  @execution_context
end

#ticksInteger (readonly)

Returns The number of ticks that have occurred since the beginning of this Engine's history.

Returns:

  • (Integer)

    The number of ticks that have occurred since the beginning of this Engine's history.

Since:

  • 0.0.1



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.

Since:

  • 0.0.1



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.

Since:

  • 0.0.1



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.

Parameters:

  • message (String)

    A user-readable log message indicating the problem that occurred

  • info (Hash) (defaults to: {})

    A hash of additional fields that indicate the nature of the problem being reported

Options Hash (info):

  • "name" (String)

    The name of the item causing the problem

  • "zone" (String)

    The name of the zone where the problem is located, if known

Since:

  • 0.0.1



208
209
210
211
# File 'lib/demiurge.rb', line 208

def admin_warning(message, info = {})
  send_notification({"message" => message, "info" => info},
                    type: ::Demiurge::Notifications::AdminWarning, zone: "admin", location: nil, actor: nil, include_context: true)
end

#advance_one_tickvoid

This method returns an undefined value.

Perform all necessary operations and phases for one "tick" of virtual time to pass in the Engine.

Since:

  • 0.0.1



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_itemsObject

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.

Since:

  • 0.0.1



338
339
340
# File 'lib/demiurge.rb', line 338

def all_actions_for_all_items
  @item_actions
end

#all_item_namesArray<String>

Get an array of all registered names for all items.

Returns:

Since:

  • 0.0.1



141
142
143
# File 'lib/demiurge.rb', line 141

def all_item_names
  @state_items.keys
end

#finished_initvoid

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.

Since:

  • 0.0.1



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_intentionsvoid

This method returns an undefined value.

Send out all pending Intentions in the Intention queue. This will ordinarily happen at least once per tick in any case. Calling this method outside the Engine's Intention phase of a tick may cause unexpected results.

Since:

  • 0.0.1



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_notificationsvoid

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.

Since:

  • 0.0.1



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_idInteger

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.

Returns:

  • (Integer)

    The new intention ID

Since:

  • 0.2.0



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.

Parameters:

  • t (String)

    The registered type name for this class object

Returns:

  • (Class)

    A StateItem Class object

Since:

  • 0.0.1



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.

Parameters:

  • name (String)

    The new item name to register with the engine

  • parent (Demiurge::StateItem)

    The parent StateItem

  • extra_state (Hash) (defaults to: {})

    Additional state data for the child

Returns:

  • (Demiurge::StateItem)

    The newly-created StateItem which has been registered with the engine

Since:

  • 0.0.1



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.

Parameters:

Returns:

  • (StateItem, nil)

    The StateItem corresponding to this name or nil

Since:

  • 0.0.1



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.

Parameters:

  • arr (Array)

    StateItem structured state in the form of Ruby objects

See Also:

Since:

  • 0.0.1



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_intentionsvoid

This method returns an undefined value.

Calculate the intentions for the next round of the Intention phase of a tick. This is not necessarily the same as all Intentions for the next tick - sometimes an executed Intention will queue more Intentions to run during the same phase of the same tick.

Since:

  • 0.0.1



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.

Parameters:

  • context (Hash{String=>String})

Yields:

  • Evaluate the following block with the given context set

Since:

  • 0.2.0



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.

Parameters:

Since:

  • 0.0.1



169
170
171
172
# File 'lib/demiurge.rb', line 169

def queue_intention(intention)
  @queued_intentions.push intention
  nil
end

#queue_item_intentionsvoid

This method returns an undefined value.

Queue all Intentions for all registered items for the current tick.

Since:

  • 0.0.1



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.

Since:

  • 0.0.1



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

Parameters:

Returns:

Since:

  • 0.0.1



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.

Parameters:

  • name (String)

    The name to use when registering this type

  • klass (Class)

    The StateItem class to register with this name

Since:

  • 0.0.1



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.

Parameters:

  • filenames (Array<String>)

    An array of filenames, suitable for calling File.read on

  • options (Hash) (defaults to: {})

    An optional hash of options for modifying the behavior of the reload

Options Hash (options:):

  • verify_only (Boolean)

    Only see if the new engine code would load, don't replace any code.

  • guessing_okay (Boolean)

    Attempt to guess about renamed objects

  • no_addition (Boolean)

    Don't allow adding new StateItems, raise an error instead

  • no_addition (Boolean)

    Don't allow removing StateItems, raise an error instead

See Also:

Since:

  • 0.0.1



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.

Parameters:

  • specs (Array<String>, Array<Array<String>>)

    An array of specs, see above

  • options (Hash) (defaults to: { "verify_only" => false, "guessing_okay" => false, "no_addition" => false, "no_removal" => false })

    An optional hash of options for modifying the behavior of the reload

Options Hash (options:):

  • verify_only (Boolean)

    Only see if the new engine code would load, don't replace any code.

  • guessing_okay (Boolean)

    Attempt to guess about renamed objects

  • no_addition (Boolean)

    Don't allow adding new StateItems, raise an error instead

  • no_addition (Boolean)

    Don't allow removing StateItems, raise an error instead

See Also:

Since:

  • 0.0.1



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 options["guessing_okay"]
    # For right this second, don't guess. When guessing happens,
    # this will populate the renamed_pairs hash.
  end

  if options["no_addition"] && !added_item_names.empty?
    raise NonMatchingStateError.new "StateItems added when they weren't allowed: #{added_item_names.inspect}!"
  end

  if options["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 options["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.

Since:

  • 0.0.1



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.)

Parameters:

  • type (String)

    The notification type of this notification

  • zone (String, nil)

    The zone name for this notification. The special "admin" zone name is used for engine-wide events

  • location (String, nil)

    The location name for this notification, or nil if no location

  • actor (String, nil)

    The name of the acting item for this notification, or nil if no item is acting

  • data (Hash) (defaults to: {})

    Additional data about this notification; please use String keys for the Hash

Since:

  • 0.0.1



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.

Parameters:

  • options (Hash) (defaults to: { "copy" => false })

    Options for dumping state

Options Hash (options):

  • copy (Boolean)

    If true, copy the serialized state rather than allowing any links into StateItem objects. This reduces performance but increases security.

Returns:

  • (Array)

    The engine's state in StateItem structured array format

See Also:

Since:

  • 0.0.1



112
113
114
115
116
117
118
# File 'lib/demiurge.rb', line 112

def structured_state(options = { "copy" => false })
  dump = @state_items.values.map { |item| item.get_structure }
  if options["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) }

Examples:

Subscribe to all notification types at a particular location


Subscribe to all "say" notifications in my same zone


Subscribe to all move_to notifications for a specific actor, with a tracker for future unsubscription


Subscribe to notifications matching a custom predicate


Parameters:

  • type (:all, String, Array<Symbol>, Array<String>) (defaults to: :all)

    A specifier for what Demiurge notification names to subscribe to

  • zone (:all, String, Array<Symbol>, Array<String>) (defaults to: :all)

    A specifier for what Zone names match this subscription

  • location (:all, String, Array<Symbol>, Array<String>) (defaults to: :all)

    A specifier for what location names match this subscription

  • predicate (Proc, nil) (defaults to: nil)

    Call this proc on each notification to see if it matches this subscription

  • actor (:all, String, Array<Symbol>, Array<String>) (defaults to: :all)

    A specifier for what Demiurge item name(s) must be the actor in a notification to match this subscription

  • tracker (Object, nil) (defaults to: nil)

    To unsubscribe from this notification later, pass in the same tracker to #unsubscribe_from_notifications, or another object that is +==+ to this tracker. A tracker is most often a String or Symbol. If the tracker is nil, you can't ever unsubscribe.

Since:

  • 0.0.1



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.

Parameters:

Since:

  • 0.0.1



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.

Parameters:

  • tracker (Object)

    The tracker from which to unsubscribe

Since:

  • 0.0.1



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.

Parameters:

  • name

    String The item name to check

Returns:

  • (Boolean)

    Whether the item name is valid, just in terms of its characters.

Since:

  • 0.0.1



417
418
419
# File 'lib/demiurge.rb', line 417

def valid_item_name?(name)
  !!(name =~ /\A[-_ 0-9a-zA-Z]+\Z/)
end

#zonesArray<Demiurge::StateItem>

Get an Array of StateItems that are top-level Zone items.

Returns:

Since:

  • 0.0.1



133
134
135
# File 'lib/demiurge.rb', line 133

def zones
  @zones
end