demiurge 0.2.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/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/.yardopts +5 -0
- data/AUTHORS.txt +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONCEPTS.md +271 -0
- data/Gemfile +4 -0
- data/HACKING.md +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/RELOADING.md +94 -0
- data/Rakefile +10 -0
- data/SECURITY.md +103 -0
- data/WORLD_FILES.md +134 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/demiurge.gemspec +31 -0
- data/exe/demirun +16 -0
- data/lib/demiurge/action_item.rb +643 -0
- data/lib/demiurge/agent.rb +338 -0
- data/lib/demiurge/container.rb +194 -0
- data/lib/demiurge/dsl.rb +583 -0
- data/lib/demiurge/exception.rb +170 -0
- data/lib/demiurge/inert_state_item.rb +21 -0
- data/lib/demiurge/intention.rb +164 -0
- data/lib/demiurge/location.rb +85 -0
- data/lib/demiurge/notification_names.rb +93 -0
- data/lib/demiurge/tmx.rb +439 -0
- data/lib/demiurge/util.rb +67 -0
- data/lib/demiurge/version.rb +4 -0
- data/lib/demiurge/zone.rb +108 -0
- data/lib/demiurge.rb +812 -0
- metadata +165 -0
data/lib/demiurge/dsl.rb
ADDED
@@ -0,0 +1,583 @@
|
|
1
|
+
require_relative "../demiurge"
|
2
|
+
|
3
|
+
class Demiurge::Engine
|
4
|
+
# This method loads new World File code into an existing engine. It
|
5
|
+
# should be passed a list of filenames, normally roughly the same
|
6
|
+
# list that was passed to Demiurge::DSL.engine_from_dsl_files to
|
7
|
+
# create the engine initially.
|
8
|
+
#
|
9
|
+
# See {#reload_from_dsl_text} for more details and allowed
|
10
|
+
# options. It is identical in operation except for taking code as
|
11
|
+
# parameters instead of filenames.
|
12
|
+
#
|
13
|
+
# @see file:RELOADING.md
|
14
|
+
# @see Demiurge::DSL.engine_from_dsl_files
|
15
|
+
# @see #reload_from_dsl_text
|
16
|
+
# @see #load_state_from_dump
|
17
|
+
# @param filenames [Array<String>] An array of filenames, suitable for calling File.read on
|
18
|
+
# @param options [Hash] An optional hash of options for modifying the behavior of the reload
|
19
|
+
# @option options [Boolean] verify_only Only see if the new engine code would load, don't replace any code.
|
20
|
+
# @option options [Boolean] guessing_okay Attempt to guess about renamed objects
|
21
|
+
# @option options [Boolean] no_addition Don't allow adding new StateItems, raise an error instead
|
22
|
+
# @option options [Boolean] no_addition Don't allow removing StateItems, raise an error instead
|
23
|
+
# @return [void] A configured Engine
|
24
|
+
# @since 0.0.1
|
25
|
+
def reload_from_dsl_files(*filenames, options: {})
|
26
|
+
filename_string_pairs = filenames.map { |fn| [fn, File.read(fn)] }
|
27
|
+
engine_from_dsl_text(*filename_string_pairs)
|
28
|
+
end
|
29
|
+
|
30
|
+
# This method loads new World File code into an existing engine. It
|
31
|
+
# should be passed a list of "specs", normally roughly the same list
|
32
|
+
# that was passed to Demiurge::DSL.engine_from_dsl_files to create
|
33
|
+
# the engine initially. A "spec" is either a single string of World
|
34
|
+
# File DSL code, or a two-element array of the form: ["label",
|
35
|
+
# "code"]. Each is a string. "Code" is World File DSL Ruby code,
|
36
|
+
# while "label" is the name that will be used in stack traces.
|
37
|
+
#
|
38
|
+
# Options:
|
39
|
+
#
|
40
|
+
# * verify_only - only check for errors, don't load the new code into the engine, aka "dry run mode"
|
41
|
+
# * guessing_okay - attempt to notice renames and modify old state to new state accordingly
|
42
|
+
# * no_addition - if any new StateItem would be created, raise a NonMatchingStateError
|
43
|
+
# * no_removal - if any StateItem would be removed, raise a NonMatchingStateError
|
44
|
+
#
|
45
|
+
# Note that if "guessing_okay" is turned on, certain cases where
|
46
|
+
# same-type or (in the future) similar-type items are added and
|
47
|
+
# removed may be "guessed" as renames rather than treated as
|
48
|
+
# addition or removal.
|
49
|
+
#
|
50
|
+
# Where the files and world objects are
|
51
|
+
# essentially the same, this should reload any code changes into the
|
52
|
+
# engine. Where they are different, the method will try to determine
|
53
|
+
# the best match between the new and old state, but may fail at
|
54
|
+
# doing so.
|
55
|
+
#
|
56
|
+
# Ordinarily, this method should be called on a fully-configured,
|
57
|
+
# fully-initialized engine with its full state loaded, in between
|
58
|
+
# ticks.
|
59
|
+
#
|
60
|
+
# When in doubt, it's better to save state and reload the engine
|
61
|
+
# from nothing. This gives far better opportunities for a human to
|
62
|
+
# determine what changes have occurred and manually fix any errors.
|
63
|
+
# A game's current state is a complex thing and a computer simply
|
64
|
+
# cannot correctly determine all possible changes to it in a useful
|
65
|
+
# way.
|
66
|
+
#
|
67
|
+
# @see file:RELOADING.md
|
68
|
+
# @see Demiurge::DSL.engine_from_dsl_files
|
69
|
+
# @see Demiurge::DSL.engine_from_dsl_text
|
70
|
+
# @see #load_state_from_dump
|
71
|
+
# @param specs [Array<String>,Array<Array<String>>] An array of specs, see above
|
72
|
+
# @param options [Hash] An optional hash of options for modifying the behavior of the reload
|
73
|
+
# @option options [Boolean] verify_only Only see if the new engine code would load, don't replace any code.
|
74
|
+
# @option options [Boolean] guessing_okay Attempt to guess about renamed objects
|
75
|
+
# @option options [Boolean] no_addition Don't allow adding new StateItems, raise an error instead
|
76
|
+
# @option options [Boolean] no_addition Don't allow removing StateItems, raise an error instead
|
77
|
+
# @return [void] A configured Engine
|
78
|
+
# @since 0.0.1
|
79
|
+
def reload_from_dsl_text(*specs, options: { "verify_only" => false, "guessing_okay" => false, "no_addition" => false, "no_removal" => false })
|
80
|
+
old_engine = self
|
81
|
+
new_engine = nil
|
82
|
+
|
83
|
+
send_notification type: Demiurge::Notifications::LoadWorldVerify, zone: "admin", location: nil, actor: nil, include_context: true
|
84
|
+
|
85
|
+
begin
|
86
|
+
new_engine = Demiurge::DSL.engine_from_dsl_text(*specs)
|
87
|
+
rescue
|
88
|
+
# Didn't work? Leave the existing engine intact, but raise.
|
89
|
+
raise Demiurge::Errors::CannotLoadWorldFiles.new("Error reloading World File text")
|
90
|
+
end
|
91
|
+
|
92
|
+
# Match up old and new state items, but the "admin" InertStateItem doesn't get messed with
|
93
|
+
old_item_names = old_engine.all_item_names - ["admin"]
|
94
|
+
new_item_names = new_engine.all_item_names - ["admin"]
|
95
|
+
|
96
|
+
shared_item_names = old_item_names & new_item_names
|
97
|
+
added_item_names = new_item_names - shared_item_names
|
98
|
+
removed_item_names = old_item_names - shared_item_names
|
99
|
+
renamed_pairs = {}
|
100
|
+
|
101
|
+
if options["guessing_okay"]
|
102
|
+
# For right this second, don't guess. When guessing happens,
|
103
|
+
# this will populate the renamed_pairs hash.
|
104
|
+
end
|
105
|
+
|
106
|
+
if options["no_addition"] && !added_item_names.empty?
|
107
|
+
raise NonMatchingStateError.new "StateItems added when they weren't allowed: #{added_item_names.inspect}!"
|
108
|
+
end
|
109
|
+
|
110
|
+
if options["no_removal"] && !removed_item_names.empty?
|
111
|
+
raise NonMatchingStateError.new "StateItems removed when they weren't allowed: #{removed_item_names.inspect}!"
|
112
|
+
end
|
113
|
+
|
114
|
+
# Okay, finished with error checking - the dry-run is over
|
115
|
+
return if options["verify_only"]
|
116
|
+
|
117
|
+
# Now, replace the engine code
|
118
|
+
|
119
|
+
send_notification type: Demiurge::Notifications::LoadWorldStart, zone: "admin", location: nil, actor: nil, include_context: true
|
120
|
+
|
121
|
+
# Replace all actions performed by ActionItems
|
122
|
+
old_engine.replace_all_actions_for_all_items(new_engine.all_actions_for_all_items)
|
123
|
+
|
124
|
+
# For removed items, delete the StateItem from the old engine
|
125
|
+
removed_item_names.each do |removed_item_name|
|
126
|
+
old_engine.unregister_state_item(old_engine.item_by_name removed_item_name)
|
127
|
+
end
|
128
|
+
|
129
|
+
# For added items, use the state data from the new engine and add the item to the old engine
|
130
|
+
added_item_names.each do |added_item_name|
|
131
|
+
new_item = new_engine.item_by_name(added_item_name)
|
132
|
+
ss = new_item.structured_state
|
133
|
+
old_engine.register_state_item(StateItem.from_name_type(old_engine, *ss))
|
134
|
+
end
|
135
|
+
|
136
|
+
# A rename is basically an add and a remove... But not necessarily
|
137
|
+
# with the same name. And the newly-created item uses the state
|
138
|
+
# from the older item. A rename is also permitted to choose a new
|
139
|
+
# type, so create the new item in the old engine with the new name
|
140
|
+
# and type, but the old state.
|
141
|
+
renamed_pairs.each do |old_name, new_name|
|
142
|
+
old_item = old_engine.item_by_name(old_name)
|
143
|
+
new_item = new_engine.item_by_name(new_name)
|
144
|
+
|
145
|
+
old_type, _, old_state = *old_item.structured_state
|
146
|
+
new_type, _, new_state = *new_item.structured_state
|
147
|
+
|
148
|
+
old_engine.unregister_state_item(old_item)
|
149
|
+
old_engine.register_state_item(StateItem.from_name_type(old_engine, new_type, new_name, old_state))
|
150
|
+
end
|
151
|
+
|
152
|
+
send_notification type: Demiurge::Notifications::LoadWorldEnd, zone: "admin", location: nil, actor: nil, include_context: true
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# This module contains the Builder classes that parse the World File DSL.
|
158
|
+
#
|
159
|
+
# @since 0.0.1
|
160
|
+
module Demiurge::DSL
|
161
|
+
# This is a primary method for creating a new Demiurge Engine. It
|
162
|
+
# should be passed a list of filenames to load World File DSL
|
163
|
+
# from. It will return a fully-configured Engine which has called
|
164
|
+
# finished_init. If the Engine should load from an existing
|
165
|
+
# state-dump, that can be accomplished via load_state_from_dump.
|
166
|
+
#
|
167
|
+
# @see file:RELOADING.md
|
168
|
+
# @see Demiurge::Engine#load_state_from_dump
|
169
|
+
# @see Demiurge::Engine#reload_from_dsl_files
|
170
|
+
# @param filenames [Array<String>] An array of filenames, suitable for calling File.read on
|
171
|
+
# @return [Demiurge::Engine] A configured Engine
|
172
|
+
# @since 0.0.1
|
173
|
+
def self.engine_from_dsl_files(*filenames)
|
174
|
+
filename_string_pairs = filenames.map { |fn| [fn, File.read(fn)] }
|
175
|
+
engine_from_dsl_text(*filename_string_pairs)
|
176
|
+
end
|
177
|
+
|
178
|
+
# This method takes either strings containing World File DSL text,
|
179
|
+
# or name/string pairs. If a pair is supplied, the name gives the
|
180
|
+
# origin of the text for error messages.
|
181
|
+
#
|
182
|
+
# @see file:RELOADING.md
|
183
|
+
# @see Demiurge::Engine#load_state_from_dump
|
184
|
+
# @see Demiurge::Engine#reload_from_dsl_text
|
185
|
+
# @param specs [Array<String>, Array<Array<String>>] Either an array of chunks of DSL text, or an Array of two-element Arrays. Each two-element Array is a String name followed by a String of DSL text
|
186
|
+
# @return [Demiurge::Engine] A configured Engine
|
187
|
+
# @since 0.0.1
|
188
|
+
def self.engine_from_dsl_text(*specs)
|
189
|
+
builder = Demiurge::DSL::TopLevelBuilder.new
|
190
|
+
|
191
|
+
specs.each do |spec|
|
192
|
+
if spec.is_a?(String)
|
193
|
+
builder.instance_eval spec
|
194
|
+
elsif spec.is_a?(Array)
|
195
|
+
if spec.size != 2
|
196
|
+
raise "Not sure what to do with a #{spec.size}-elt array, normally this is a filename/string pair!"
|
197
|
+
end
|
198
|
+
builder.instance_eval spec[1], spec[0]
|
199
|
+
else
|
200
|
+
raise "Not sure what to do in engine_from_dsl_text with a #{spec.class}!"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
builder.built_engine
|
205
|
+
end
|
206
|
+
|
207
|
+
# ActionItemBuilder is the parent class of all Builder classes
|
208
|
+
# except the {Demiurge::DSL::TopLevelBuilder}. It's used for a block
|
209
|
+
# of the World File DSL.
|
210
|
+
#
|
211
|
+
# @since 0.0.1
|
212
|
+
class ActionItemBuilder
|
213
|
+
# @return [StateItem] The item built by this builder
|
214
|
+
attr_reader :built_item
|
215
|
+
|
216
|
+
private
|
217
|
+
def check_options(hash, legal_options)
|
218
|
+
illegal_options = hash.keys - legal_options
|
219
|
+
raise "Illegal options #{illegal_options.inspect} passed to #{caller(1, 3).inspect}!" unless illegal_options.empty?
|
220
|
+
end
|
221
|
+
public
|
222
|
+
|
223
|
+
# @return [Array<String>] Legal options to pass to {ActionItemBuilder#initialize}
|
224
|
+
LEGAL_OPTIONS = [ "state", "type", "no_build" ]
|
225
|
+
|
226
|
+
# Initialize a DSL Builder block to configure some sort of ActionItem.
|
227
|
+
#
|
228
|
+
# @param name [String] The name to be registered with the Engine
|
229
|
+
# @param engine [Demiurge::Engine] The engine that will include this item
|
230
|
+
# @param options [Hash] Options for how the DSL block acts
|
231
|
+
# @option options [Hash] state The initial state Hash to create the item with
|
232
|
+
# @option options [String] type The item type to create
|
233
|
+
# @option options [Boolean] no_build If true, don't create and register a new StateItem with the Engine
|
234
|
+
# @return [void]
|
235
|
+
# @since 0.0.1
|
236
|
+
def initialize(name, engine, options = {})
|
237
|
+
check_options(options, LEGAL_OPTIONS)
|
238
|
+
@name = name
|
239
|
+
@engine = engine
|
240
|
+
@state = options["state"] || {}
|
241
|
+
@position = nil
|
242
|
+
@display = nil # This is display-specific information that gets passed to the display library
|
243
|
+
|
244
|
+
unless options["type"]
|
245
|
+
raise "You must pass a type when initializing a builder!"
|
246
|
+
end
|
247
|
+
unless options["no_build"]
|
248
|
+
@built_item = ::Demiurge::StateItem.from_name_type(@engine, options["type"], @name, @state)
|
249
|
+
@engine.register_state_item(@built_item)
|
250
|
+
end
|
251
|
+
nil
|
252
|
+
end
|
253
|
+
|
254
|
+
# If the DSL block sets state on the Builder object, this allows
|
255
|
+
# it to get to the internal Hash object rather than a
|
256
|
+
# wrapper. This should only be used internally to the DSL, not by
|
257
|
+
# others.
|
258
|
+
#
|
259
|
+
# @see #state
|
260
|
+
# @return [Hash] The state Hash
|
261
|
+
# @api private
|
262
|
+
# @since 0.0.1
|
263
|
+
def __state_internal
|
264
|
+
@built_item.state
|
265
|
+
end
|
266
|
+
|
267
|
+
# Get the state, or at least a wrapper object to it, for DSL usage.
|
268
|
+
#
|
269
|
+
# @return [Demiurge::ActionItemInternal::ActionItemStateWrapper] The state Hash wrapper
|
270
|
+
# @since 0.0.1
|
271
|
+
def state
|
272
|
+
@wrapper ||= ::Demiurge::ActionItemInternal::ActionItemStateWrapper.new(self)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Register an action with the Engine. Since StateItems are
|
276
|
+
# effectively disposable, we need somewhere outside of the
|
277
|
+
# StateItem itself to store its actions as Ruby code. The Engine
|
278
|
+
# is the place they are stored.
|
279
|
+
#
|
280
|
+
# @param action [Hash] The action hash to use as the internal action structure
|
281
|
+
# @api private
|
282
|
+
# @since 0.0.1
|
283
|
+
private
|
284
|
+
def register_built_action(action)
|
285
|
+
raise("Must specify a string 'name' to register_build_action! Only gave #{action.inspect}!") unless action["name"]
|
286
|
+
check_options(action, ::Demiurge::ActionItem::ACTION_LEGAL_KEYS)
|
287
|
+
@built_item.register_actions(action["name"] => action)
|
288
|
+
end
|
289
|
+
public
|
290
|
+
|
291
|
+
# Perform the given action every so many ticks. This will set up
|
292
|
+
# the necessary state entries to cause the action to occur each
|
293
|
+
# time that many ticks pass. The given action name is attached to
|
294
|
+
# the given block (if any.) The named action can be modified using
|
295
|
+
# define_action if you want to set extra settings like engine_code
|
296
|
+
# or busy for the action in question. If no block is given, you
|
297
|
+
# should use define_action to create the action in question, or it
|
298
|
+
# will have no definition and cause errors.
|
299
|
+
#
|
300
|
+
# @param action_name [String] The action name for this item to use repeatedly
|
301
|
+
# @param t [Integer] The number of ticks that pass between actions
|
302
|
+
# @yield [...] Called when the action is performed with any arguments supplied by the caller
|
303
|
+
# @yieldreturn [void]
|
304
|
+
# @return [void]
|
305
|
+
# @since 0.0.1
|
306
|
+
def every_X_ticks(action_name, t, &block)
|
307
|
+
raise("Must provide a positive number for how many ticks, not #{t.inspect}!") unless t.is_a?(Numeric) && t >= 0.0
|
308
|
+
@built_item.state["everies"] ||= []
|
309
|
+
@built_item.state["everies"] << { "action" => action_name, "every" => t, "counter" => 0 }
|
310
|
+
@built_item.register_actions(action_name => { "name" => action_name, "block" => block })
|
311
|
+
nil
|
312
|
+
end
|
313
|
+
|
314
|
+
# Set the position of the built object.
|
315
|
+
#
|
316
|
+
# @param pos [String] The new position string for this built object.
|
317
|
+
# @return [void]
|
318
|
+
# @since 0.0.1
|
319
|
+
def position(pos)
|
320
|
+
@built_item.state["position"] = pos
|
321
|
+
nil
|
322
|
+
end
|
323
|
+
|
324
|
+
# Pass a block through that is intended for the display library to
|
325
|
+
# use later. If no display library is used, this is a no-op.
|
326
|
+
#
|
327
|
+
# @yield [] The block will be called by the display library, in a display-library-specific context, or not at all
|
328
|
+
# @return [void]
|
329
|
+
# @since 0.0.1
|
330
|
+
def display(&block)
|
331
|
+
# Need to figure out how to pass this through to the Display
|
332
|
+
# library. By design, the simulation/state part of Demiurge
|
333
|
+
# ignores this completely.
|
334
|
+
@built_item.register_actions("$display" => { "name" => "$display", "block" => block })
|
335
|
+
nil
|
336
|
+
end
|
337
|
+
|
338
|
+
# The specified action will be called for notifications of the
|
339
|
+
# appropriate type.
|
340
|
+
# @todo Figure out timing of the subscription - right now it will use the item's location midway through parsing the DSL!
|
341
|
+
#
|
342
|
+
# @param event [String] The name of the notification to subscribe to
|
343
|
+
# @param action_name [String] The action name of the new action
|
344
|
+
# @param options [Hash] Additional specifications about what/how to subscribe
|
345
|
+
# @option options [String,:all] location The location name to subscribe for - defaults to this item's location
|
346
|
+
# @option options [String,:all] zone The zone name to subscribe for - defaults to this item's zone
|
347
|
+
# @option options [String,:all] actor The acting item name to subscribe for - defaults to any item
|
348
|
+
# @yield [Hash] Receives notification hashes when these notifications occur
|
349
|
+
# @return [void]
|
350
|
+
# @since 0.0.1
|
351
|
+
def on_notification(event, action_name, options = {}, &block)
|
352
|
+
@built_item.state["on_handlers"] ||= {}
|
353
|
+
@built_item.state["on_handlers"][event] = action_name
|
354
|
+
register_built_action("name" => action_name, "block" => block)
|
355
|
+
|
356
|
+
location = options[:location] || options["location"] || @built_item.location
|
357
|
+
zone = options[:zone] || options["zone"] || location.zone_name || @built_item.zone_name
|
358
|
+
item = options[:actor] || options["actor"] || :all
|
359
|
+
|
360
|
+
@engine.subscribe_to_notifications type: event, zone: zone, location: location, actor: item do |notification|
|
361
|
+
# To keep this statedump-safe, need to look up the item again
|
362
|
+
# every time. @built_item isn't guaranteed to last.
|
363
|
+
@engine.item_by_name(@name).run_action(action_name, notification)
|
364
|
+
end
|
365
|
+
nil
|
366
|
+
end
|
367
|
+
|
368
|
+
# "on" is an older name for "on_notification" and is deprecated.
|
369
|
+
# @deprecated
|
370
|
+
alias_method :on, :on_notification
|
371
|
+
|
372
|
+
# The specified action will be called for Intentions using the
|
373
|
+
# appropriate action. This is used to modify or cancel an
|
374
|
+
# Intention before it runs.
|
375
|
+
#
|
376
|
+
# @param caught_action [String] The action type of the Intention being caught, or "all" for all intentions
|
377
|
+
# @param action_to_run [String] The action name of the new intercepting action
|
378
|
+
# @yield [Intention] Receives the Intention when these Intentions occur
|
379
|
+
# @return [void]
|
380
|
+
# @since 0.0.1
|
381
|
+
def on_intention(caught_action, action_to_run, &block)
|
382
|
+
@built_item.state["on_action_handlers"] ||= {}
|
383
|
+
raise "Already have an on_action (offer) handler for action #{caught_action}! Failing!" if @built_item.state["on_action_handlers"][caught_action]
|
384
|
+
@built_item.state["on_action_handlers"][caught_action] = action_to_run
|
385
|
+
register_built_action("name" => action_to_run, "block" => block)
|
386
|
+
nil
|
387
|
+
end
|
388
|
+
|
389
|
+
# "on_action" is an older name for "on_intention" and is deprecated.
|
390
|
+
# @deprecated
|
391
|
+
alias_method :on_action, :on_intention
|
392
|
+
|
393
|
+
# If you want to define an action for later calling, or to set
|
394
|
+
# options on an action that was defined as part of another
|
395
|
+
# handler, you can call define_action to make that happen.
|
396
|
+
#
|
397
|
+
# @example Make an every_X_ticks action also keep the agent busy and run as Engine code
|
398
|
+
# ```
|
399
|
+
# every_X_ticks("burp", 15) { engine.item_by_name("sooper sekrit").ruby_only_burp_action }
|
400
|
+
# define_action("burp", "engine_code" => true, "busy" => 7)
|
401
|
+
# ```
|
402
|
+
#
|
403
|
+
# @param action_name [String] The action name to declare or modify
|
404
|
+
# @param options [Hash] Options for this action
|
405
|
+
# @option options [Integer] busy How many ticks an agent should remain busy for after taking this action
|
406
|
+
# @option options [Boolean] engine_code If true, use the EngineBlockRunner instead of a normal runner for this code; usually a bad idea
|
407
|
+
# @option options [Array<String>] tags Tags that the action can be queried by later - useful for tagging player or agent actions, or admin-only actions
|
408
|
+
# @yield [...] Actions receive whatever arguments their later caller supplies
|
409
|
+
# @yieldreturn [void]
|
410
|
+
# @return [void]
|
411
|
+
# @since 0.0.1
|
412
|
+
def define_action(action_name, options = {}, &block)
|
413
|
+
legal_options = [ "busy", "engine_code", "tags" ]
|
414
|
+
illegal_keys = options.keys - legal_options
|
415
|
+
raise("Illegal keys #{illegal_keys.inspect} passed to define_action of #{action_name.inspect}!") unless illegal_keys.empty?;
|
416
|
+
register_built_action({ "name" => action_name, "block" => block }.merge(options))
|
417
|
+
nil
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# This is the top-level DSL Builder class, for parsing the top syntactic level of the World Files.
|
422
|
+
#
|
423
|
+
# @since 0.0.1
|
424
|
+
class TopLevelBuilder
|
425
|
+
# This is the private structure of type names that are registered with the Demiurge World File DSL
|
426
|
+
@@types = {}
|
427
|
+
|
428
|
+
# Constructor for a new set of World Files and their top-level state.
|
429
|
+
def initialize(options = {})
|
430
|
+
@zones = []
|
431
|
+
@engine = options["engine"] || ::Demiurge::Engine.new(types: @@types, state: [])
|
432
|
+
end
|
433
|
+
|
434
|
+
# For now, this just declares an InertStateItem for a given name.
|
435
|
+
# It doesn't change the behavior at all. It just keeps that item
|
436
|
+
# name from being "orphaned" state that doesn't correspond to any
|
437
|
+
# state item.
|
438
|
+
#
|
439
|
+
# Later, this may be a way to describe how important or transitory
|
440
|
+
# state is - is it reset like a zone? Completely transient?
|
441
|
+
# Cleared per reboot?
|
442
|
+
#
|
443
|
+
# @param item_name [String] The item name for scoping the state in the Engine
|
444
|
+
# @param options [Hash] Options about the InertStateItem
|
445
|
+
# @option options [String] zone The zone this InertStateItem considers itself to be in, defaults to "admin"
|
446
|
+
# @option options [Hash] state The initial state Hash
|
447
|
+
# @option options [String] type The object type to instantiate, if not InertStateItem
|
448
|
+
# @return [void]
|
449
|
+
# @since 0.0.1
|
450
|
+
def inert(item_name, options = {})
|
451
|
+
zone_name = options["zone"] || "admin"
|
452
|
+
state = options["state"] || {}
|
453
|
+
state.merge! "zone" => zone_name, "home_zone" => zone_name
|
454
|
+
inert_item = ::Demiurge::StateItem.from_name_type(@engine, options["type"] || "InertStateItem", item_name, state)
|
455
|
+
@engine.register_state_item(inert_item)
|
456
|
+
nil
|
457
|
+
end
|
458
|
+
|
459
|
+
# Start a new Zone block, using a ZoneBuilder.
|
460
|
+
def zone(name, options = {}, &block)
|
461
|
+
if @zones.any? { |z| z.name == name }
|
462
|
+
# Reopening an existing zone
|
463
|
+
builder = ZoneBuilder.new(name, @engine, options.merge("existing" => @zones.detect { |z| z.name == name }))
|
464
|
+
else
|
465
|
+
builder = ZoneBuilder.new(name, @engine, options)
|
466
|
+
end
|
467
|
+
|
468
|
+
builder.instance_eval(&block)
|
469
|
+
new_zone = builder.built_item
|
470
|
+
|
471
|
+
@zones |= [ new_zone ] if new_zone # Add if not already present
|
472
|
+
nil
|
473
|
+
end
|
474
|
+
|
475
|
+
# It's hard to figure out where and how to register types and
|
476
|
+
# plugins for the World File format. By their nature, they need to
|
477
|
+
# be in place before an Engine exists, so that's not the right
|
478
|
+
# place. If they didn't exist before engines, we'd somehow need to
|
479
|
+
# register them with each engine as it was created. Since Engines
|
480
|
+
# keep track of that, that's exactly the same problem we're trying
|
481
|
+
# to solve, just for the Engine builder. And it seems like
|
482
|
+
# "register this plugin with Demiurge World Files" is more of a
|
483
|
+
# process-global operation than a per-Engine operation. So these
|
484
|
+
# wind up in awkward spots.
|
485
|
+
def self.register_type(name, klass)
|
486
|
+
if @@types[name.to_s]
|
487
|
+
raise("Attempting to re-register type #{name.inspect} with a different class!") unless @@types[name.to_s] == klass
|
488
|
+
else
|
489
|
+
@@types[name.to_s] = klass
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
# Return the built Engine, but first call the .finished_init
|
494
|
+
# callback. This will make sure that cached and duplicated data
|
495
|
+
# structures are properly filled in.
|
496
|
+
def built_engine
|
497
|
+
@engine.finished_init
|
498
|
+
@engine
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# Declare an "agent" block in the World File DSL.
|
503
|
+
class AgentBuilder < ActionItemBuilder
|
504
|
+
def initialize(name, engine, options = {})
|
505
|
+
options = { "type" => "Agent" }.merge(options)
|
506
|
+
super(name, engine, options)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
# Declare a "zone" block in the World File DSL.
|
511
|
+
class ZoneBuilder < ActionItemBuilder
|
512
|
+
# Constructor. See if this zone name already exists, and either
|
513
|
+
# create a new zone or append to the old one.
|
514
|
+
def initialize(name, engine, options = {})
|
515
|
+
@existing = options.delete("existing")
|
516
|
+
if @existing
|
517
|
+
old_type = @existing.state_type
|
518
|
+
new_type = options["type"]
|
519
|
+
if new_type && old_type != new_type
|
520
|
+
raise("Can't reopen zone with type #{(options["type"] || "Unspecified").inspect} after creating with type #{old_type.inspect}!")
|
521
|
+
end
|
522
|
+
options["no_build"] = true
|
523
|
+
@built_item = @existing
|
524
|
+
end
|
525
|
+
super(name, engine, options.merge("type" => options["type"] || "Zone"))
|
526
|
+
@locations = []
|
527
|
+
@agents = []
|
528
|
+
end
|
529
|
+
|
530
|
+
# Declare a location in this zone.
|
531
|
+
def location(name, options = {}, &block)
|
532
|
+
state = { "zone" => @name, "home_zone" => @name }.merge(options)
|
533
|
+
builder = LocationBuilder.new(name, @engine, "type" => options["type"] || "Location", "state" => state)
|
534
|
+
builder.instance_eval(&block)
|
535
|
+
@built_item.state["contents"] << name
|
536
|
+
nil
|
537
|
+
end
|
538
|
+
|
539
|
+
# Declare an agent in this zone. If the agent doesn't get a
|
540
|
+
# location declaration, by default the agent will usually be
|
541
|
+
# invisible (not an interactable location) but will be
|
542
|
+
# instantiable as a parent.
|
543
|
+
def agent(name, options = {}, &block)
|
544
|
+
state = { "zone" => @name, "home_zone" => @name }.merge(options)
|
545
|
+
builder = AgentBuilder.new(name, @engine, "type" => options["type"] || "Agent", "state" => state)
|
546
|
+
builder.instance_eval(&block)
|
547
|
+
@built_item.state["contents"] << name
|
548
|
+
nil
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
# Declare a "location" block in a World File.
|
553
|
+
class LocationBuilder < ActionItemBuilder
|
554
|
+
# Constructor for a "location" DSL block
|
555
|
+
def initialize(name, engine, options = {})
|
556
|
+
options["type"] ||= "Location"
|
557
|
+
super
|
558
|
+
@agents = []
|
559
|
+
end
|
560
|
+
|
561
|
+
# Declare a description for this location.
|
562
|
+
def description(d)
|
563
|
+
@state["description"] = d
|
564
|
+
end
|
565
|
+
|
566
|
+
# Declare an agent in this location.
|
567
|
+
def agent(name, options = {}, &block)
|
568
|
+
state = { "position" => @name, "zone" => @state["zone"], "home_zone" => @state["zone"] }
|
569
|
+
builder = AgentBuilder.new(name, @engine, options.merge("state" => state) )
|
570
|
+
builder.instance_eval(&block)
|
571
|
+
@built_item.state["contents"] << name
|
572
|
+
nil
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
end
|
577
|
+
|
578
|
+
Demiurge::DSL::TopLevelBuilder.register_type "ActionItem", Demiurge::ActionItem
|
579
|
+
Demiurge::DSL::TopLevelBuilder.register_type "InertStateItem", Demiurge::InertStateItem
|
580
|
+
Demiurge::DSL::TopLevelBuilder.register_type "Zone", Demiurge::Zone
|
581
|
+
Demiurge::DSL::TopLevelBuilder.register_type "Location", Demiurge::Location
|
582
|
+
Demiurge::DSL::TopLevelBuilder.register_type "Agent", Demiurge::Agent
|
583
|
+
Demiurge::DSL::TopLevelBuilder.register_type "WanderingAgent", Demiurge::WanderingAgent
|