inform-runtime 1.2.3 → 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 +2 -1
- data/lib/story_teller/context.rb +23 -27
- data/lib/story_teller/daemon.rb +2 -2
- data/lib/story_teller/engine.rb +45 -10
- data/lib/story_teller/events.rb +1 -1
- 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 -4
- data/lib/story_teller/history.rb +1 -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 +2 -2
- data/lib/story_teller/library/bootstrap.rb +2 -2
- 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 +3 -20
- data/lib/story_teller/library/location.rb +9 -3
- data/lib/story_teller/library.rb +1 -1
- data/lib/story_teller/logging.rb +1 -1
- data/lib/story_teller/mixins.rb +11 -4
- data/lib/story_teller/plurals.rb +2 -1
- data/lib/story_teller/privileges.rb +89 -6
- data/lib/story_teller/prototype.rb +150 -32
- data/lib/story_teller/publication.rb +2 -1
- data/lib/story_teller/session.rb +115 -25
- data/lib/story_teller/stdlib.rb +231 -1
- 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 +6 -10
- data/lib/story_teller/core.rb +0 -39
- 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,13 +173,13 @@ 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)
|
|
@@ -170,14 +191,13 @@ module Inform
|
|
|
170
191
|
# StoryTeller::Library.declarations.properties.key?(property)
|
|
171
192
|
# self.prototype(method, *args, &block)
|
|
172
193
|
else
|
|
173
|
-
|
|
194
|
+
raise NoMethodError, "undefined method `#{method}' for #{self}"
|
|
174
195
|
end
|
|
175
196
|
end
|
|
176
197
|
# rubocop: enable Metrics/AbcSize
|
|
177
198
|
# rubocop: enable Metrics/CyclomaticComplexity
|
|
178
199
|
# rubocop: enable Metrics/MethodLength
|
|
179
200
|
# rubocop: enable Metrics/PerceivedComplexity
|
|
180
|
-
# rubocop: enable Style/MissingRespondToMissing
|
|
181
201
|
|
|
182
202
|
# TODO: Remove (2026-04-28)
|
|
183
203
|
# def respond_to_missing?(method, include_all=false)
|
|
@@ -206,32 +226,81 @@ module Inform
|
|
|
206
226
|
).freeze
|
|
207
227
|
|
|
208
228
|
# rubocop: disable Metrics/AbcSize
|
|
209
|
-
# rubocop: disable Metrics/CyclomaticComplexity
|
|
210
229
|
# rubocop: disable Metrics/MethodLength
|
|
211
230
|
def prototype(key, *args, &block)
|
|
212
231
|
ensure_properties_attribute!
|
|
213
232
|
accessor, writer = accessor_and_writer(key)
|
|
233
|
+
link_result = nil
|
|
234
|
+
|
|
214
235
|
if key_refers_to_link?(key)
|
|
215
236
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
247
|
end
|
|
223
|
-
self.properties[key] = args.first
|
|
224
248
|
end
|
|
249
|
+
|
|
225
250
|
accessor = format(AccessorMethodTemplate, accessor: accessor)
|
|
226
251
|
self.instance_eval(accessor, __FILE__, __LINE__)
|
|
227
252
|
writer = format(WriterMethodTemplate, writer: writer)
|
|
228
253
|
self.instance_eval(writer, __FILE__, __LINE__)
|
|
254
|
+
|
|
255
|
+
return link_result unless link_result.nil?
|
|
256
|
+
|
|
229
257
|
self.send(key, *args, &block)
|
|
230
258
|
end
|
|
231
259
|
# rubocop: enable Metrics/AbcSize
|
|
232
|
-
# rubocop: enable Metrics/CyclomaticComplexity
|
|
233
260
|
# rubocop: enable Metrics/MethodLength
|
|
234
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
|
+
|
|
235
304
|
MISSING_PROPERTIES_MESSAGE = 'Missing properties attribute for: ' \
|
|
236
305
|
'%<identity>s'.freeze
|
|
237
306
|
|
|
@@ -240,14 +309,46 @@ module Inform
|
|
|
240
309
|
raise StandardError, format(MISSING_PROPERTIES_MESSAGE, identity: self.identity)
|
|
241
310
|
end
|
|
242
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
|
+
|
|
243
318
|
def key_refers_to_link?(key)
|
|
244
|
-
|
|
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
|
|
245
344
|
end
|
|
246
345
|
|
|
247
346
|
def _get(key)
|
|
248
347
|
result = nil
|
|
249
348
|
result = self._get_object(key) if self.respond_to?(:_key?) && self._key?(key)
|
|
250
|
-
result
|
|
349
|
+
return result unless result.nil?
|
|
350
|
+
|
|
351
|
+
resolve_link_property(key) || self.properties.dup[property_key_for(key)]
|
|
251
352
|
end
|
|
252
353
|
|
|
253
354
|
# rubocop: disable Metrics/AbcSize
|
|
@@ -260,11 +361,13 @@ module Inform
|
|
|
260
361
|
result = nil
|
|
261
362
|
if values.first.object?
|
|
262
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)
|
|
263
366
|
elsif values.first.nil? && self.respond_to?(:_key?) && self._key?(key)
|
|
264
367
|
result = self._unset_object(key) if self.respond_to?(:_unset_object)
|
|
265
368
|
elsif values.first.nil?
|
|
266
369
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
267
|
-
self.properties.delete(key)
|
|
370
|
+
self.properties.delete(property_key_for(key))
|
|
268
371
|
self.save_changes if self.respond_to?(:save_changes)
|
|
269
372
|
elsif values.length > 1
|
|
270
373
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
@@ -277,7 +380,7 @@ module Inform
|
|
|
277
380
|
self.properties[key] = values.first
|
|
278
381
|
self.save_changes if self.respond_to?(:save_changes)
|
|
279
382
|
end
|
|
280
|
-
result || self.properties.dup[key]
|
|
383
|
+
result || self.properties.dup[property_key_for(key)]
|
|
281
384
|
end
|
|
282
385
|
# rubocop: enable Metrics/AbcSize
|
|
283
386
|
# rubocop: enable Metrics/CyclomaticComplexity
|
|
@@ -293,23 +396,38 @@ module Inform
|
|
|
293
396
|
result = nil
|
|
294
397
|
if value.object?
|
|
295
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)
|
|
296
401
|
elsif value.nil? && self.respond_to?(:_key?) && self._key?(key)
|
|
297
402
|
result = self._unset_object(key) if self.respond_to?(:_unset_object)
|
|
298
403
|
elsif value.nil? && self.properties.key?(key)
|
|
299
404
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
300
|
-
self.properties.delete(key)
|
|
405
|
+
self.properties.delete(property_key_for(key))
|
|
301
406
|
self.save_changes if self.respond_to?(:save_changes)
|
|
302
407
|
else
|
|
303
408
|
self.will_change_column(:properties) if self.respond_to?(:will_change_column)
|
|
304
409
|
self.properties[key] = value
|
|
305
410
|
self.save_changes if self.respond_to?(:save_changes)
|
|
306
411
|
end
|
|
307
|
-
result || self.properties.dup[key]
|
|
412
|
+
result || self.properties.dup[property_key_for(key)]
|
|
308
413
|
end
|
|
309
414
|
# rubocop: enable Metrics/AbcSize
|
|
310
415
|
# rubocop: enable Metrics/CyclomaticComplexity
|
|
311
416
|
# rubocop: enable Metrics/MethodLength
|
|
312
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
|
|
313
431
|
end
|
|
314
432
|
# module Prototypical
|
|
315
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
|