inform-runtime 1.2.2 → 1.3.1
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 +4 -4
- data/README.md +33 -41
- data/lib/story_teller/articles.rb +2 -1
- data/lib/story_teller/builtins.rb +63 -10
- data/lib/story_teller/color.rb +2 -1
- data/lib/story_teller/command.rb +4 -1
- data/lib/story_teller/context.rb +23 -27
- data/lib/story_teller/daemon.rb +2 -2
- data/lib/story_teller/engine.rb +80 -16
- data/lib/story_teller/events.rb +9 -2
- data/lib/story_teller/experimental/handler_dsl.rb +1 -16
- data/lib/story_teller/experimental/reverse_engineer_class.rb +1 -1
- data/lib/story_teller/grammar_parser.rb +60 -13
- data/lib/story_teller/helpers.rb +8 -18
- data/lib/story_teller/history.rb +2 -1
- data/lib/story_teller/inflector.rb +4 -2
- data/lib/story_teller/inform/ephemeral/link.rb +63 -31
- data/lib/story_teller/inform/ephemeral/module.rb +6 -5
- data/lib/story_teller/inform/ephemeral/object.rb +223 -236
- data/lib/story_teller/inform/ephemeral/tag.rb +27 -14
- data/lib/story_teller/io.rb +99 -49
- data/lib/story_teller/kernel.rb +13 -5
- data/lib/story_teller/library/bootstrap.rb +10 -27
- data/lib/story_teller/library/declarations.rb +1 -1
- data/lib/story_teller/library/directives.rb +1 -1
- data/lib/story_teller/library/loader.rb +4 -20
- data/lib/story_teller/library/location.rb +9 -3
- data/lib/story_teller/library.rb +1 -1
- data/lib/story_teller/logging.rb +165 -67
- data/lib/story_teller/mixins.rb +11 -4
- data/lib/story_teller/plurals.rb +2 -1
- data/lib/story_teller/privileges.rb +117 -0
- data/lib/story_teller/prototype.rb +158 -32
- data/lib/story_teller/publication.rb +2 -1
- data/lib/story_teller/session.rb +115 -25
- data/lib/story_teller/stdlib.rb +232 -2
- data/lib/story_teller/subscription.rb +2 -1
- data/lib/story_teller/tree.rb +2 -1
- data/lib/story_teller/version.rb +2 -2
- data/lib/story_teller/world_tree.rb +21 -23
- data/lib/story_teller.rb +18 -5
- metadata +7 -10
- data/lib/story_teller/core.rb +0 -38
- data/lib/story_teller/ephemeral_adapter.rb +0 -40
- data/lib/story_teller/inform/base.rb +0 -160
- data/lib/story_teller/model_adapter.rb +0 -132
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# encoding: utf-8
|
|
3
3
|
# frozen_string_literal: false
|
|
4
4
|
|
|
5
|
-
# Copyright Nels Nelson 2008-
|
|
5
|
+
# Copyright Nels Nelson 2008-2026 but freely usable (see license)
|
|
6
6
|
#
|
|
7
7
|
# This file is part of StoryTeller.
|
|
8
8
|
#
|
|
@@ -34,7 +34,9 @@ module Inform
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def values
|
|
37
|
-
|
|
37
|
+
result = super
|
|
38
|
+
return result if result && result != __method__
|
|
39
|
+
raise NoMethodError, "undefined method `#{__method__}' for #{self}"
|
|
38
40
|
rescue StandardError => e
|
|
39
41
|
log.warn "Unexpected error invoking super(Inform::Prototypical#values): #{e.message}"
|
|
40
42
|
instance_variables_map
|
|
@@ -126,25 +128,44 @@ module Inform
|
|
|
126
128
|
MethodWriterPattern = %r{=$}.freeze
|
|
127
129
|
LinkPattern = %r{.+_to$}.freeze
|
|
128
130
|
|
|
131
|
+
# rubocop: disable Style/MissingRespondToMissing
|
|
132
|
+
def method_missing(method, *args, &block)
|
|
133
|
+
stack = Thread.current[:prototypical_method_missing_stack] ||= []
|
|
134
|
+
frame = [object_id, method]
|
|
135
|
+
|
|
136
|
+
if stack.include?(frame)
|
|
137
|
+
raise NoMethodError, "Aborting Prototypical missing method " \
|
|
138
|
+
"resolution #{self.class}##{method} on #{inspect}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
stack << frame
|
|
142
|
+
attempt_resolution(method, *args, &block)
|
|
143
|
+
ensure
|
|
144
|
+
stack&.pop
|
|
145
|
+
end
|
|
146
|
+
# rubocop: enable Style/MissingRespondToMissing
|
|
147
|
+
|
|
129
148
|
# rubocop: disable Metrics/AbcSize
|
|
130
149
|
# rubocop: disable Metrics/CyclomaticComplexity
|
|
131
150
|
# rubocop: disable Metrics/MethodLength
|
|
132
151
|
# rubocop: disable Metrics/PerceivedComplexity
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return super if method == :to_ary
|
|
152
|
+
def attempt_resolution(method, *args, &block)
|
|
153
|
+
raise NoMethodError, "undefined method `#{method}' for #{self}" if method == :to_ary
|
|
136
154
|
m = method.to_s
|
|
137
|
-
|
|
155
|
+
raise NoMethodError, "undefined method `#{method}' for #{self}" if MethodBooleanOrBangPattern.match?(m)
|
|
138
156
|
mname = MethodWriterPattern.match?(m) ? m.chop : m
|
|
139
157
|
property = mname.to_sym
|
|
140
158
|
variable = format(VariableTemplate, property: property).to_sym
|
|
141
159
|
event = context_event
|
|
142
160
|
|
|
161
|
+
library = respond_to?(:inflib, true) ? inflib : nil
|
|
162
|
+
object_properties = respond_to?(:properties, true) ? properties : nil
|
|
163
|
+
|
|
143
164
|
if MethodWriterPattern.match?(m) || (LinkPattern.match?(m) && !args.empty?)
|
|
144
165
|
if !event.nil? && StoryTeller::Engine.invocation_properties.include?(property)
|
|
145
166
|
event.send(method, *args, &block)
|
|
146
|
-
elsif
|
|
147
|
-
|
|
167
|
+
elsif library&.library_method?(method)
|
|
168
|
+
library.send(method, *args, &block)
|
|
148
169
|
else
|
|
149
170
|
self.prototype(method, *args, &block)
|
|
150
171
|
end
|
|
@@ -152,26 +173,33 @@ module Inform
|
|
|
152
173
|
result = event.send(method, *args, &block)
|
|
153
174
|
log.debug("Value for event #{event}.#{method} is: #{result || 'nil'}") if defined? DEBUG
|
|
154
175
|
result
|
|
155
|
-
elsif
|
|
156
|
-
|
|
157
|
-
elsif
|
|
158
|
-
|
|
159
|
-
elsif
|
|
160
|
-
|
|
161
|
-
elsif
|
|
176
|
+
elsif library&.library_method?(method)
|
|
177
|
+
library.send(method, *args, &block)
|
|
178
|
+
elsif library.respond_to?(method) && StoryTeller::Engine.invocation_properties.include?(property)
|
|
179
|
+
library.send(method, *args, &block)
|
|
180
|
+
elsif library&.instance_variable_get(variable) || library&.instance_variables&.include?(variable)
|
|
181
|
+
library.instance_variable_get(variable)
|
|
182
|
+
elsif property_defined?(object_properties, property) ||
|
|
162
183
|
(respond_to?(:_key?) && _key?(property)) ||
|
|
163
184
|
!args.empty?
|
|
164
185
|
self.prototype(method, *args, &block)
|
|
186
|
+
# TODO: Remove (2026-04-28)
|
|
187
|
+
# Experiment with supporting only property setting for those
|
|
188
|
+
# declared in StoryTeller::Library.declarations.properties.
|
|
189
|
+
# elsif (properties.respond_to?(:key?) && properties.key?(property)) ||
|
|
190
|
+
# (respond_to?(:_key?) && _key?(property)) ||
|
|
191
|
+
# StoryTeller::Library.declarations.properties.key?(property)
|
|
192
|
+
# self.prototype(method, *args, &block)
|
|
165
193
|
else
|
|
166
|
-
|
|
194
|
+
raise NoMethodError, "undefined method `#{method}' for #{self}"
|
|
167
195
|
end
|
|
168
196
|
end
|
|
169
197
|
# rubocop: enable Metrics/AbcSize
|
|
170
198
|
# rubocop: enable Metrics/CyclomaticComplexity
|
|
171
199
|
# rubocop: enable Metrics/MethodLength
|
|
172
200
|
# rubocop: enable Metrics/PerceivedComplexity
|
|
173
|
-
# rubocop: enable Style/MissingRespondToMissing
|
|
174
201
|
|
|
202
|
+
# TODO: Remove (2026-04-28)
|
|
175
203
|
# def respond_to_missing?(method, include_all=false)
|
|
176
204
|
# m = method.to_s
|
|
177
205
|
# return false if m =~ /\?$/
|
|
@@ -198,32 +226,81 @@ module Inform
|
|
|
198
226
|
).freeze
|
|
199
227
|
|
|
200
228
|
# rubocop: disable Metrics/AbcSize
|
|
201
|
-
# rubocop: disable Metrics/CyclomaticComplexity
|
|
202
229
|
# rubocop: disable Metrics/MethodLength
|
|
203
230
|
def prototype(key, *args, &block)
|
|
204
231
|
ensure_properties_attribute!
|
|
205
232
|
accessor, writer = accessor_and_writer(key)
|
|
233
|
+
link_result = nil
|
|
234
|
+
|
|
206
235
|
if key_refers_to_link?(key)
|
|
207
236
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
237
|
+
|
|
238
|
+
if args.empty?
|
|
239
|
+
link_result = resolve_link_property(key)
|
|
240
|
+
else
|
|
241
|
+
linked_object = object_for_link_value(args.first)
|
|
242
|
+
if linked_object.nil?
|
|
243
|
+
self.properties[key] = args.first
|
|
244
|
+
else
|
|
245
|
+
link_result = set_link_property(key, linked_object)
|
|
246
|
+
end
|
|
214
247
|
end
|
|
215
|
-
self.properties[key] = args.first
|
|
216
248
|
end
|
|
249
|
+
|
|
217
250
|
accessor = format(AccessorMethodTemplate, accessor: accessor)
|
|
218
251
|
self.instance_eval(accessor, __FILE__, __LINE__)
|
|
219
252
|
writer = format(WriterMethodTemplate, writer: writer)
|
|
220
253
|
self.instance_eval(writer, __FILE__, __LINE__)
|
|
254
|
+
|
|
255
|
+
return link_result unless link_result.nil?
|
|
256
|
+
|
|
221
257
|
self.send(key, *args, &block)
|
|
222
258
|
end
|
|
223
259
|
# rubocop: enable Metrics/AbcSize
|
|
224
|
-
# rubocop: enable Metrics/CyclomaticComplexity
|
|
225
260
|
# rubocop: enable Metrics/MethodLength
|
|
226
261
|
|
|
262
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
263
|
+
def resolve_link_property(key)
|
|
264
|
+
return nil unless key_refers_to_link?(key)
|
|
265
|
+
|
|
266
|
+
linked_object = self._get_object(key) if self.respond_to?(:_key?) &&
|
|
267
|
+
self._key?(key) &&
|
|
268
|
+
self.respond_to?(:_get_object)
|
|
269
|
+
return linked_object unless linked_object.nil?
|
|
270
|
+
|
|
271
|
+
property_key = property_key_for(key)
|
|
272
|
+
return nil if property_key.nil?
|
|
273
|
+
|
|
274
|
+
linked_object = object_for_link_value(self.properties[property_key])
|
|
275
|
+
return nil if linked_object.nil?
|
|
276
|
+
|
|
277
|
+
set_link_property(key, linked_object, property_key: property_key)
|
|
278
|
+
end
|
|
279
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
280
|
+
|
|
281
|
+
def object_for_link_value(value)
|
|
282
|
+
return value if value.respond_to?(:object?) && value.object?
|
|
283
|
+
return nil unless value.is_a?(Symbol) || value.is_a?(String)
|
|
284
|
+
|
|
285
|
+
object = constant_for_link_value(value)
|
|
286
|
+
return object if object.respond_to?(:object?) && object.object?
|
|
287
|
+
|
|
288
|
+
nil
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def constant_for_link_value(value)
|
|
292
|
+
names = value.to_s.split('::').reject(&:empty?)
|
|
293
|
+
return nil if names.empty?
|
|
294
|
+
|
|
295
|
+
names.inject(::Object) do |scope, name|
|
|
296
|
+
return nil unless scope.const_defined?(name)
|
|
297
|
+
|
|
298
|
+
scope.const_get(name)
|
|
299
|
+
end
|
|
300
|
+
rescue NameError
|
|
301
|
+
nil
|
|
302
|
+
end
|
|
303
|
+
|
|
227
304
|
MISSING_PROPERTIES_MESSAGE = 'Missing properties attribute for: ' \
|
|
228
305
|
'%<identity>s'.freeze
|
|
229
306
|
|
|
@@ -232,14 +309,46 @@ module Inform
|
|
|
232
309
|
raise StandardError, format(MISSING_PROPERTIES_MESSAGE, identity: self.identity)
|
|
233
310
|
end
|
|
234
311
|
|
|
312
|
+
def property_defined?(object_properties, property)
|
|
313
|
+
return false unless object_properties.respond_to?(:key?)
|
|
314
|
+
|
|
315
|
+
object_properties.key?(property) || object_properties.key?(property.to_s)
|
|
316
|
+
end
|
|
317
|
+
|
|
235
318
|
def key_refers_to_link?(key)
|
|
236
|
-
|
|
319
|
+
return false unless LinkPattern.match?(key.to_s)
|
|
320
|
+
|
|
321
|
+
declared_property?(key)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def declared_property?(key)
|
|
325
|
+
return false unless defined?(StoryTeller::Library)
|
|
326
|
+
return false unless StoryTeller::Library.respond_to?(:declarations)
|
|
327
|
+
|
|
328
|
+
declarations = StoryTeller::Library.declarations
|
|
329
|
+
properties = declarations.properties if declarations.respond_to?(:properties)
|
|
330
|
+
return false unless properties.respond_to?(:key?)
|
|
331
|
+
|
|
332
|
+
properties.key?(key.to_sym) || properties.key?(key.to_s)
|
|
333
|
+
rescue StandardError
|
|
334
|
+
false
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def property_key_for(key)
|
|
338
|
+
return key if self.properties.key?(key)
|
|
339
|
+
|
|
340
|
+
string_key = key.to_s
|
|
341
|
+
return string_key if self.properties.key?(string_key)
|
|
342
|
+
|
|
343
|
+
nil
|
|
237
344
|
end
|
|
238
345
|
|
|
239
346
|
def _get(key)
|
|
240
347
|
result = nil
|
|
241
348
|
result = self._get_object(key) if self.respond_to?(:_key?) && self._key?(key)
|
|
242
|
-
result
|
|
349
|
+
return result unless result.nil?
|
|
350
|
+
|
|
351
|
+
resolve_link_property(key) || self.properties.dup[property_key_for(key)]
|
|
243
352
|
end
|
|
244
353
|
|
|
245
354
|
# rubocop: disable Metrics/AbcSize
|
|
@@ -252,11 +361,13 @@ module Inform
|
|
|
252
361
|
result = nil
|
|
253
362
|
if values.first.object?
|
|
254
363
|
result = self._set_object(key, values.first) if respond_to? :_set_object
|
|
364
|
+
elsif key_refers_to_link?(key) && (linked_object = object_for_link_value(values.first))
|
|
365
|
+
result = set_link_property(key, linked_object)
|
|
255
366
|
elsif values.first.nil? && self.respond_to?(:_key?) && self._key?(key)
|
|
256
367
|
result = self._unset_object(key) if self.respond_to?(:_unset_object)
|
|
257
368
|
elsif values.first.nil?
|
|
258
369
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
259
|
-
self.properties.delete(key)
|
|
370
|
+
self.properties.delete(property_key_for(key))
|
|
260
371
|
self.save_changes if self.respond_to?(:save_changes)
|
|
261
372
|
elsif values.length > 1
|
|
262
373
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
@@ -269,7 +380,7 @@ module Inform
|
|
|
269
380
|
self.properties[key] = values.first
|
|
270
381
|
self.save_changes if self.respond_to?(:save_changes)
|
|
271
382
|
end
|
|
272
|
-
result || self.properties.dup[key]
|
|
383
|
+
result || self.properties.dup[property_key_for(key)]
|
|
273
384
|
end
|
|
274
385
|
# rubocop: enable Metrics/AbcSize
|
|
275
386
|
# rubocop: enable Metrics/CyclomaticComplexity
|
|
@@ -285,23 +396,38 @@ module Inform
|
|
|
285
396
|
result = nil
|
|
286
397
|
if value.object?
|
|
287
398
|
result = self._set_object(key, value) if self.respond_to?(:_set_object)
|
|
399
|
+
elsif key_refers_to_link?(key) && (linked_object = object_for_link_value(value))
|
|
400
|
+
result = set_link_property(key, linked_object)
|
|
288
401
|
elsif value.nil? && self.respond_to?(:_key?) && self._key?(key)
|
|
289
402
|
result = self._unset_object(key) if self.respond_to?(:_unset_object)
|
|
290
403
|
elsif value.nil? && self.properties.key?(key)
|
|
291
404
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
292
|
-
self.properties.delete(key)
|
|
405
|
+
self.properties.delete(property_key_for(key))
|
|
293
406
|
self.save_changes if self.respond_to?(:save_changes)
|
|
294
407
|
else
|
|
295
408
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
296
409
|
self.properties[key] = value
|
|
297
410
|
self.save_changes if self.respond_to?(:save_changes)
|
|
298
411
|
end
|
|
299
|
-
result || self.properties.dup[key]
|
|
412
|
+
result || self.properties.dup[property_key_for(key)]
|
|
300
413
|
end
|
|
301
414
|
# rubocop: enable Metrics/AbcSize
|
|
302
415
|
# rubocop: enable Metrics/CyclomaticComplexity
|
|
303
416
|
# rubocop: enable Metrics/MethodLength
|
|
304
417
|
# rubocop: enable Metrics/PerceivedComplexity
|
|
418
|
+
|
|
419
|
+
def set_link_property(key, object, property_key: property_key_for(key))
|
|
420
|
+
result = if self.respond_to?(:_set_object)
|
|
421
|
+
self._set_object(key, object)
|
|
422
|
+
elsif self.respond_to?(:link)
|
|
423
|
+
self.link(key, object)
|
|
424
|
+
object
|
|
425
|
+
end
|
|
426
|
+
self.properties.delete(property_key) unless property_key.nil?
|
|
427
|
+
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
428
|
+
self.save_changes if self.respond_to?(:save_changes)
|
|
429
|
+
result || object
|
|
430
|
+
end
|
|
305
431
|
end
|
|
306
432
|
# module Prototypical
|
|
307
433
|
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
# lib/story_teller/publication.rb
|
|
1
2
|
# encoding: utf-8
|
|
2
3
|
# frozen_string_literal: false
|
|
3
4
|
|
|
4
|
-
# Copyright Nels Nelson 2008-
|
|
5
|
+
# Copyright Nels Nelson 2008-2026 but freely usable (see license)
|
|
5
6
|
#
|
|
6
7
|
# This file is part of the StoryTeller.
|
|
7
8
|
#
|
data/lib/story_teller/session.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
# lib/story_teller/session.rb
|
|
1
2
|
# encoding: utf-8
|
|
2
3
|
# frozen_string_literal: false
|
|
3
4
|
|
|
4
|
-
# Copyright Nels Nelson 2008-
|
|
5
|
+
# Copyright Nels Nelson 2008-2026 but freely usable (see license)
|
|
5
6
|
#
|
|
6
7
|
# This file is part of StoryTeller.
|
|
7
8
|
#
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
module SessionManagementMethods
|
|
23
24
|
class Registry < Hash; end
|
|
24
25
|
SessionsByChannel = Registry.new
|
|
26
|
+
SessionsByPlayer = Registry.new
|
|
25
27
|
|
|
26
28
|
def add_promiscuous_states(*states)
|
|
27
29
|
Promiscuous.merge states.flatten
|
|
@@ -31,12 +33,52 @@ module SessionManagementMethods
|
|
|
31
33
|
SessionsByChannel
|
|
32
34
|
end
|
|
33
35
|
|
|
36
|
+
def sessions_by_player
|
|
37
|
+
SessionsByPlayer
|
|
38
|
+
end
|
|
39
|
+
|
|
34
40
|
def register(channel, session)
|
|
35
41
|
sessions_by_channel[channel] = session
|
|
42
|
+
session.channel = channel if session.respond_to?(:channel=)
|
|
43
|
+
session
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def register_player(player, session)
|
|
47
|
+
sessions_by_player[player] = session
|
|
48
|
+
session
|
|
36
49
|
end
|
|
37
50
|
|
|
38
51
|
def unregister(channel)
|
|
39
|
-
sessions_by_channel.delete(channel)
|
|
52
|
+
session = sessions_by_channel.delete(channel)
|
|
53
|
+
sessions_by_player.delete_if { |_player, candidate| candidate.equal?(session) }
|
|
54
|
+
session
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def release_player(player, session = nil)
|
|
58
|
+
return nil if player.nil?
|
|
59
|
+
return sessions_by_player.delete(player) if session.nil?
|
|
60
|
+
return sessions_by_player.delete(player) if sessions_by_player[player].equal?(session)
|
|
61
|
+
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def of(obj)
|
|
66
|
+
return nil if obj.nil?
|
|
67
|
+
return obj if obj.is_a?(self)
|
|
68
|
+
|
|
69
|
+
sessions_by_channel[obj] ||
|
|
70
|
+
sessions_by_player[obj] ||
|
|
71
|
+
session_for_method(obj, :player) ||
|
|
72
|
+
session_for_method(obj, :selfobj) ||
|
|
73
|
+
session_for_ivar(obj, :@player)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def players
|
|
77
|
+
sessions_by_player.keys
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def channels
|
|
81
|
+
sessions_by_channel.keys
|
|
40
82
|
end
|
|
41
83
|
|
|
42
84
|
def [](channel)
|
|
@@ -52,7 +94,21 @@ module SessionManagementMethods
|
|
|
52
94
|
end
|
|
53
95
|
|
|
54
96
|
def get(channel)
|
|
55
|
-
sessions_by_channel[channel] ||=
|
|
97
|
+
sessions_by_channel[channel] ||= new(channel)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def session_for_method(obj, method_name)
|
|
103
|
+
return nil unless obj.respond_to?(method_name)
|
|
104
|
+
|
|
105
|
+
sessions_by_player[obj.public_send(method_name)]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def session_for_ivar(obj, ivar_name)
|
|
109
|
+
return nil unless obj.instance_variable_defined?(ivar_name)
|
|
110
|
+
|
|
111
|
+
sessions_by_player[obj.instance_variable_get(ivar_name)]
|
|
56
112
|
end
|
|
57
113
|
end
|
|
58
114
|
# module SessionManagementMethods
|
|
@@ -84,28 +140,43 @@ module SessionStateManagementMethods
|
|
|
84
140
|
end
|
|
85
141
|
end
|
|
86
142
|
|
|
87
|
-
module
|
|
143
|
+
# module StoryTeller
|
|
144
|
+
module StoryTeller
|
|
145
|
+
# module IO
|
|
88
146
|
module IO
|
|
89
147
|
# The Session class
|
|
90
148
|
# TODO: Refactor method implementations into modules
|
|
149
|
+
# rubocop: disable Metrics/ClassLength
|
|
91
150
|
class Session
|
|
92
|
-
# These states accept any input, including no input
|
|
93
|
-
Promiscuous = Set.new
|
|
94
151
|
include SessionStateManagementMethods
|
|
95
152
|
|
|
96
153
|
class << self
|
|
97
154
|
include SessionManagementMethods
|
|
98
155
|
end
|
|
99
|
-
attr_accessor :channel, :state, :status
|
|
156
|
+
attr_accessor :channel, :machine, :player, :state, :status
|
|
100
157
|
attr_reader :last_good_state, :last_activity, :previous, :inbound, :outbound, :settings
|
|
101
158
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
159
|
+
# These states accept any input, including no input
|
|
160
|
+
Promiscuous = Set.new
|
|
161
|
+
|
|
162
|
+
# rubocop: disable Metrics/MethodLength
|
|
163
|
+
def initialize(channel = nil, machine: nil, player: nil, state: :playing, settings: {})
|
|
164
|
+
@channel = channel
|
|
165
|
+
@machine = machine
|
|
166
|
+
@player = nil
|
|
167
|
+
@state = state
|
|
168
|
+
@last_good_state = state
|
|
106
169
|
@session_data = {}
|
|
107
|
-
@settings =
|
|
170
|
+
@settings = settings
|
|
171
|
+
@status = nil
|
|
172
|
+
@last_activity = Time.now
|
|
173
|
+
|
|
174
|
+
self.class.register(channel, self) unless channel.nil?
|
|
175
|
+
control(player) unless player.nil?
|
|
108
176
|
end
|
|
177
|
+
# rubocop: enable Metrics/MethodLength
|
|
178
|
+
|
|
179
|
+
alias preferences settings
|
|
109
180
|
|
|
110
181
|
def init_channel_session(channel)
|
|
111
182
|
Session[channel] = self
|
|
@@ -115,6 +186,13 @@ class Session
|
|
|
115
186
|
@last_activity = Time.now
|
|
116
187
|
end
|
|
117
188
|
|
|
189
|
+
def control(player)
|
|
190
|
+
self.class.release_player(@player, self)
|
|
191
|
+
@player = player
|
|
192
|
+
self.class.register_player(player, self) unless player.nil?
|
|
193
|
+
self
|
|
194
|
+
end
|
|
195
|
+
|
|
118
196
|
def receive(message)
|
|
119
197
|
return if message.nil? # It is okay if the message parameter is an empty string
|
|
120
198
|
return disconnected if @channel.nil?
|
|
@@ -122,7 +200,7 @@ class Session
|
|
|
122
200
|
process(sanitize(message))
|
|
123
201
|
end
|
|
124
202
|
|
|
125
|
-
def update(machine =
|
|
203
|
+
def update(machine = @machine)
|
|
126
204
|
safely_progress(machine)
|
|
127
205
|
after_update if self.respond_to?(:after_update)
|
|
128
206
|
@last_good_state = @state unless @state == :confused
|
|
@@ -140,24 +218,27 @@ class Session
|
|
|
140
218
|
self
|
|
141
219
|
end
|
|
142
220
|
|
|
221
|
+
def expose_to(machine)
|
|
222
|
+
machine.instance_variable_set(:@buffer, @buffer)
|
|
223
|
+
machine.instance_variable_set(:@session, self)
|
|
224
|
+
end
|
|
225
|
+
|
|
143
226
|
ExitCommandPattern = /^(exit|quit|q)$/i.freeze
|
|
144
227
|
|
|
145
228
|
def exit_command?(message)
|
|
146
229
|
!Promiscuous.include?(@state) && ExitCommandPattern.match?(message)
|
|
147
230
|
end
|
|
148
231
|
|
|
149
|
-
def process(message)
|
|
150
|
-
|
|
151
|
-
|
|
232
|
+
def process(message, machine = @machine)
|
|
233
|
+
raise ArgumentError, 'session machine is required' if machine.nil?
|
|
234
|
+
|
|
235
|
+
@machine = machine
|
|
236
|
+
@buffer = sanitize(message)
|
|
152
237
|
@last_activity = Time.now
|
|
153
238
|
@status = :active
|
|
154
|
-
if valid_state?
|
|
155
|
-
update
|
|
156
|
-
else
|
|
157
|
-
@inbound.publish @buffer
|
|
158
|
-
end
|
|
159
239
|
|
|
160
|
-
|
|
240
|
+
expose_to(machine)
|
|
241
|
+
update(machine)
|
|
161
242
|
end
|
|
162
243
|
|
|
163
244
|
def respond
|
|
@@ -165,7 +246,7 @@ class Session
|
|
|
165
246
|
end
|
|
166
247
|
|
|
167
248
|
def safely_progress(machine = self)
|
|
168
|
-
@state = machine.
|
|
249
|
+
@state = machine.public_send(@state)
|
|
169
250
|
raise 'Bad state machine implementation: nil state' if @state.nil?
|
|
170
251
|
rescue StandardError => e
|
|
171
252
|
log.error "Error updating state: #{e.message}", e
|
|
@@ -176,7 +257,7 @@ class Session
|
|
|
176
257
|
def valid_state?(machine = self)
|
|
177
258
|
case @state
|
|
178
259
|
when String, Symbol
|
|
179
|
-
machine.respond_to?
|
|
260
|
+
machine.respond_to?(@state)
|
|
180
261
|
else
|
|
181
262
|
false
|
|
182
263
|
end
|
|
@@ -189,12 +270,21 @@ class Session
|
|
|
189
270
|
prompt
|
|
190
271
|
@last_good_state
|
|
191
272
|
end
|
|
273
|
+
|
|
274
|
+
private
|
|
275
|
+
|
|
276
|
+
def prepare_machine(machine)
|
|
277
|
+
return if machine.equal?(self)
|
|
278
|
+
|
|
279
|
+
machine.instance_variable_set(:@buffer, @buffer)
|
|
280
|
+
end
|
|
192
281
|
end
|
|
193
282
|
# class Session
|
|
283
|
+
# rubocop: enable Metrics/ClassLength
|
|
194
284
|
|
|
195
285
|
# The Connection class
|
|
196
286
|
class Connection; end
|
|
197
287
|
end
|
|
198
288
|
# module IO
|
|
199
289
|
end
|
|
200
|
-
# module
|
|
290
|
+
# module StoryTeller
|