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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -41
  3. data/lib/story_teller/articles.rb +2 -1
  4. data/lib/story_teller/builtins.rb +63 -10
  5. data/lib/story_teller/color.rb +2 -1
  6. data/lib/story_teller/command.rb +2 -1
  7. data/lib/story_teller/context.rb +23 -27
  8. data/lib/story_teller/daemon.rb +2 -2
  9. data/lib/story_teller/engine.rb +45 -10
  10. data/lib/story_teller/events.rb +1 -1
  11. data/lib/story_teller/experimental/handler_dsl.rb +1 -16
  12. data/lib/story_teller/experimental/reverse_engineer_class.rb +1 -1
  13. data/lib/story_teller/grammar_parser.rb +60 -13
  14. data/lib/story_teller/helpers.rb +8 -4
  15. data/lib/story_teller/history.rb +1 -1
  16. data/lib/story_teller/inflector.rb +4 -2
  17. data/lib/story_teller/inform/ephemeral/link.rb +63 -31
  18. data/lib/story_teller/inform/ephemeral/module.rb +6 -5
  19. data/lib/story_teller/inform/ephemeral/object.rb +223 -236
  20. data/lib/story_teller/inform/ephemeral/tag.rb +27 -14
  21. data/lib/story_teller/io.rb +99 -49
  22. data/lib/story_teller/kernel.rb +2 -2
  23. data/lib/story_teller/library/bootstrap.rb +2 -2
  24. data/lib/story_teller/library/declarations.rb +1 -1
  25. data/lib/story_teller/library/directives.rb +1 -1
  26. data/lib/story_teller/library/loader.rb +3 -20
  27. data/lib/story_teller/library/location.rb +9 -3
  28. data/lib/story_teller/library.rb +1 -1
  29. data/lib/story_teller/logging.rb +1 -1
  30. data/lib/story_teller/mixins.rb +11 -4
  31. data/lib/story_teller/plurals.rb +2 -1
  32. data/lib/story_teller/privileges.rb +89 -6
  33. data/lib/story_teller/prototype.rb +150 -32
  34. data/lib/story_teller/publication.rb +2 -1
  35. data/lib/story_teller/session.rb +115 -25
  36. data/lib/story_teller/stdlib.rb +231 -1
  37. data/lib/story_teller/subscription.rb +2 -1
  38. data/lib/story_teller/tree.rb +2 -1
  39. data/lib/story_teller/version.rb +2 -2
  40. data/lib/story_teller/world_tree.rb +21 -23
  41. data/lib/story_teller.rb +18 -5
  42. metadata +6 -10
  43. data/lib/story_teller/core.rb +0 -39
  44. data/lib/story_teller/ephemeral_adapter.rb +0 -40
  45. data/lib/story_teller/inform/base.rb +0 -160
  46. data/lib/story_teller/model_adapter.rb +0 -132
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8342db25700d881e383905faaf8c8d3029511a245b9cc1e4b49aec3ebf6a0149
4
- data.tar.gz: dece3144963ca7775f4b8367047a30a883dbdaefe68df0e689a5f1f9a0482089
3
+ metadata.gz: 1a9737a28a03ce80dd3698d1fc2824b22cb3090d4f0db6c3b52c84134e17a04c
4
+ data.tar.gz: 30ea465986dd84a628280ffa63c83228e04689db657d26c1bf449a15a779165a
5
5
  SHA512:
6
- metadata.gz: 5142ffd42f10634c007b7aa945bd68b1d653331be622bac0b83051ed9e57d64e8bd7d190954ac42c642b7208aad9eb2fa99f20f2355c1d3c2516a493d152dca2
7
- data.tar.gz: ebda0189c7b38d24a1d02719756c7c5e5c7ab0c1384b33c19112636c764ad15e8aab22273fd7cf38afdf28543034a1b85fdc5baf0eeb9bb6aae6c0a387c04ace
6
+ metadata.gz: f0f82b48842ed1cb74515abf3e26d2aeadc1c29004177a556e819ff814c3ce7ee63cf8b4225f5a53a3bfa8102c6bfa2ad0a21ad4b2654518269535c9fc3eca0f
7
+ data.tar.gz: e6122d69fc1a5fbbe2640709f567889a7d70d303740796883bec8ffb3001abd243b91597c02f12a7d31e9aac61de7e08b288cd648c9f784140e935279a2fb085
data/README.md CHANGED
@@ -1,16 +1,14 @@
1
- # StoryTeller
1
+ # StoryTeller Engine
2
2
 
3
3
  [![License](https://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](license-gpl)
4
4
 
5
-
6
-
7
- **StoryTeller** is a runtime layer for use by software programs used to
5
+ **StoryTeller Engine** is a runtime layer for use by software programs used to
8
6
  play interactive-fiction games which are built for the [Inform 6 Ruby Port].
9
7
 
10
- StoryTeller is intended to simply provide the barest minimum facilities
11
- required by the Inform 6 Library Ruby Port software to function in the
12
- context of a game play specified by a game directory or file. It does not
13
- attempt to replicate a byte code interpreter runtime like frotz.
8
+ The StoryTeller Engine is intended to simply provide the barest minimum
9
+ facilities required by the Inform 6 Library Ruby Port software to function
10
+ in the context of a game play specified by a game directory or file. It
11
+ does not attempt to replicate a byte code interpreter runtime like frotz.
14
12
 
15
13
 
16
14
  ## Install mise-en-place
@@ -97,81 +95,75 @@ bundle exec rake test
97
95
  Here is a bird's-eye view of the project layout.
98
96
 
99
97
  ```sh
100
- # date && tree -A -I "game|logs|vendor|tmp|Gemfile.lock"
101
- Sat Dec 13 22:05:24 CST 2025
98
+ # $ date && tree -A -I "coverage|game|logs|vendor|tmp|Gemfile.lock"
99
+ Sun May 10 09:36:13 CDT 2026
102
100
  .
103
- ├── config
104
- │ └── database.yml
105
101
  ├── CONTRIBUTING.md
106
- ├── exe
107
- │ └── inform.rb
108
102
  ├── Gemfile
103
+ ├── inform-runtime-1.2.3.gem
109
104
  ├── inform-runtime.gemspec
110
105
  ├── lib
111
- │ ├── runtime
106
+ │ ├── story_teller
112
107
  │ │ ├── articles.rb
113
108
  │ │ ├── builtins.rb
114
109
  │ │ ├── color.rb
115
110
  │ │ ├── command.rb
116
- │ │ ├── config.rb
117
111
  │ │ ├── context.rb
118
112
  │ │ ├── daemon.rb
119
- │ │ ├── database.rb
113
+ │ │ ├── engine.rb
120
114
  │ │ ├── events.rb
121
115
  │ │ ├── experimental
122
- │ │ │ └── handler_dsl.rb
123
- │ │ ├── game_loader.rb
124
- │ │ ├── game.rb
116
+ │ │ │ ├── handler_dsl.rb
117
+ │ │ │ └── reverse_engineer_class.rb
125
118
  │ │ ├── grammar_parser.rb
126
119
  │ │ ├── helpers.rb
127
120
  │ │ ├── history.rb
128
121
  │ │ ├── inflector.rb
122
+ │ │ ├── inform
123
+ │ │ │ ├── ephemeral
124
+ │ │ │ │ ├── link.rb
125
+ │ │ │ │ ├── module.rb
126
+ │ │ │ │ ├── object.rb
127
+ │ │ │ │ └── tag.rb
128
+ │ │ │ └── models.rb
129
129
  │ │ ├── io.rb
130
130
  │ │ ├── kernel.rb
131
- │ │ ├── library_loader.rb
131
+ │ │ ├── library
132
+ │ │ │ ├── bootstrap.rb
133
+ │ │ │ ├── declarations.rb
134
+ │ │ │ ├── directives.rb
135
+ │ │ │ ├── loader.rb
136
+ │ │ │ └── location.rb
132
137
  │ │ ├── library.rb
133
- │ │ ├── link.rb
134
138
  │ │ ├── logging.rb
135
139
  │ │ ├── mixins.rb
136
- │ │ ├── module.rb
137
- │ │ ├── object.rb
138
- │ │ ├── options.rb
139
- │ │ ├── persistence.rb
140
140
  │ │ ├── plurals.rb
141
+ │ │ ├── privileges.rb
141
142
  │ │ ├── prototype.rb
142
143
  │ │ ├── publication.rb
143
- │ │ ├── runtime.rb
144
144
  │ │ ├── session.rb
145
- │ │ ├── snapshots.rb
146
145
  │ │ ├── stdlib.rb
147
146
  │ │ ├── subscription.rb
148
- │ │ ├── tag.rb
149
147
  │ │ ├── tree.rb
150
148
  │ │ ├── version.rb
151
149
  │ │ └── world_tree.rb
152
- │ └── runtime.rb
150
+ │ └── story_teller.rb
153
151
  ├── LICENSE
154
152
  ├── Rakefile
155
153
  ├── README.md
156
154
  ├── roller2.jpg
157
- ├── scripts
158
- │ ├── backup.rb
159
- │ ├── clone.rb
160
- │ ├── delete.rb
161
- │ ├── init.rb
162
- │ ├── rebuild.rb
163
- │ └── restore.rb
164
155
  └── spec
165
- ├── game_spec.rb
156
+ ├── engine_spec.rb
157
+ ├── inform_ephemeral_spec.rb
166
158
  ├── inform_spec.rb
167
- ├── inform_system_spec.rb
168
- ├── object_spec.rb
169
159
  ├── parser_spec.rb
160
+ ├── prototype_spec.rb
170
161
  ├── spec_helper.rb
162
+ ├── stdlib_spec.rb
171
163
  ├── verblib_spec.rb
172
164
  └── verify_gem_spec.rb
173
165
 
174
- 7 directories, 63 files
166
+ 8 directories, 57 files
175
167
  ```
176
168
 
177
169
  Thanks!
@@ -1,7 +1,8 @@
1
+ # lib/story_teller/articles.rb
1
2
  # encoding: utf-8
2
3
  # frozen_string_literal: false
3
4
 
4
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
5
6
  #
6
7
  # This file is part of the StoryTeller.
7
8
  #
@@ -2,7 +2,7 @@
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: false
4
4
 
5
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
6
  #
7
7
  # This file is part of the StoryTeller.
8
8
  #
@@ -165,25 +165,78 @@ module Builtins
165
165
 
166
166
  def objectloop(*_args, &block)
167
167
  if self.is_a?(InformLibrary)
168
- object_class = StoryTeller::ModelAdapter.object_class
169
- object_class&.all&.each { |o| block.call(o) }
168
+ inform_object_collection.each { |object| block.call(object) }
170
169
  elsif self.respond_to?(:descendants)
171
- self.descendants.each { |o| block.call(o) }
170
+ self.descendants.each { |object| block.call(object) }
172
171
  end
173
172
  end
174
173
 
175
174
  def getobject(id)
176
- object_class = StoryTeller::ModelAdapter.object_class!
177
- o = object_class[id]
178
- o.refresh
175
+ object = inform_object_fetch(id)
176
+ object.refresh if object.respond_to?(:refresh)
177
+ object
179
178
  rescue StandardError
180
179
  raise NoSuchObject
181
180
  end
182
181
 
182
+ # rubocop: disable Metrics/AbcSize
183
+ # rubocop: disable Metrics/CyclomaticComplexity
184
+ # rubocop: disable Metrics/MethodLength
183
185
  def findobject(*args, &block)
184
- object_class = StoryTeller::ModelAdapter.object_class!
185
- return object_class.dataset.order(:name, :id).grep(:name, *args).to_a if args.first.is_a?(Regexp)
186
- object_class.dataset.grep(&block).to_a
186
+ object_class = inform_object_class
187
+
188
+ if sequel_model?(object_class)
189
+ return object_class.dataset.order(:name, :id).grep(:name, *args).to_a if args.first.is_a?(Regexp)
190
+
191
+ return object_class.dataset.grep(&block).to_a
192
+ end
193
+
194
+ objects = inform_object_collection
195
+ if args.first.is_a?(Regexp)
196
+ return objects.select do |object|
197
+ object_name_matches?(object, args.first)
198
+ end
199
+ end
200
+
201
+ block.nil? ? objects : objects.select(&block)
202
+ end
203
+ # rubocop: enable Metrics/AbcSize
204
+ # rubocop: enable Metrics/CyclomaticComplexity
205
+ # rubocop: enable Metrics/MethodLength
206
+
207
+ def inform_object_class
208
+ if defined?(Inform::Object) && sequel_model?(Inform::Object)
209
+ Inform::Object
210
+ elsif defined?(Inform::Ephemeral::Object)
211
+ Inform::Ephemeral::Object
212
+ end
213
+ end
214
+
215
+ def sequel_model?(object_class)
216
+ !object_class.nil? && object_class.respond_to?(:dataset)
217
+ end
218
+
219
+ def inform_object_collection
220
+ object_class = inform_object_class
221
+ return Array::Empty if object_class.nil?
222
+ return object_class.all if object_class.respond_to?(:all)
223
+
224
+ Array::Empty
225
+ end
226
+
227
+ def inform_object_fetch(id)
228
+ object_class = inform_object_class
229
+ raise NoSuchObject if object_class.nil?
230
+
231
+ object = object_class[id] if object_class.respond_to?(:[])
232
+ raise NoSuchObject if object.nil?
233
+
234
+ object
235
+ end
236
+
237
+ def object_name_matches?(object, pattern)
238
+ name = object.name if object.respond_to?(:name)
239
+ Array(name).any? { |part| part.to_s.match?(pattern) } || name.to_s.match?(pattern)
187
240
  end
188
241
 
189
242
  def deadflag; @deadflag; end
@@ -1,7 +1,8 @@
1
+ # lib/story_teller/color.rb
1
2
  # encoding: utf-8
2
3
  # frozen_string_literal: false
3
4
 
4
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
5
6
  #
6
7
  # This file is part of the StoryTeller.
7
8
  #
@@ -1,7 +1,8 @@
1
+ # lib/story_teller/command.rb
1
2
  # encoding: utf-8
2
3
  # frozen_string_literal: false
3
4
 
4
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
5
6
  #
6
7
  # This file is part of the StoryTeller.
7
8
  #
@@ -2,7 +2,7 @@
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: false
4
4
 
5
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
6
  #
7
7
  # This file is part of the StoryTeller.
8
8
  #
@@ -34,42 +34,38 @@ end
34
34
  module Inform
35
35
  # Define Context module
36
36
  module Context
37
- module_function
38
-
39
37
  @definition = nil
40
38
 
41
- def definition
42
- @definition
43
- end
44
-
45
- alias get definition
46
-
47
39
  AttributeFieldReferenceTemplate = '@%<attribute>s'.freeze
40
+ AttributeSetterMethodTemplate = '%<attribute>s='.freeze
48
41
 
49
- def get_value(from, attribute)
50
- return from.send(attribute) if from.respond_to?(attribute)
51
- from.instance_variable_get(format(AttributeFieldReferenceTemplate, attribute: attribute).to_sym)
52
- end
42
+ class << self
43
+ attr_reader :definition
44
+ alias get definition
53
45
 
54
- AttributeSetterMethodTemplate = '%<attribute>s='.freeze
46
+ def get_value(from, attribute)
47
+ return from.send(attribute) if from.respond_to?(attribute)
48
+ from.instance_variable_get(format(AttributeFieldReferenceTemplate, attribute: attribute).to_sym)
49
+ end
55
50
 
56
- def setter_method(attribute)
57
- format(AttributeSetterMethodTemplate, attribute: attribute).to_sym
58
- end
51
+ def setter_method(attribute)
52
+ format(AttributeSetterMethodTemplate, attribute: attribute).to_sym
53
+ end
59
54
 
60
- def each_attribute(obj)
61
- @definition&.members&.each do |attribute|
62
- yield(attribute, get_value(obj, attribute))
55
+ def each_attribute(obj)
56
+ @definition&.members&.each do |attribute|
57
+ yield(attribute, get_value(obj, attribute))
58
+ end
63
59
  end
64
- end
65
60
 
66
- def set(definition)
67
- @definition = definition
68
- @definition&.members&.each do |attribute|
69
- StoryTeller::Command.send(:attr_accessor, attribute) unless StoryTeller::Command.respond_to? attribute
70
- Inform::Event.send(:attr_accessor, attribute) unless Inform::Event.respond_to? attribute
61
+ def set(definition)
62
+ @definition = definition
63
+ @definition&.members&.each do |attribute|
64
+ StoryTeller::Command.send(:attr_accessor, attribute) unless StoryTeller::Command.respond_to? attribute
65
+ Inform::Event.send(:attr_accessor, attribute) unless Inform::Event.respond_to? attribute
66
+ end
67
+ @definition
71
68
  end
72
- @definition
73
69
  end
74
70
  end
75
71
  # module Context
@@ -1,8 +1,8 @@
1
- # src/lib/daemon.rb
1
+ # lib/story_teller/daemon.rb
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: false
4
4
 
5
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
6
  #
7
7
  # This file is part of the StoryTeller.
8
8
  #
@@ -2,7 +2,7 @@
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: false
4
4
 
5
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
6
  #
7
7
  # This file is part of the StoryTeller.
8
8
  #
@@ -33,9 +33,7 @@ module StoryTeller
33
33
  module Library
34
34
  def play
35
35
  log.debug "Using engine-managed InformLibrary play method override"
36
- inform_library = StoryTeller::Engine.library
37
- inform_library.play
38
- inform_library
36
+ StoryTeller::Engine.library.tap(&:play)
39
37
  end
40
38
  end
41
39
 
@@ -45,6 +43,7 @@ module StoryTeller
45
43
  environment: 'default',
46
44
  inform6lib_gem_code_prefix: 'inform',
47
45
  language: 'English',
46
+ persist_player_location: false,
48
47
  properties: 'player actor action action_to_be ' \
49
48
  'results parameters pattern ' \
50
49
  'noun second inp1 inp2 ' \
@@ -61,11 +60,42 @@ module StoryTeller
61
60
  @libraries ||= {}
62
61
  end
63
62
 
64
- def library
65
- selfobj = parser_selfobj
63
+ def persist_player_location?
64
+ return @persist_player_location unless @persist_player_location.nil?
65
+
66
+ default_config.fetch(:persist_player_location, false)
67
+ end
68
+
69
+ def persist_player_location=(value)
70
+ @persist_player_location = value
71
+ end
72
+
73
+ def player_object
74
+ @player_object || parser_selfobj
75
+ end
76
+
77
+ def player_object=(object)
78
+ raise ManagedLibraryError, "player object is nil" if object.nil?
79
+
80
+ @player_object = object
81
+ replace_parser_selfobj(object)
82
+ libraries.clear
83
+ end
84
+
85
+ def library(selfobj = player_object)
66
86
  return libraries[selfobj] if !selfobj.nil? && libraries.key?(selfobj)
67
87
 
68
- bind_managed_library(InformLibrary.new)
88
+ inflib = InformLibrary.new.tap { |inflib| inflib.selfobj = selfobj }
89
+ bind_managed_library(inflib)
90
+ end
91
+
92
+ def replace_parser_selfobj(object)
93
+ return object unless defined?(Inform::Parser)
94
+
95
+ Inform::Parser.send(:remove_const, :SelfObj) if Inform::Parser.const_defined?(:SelfObj, false)
96
+ Inform::Parser.const_set(:SelfObj, object)
97
+
98
+ object
69
99
  end
70
100
 
71
101
  def bind_managed_library(inform_library)
@@ -77,12 +107,14 @@ module StoryTeller
77
107
  libraries[selfobj] = inform_library
78
108
  selfobj.inflib = inform_library if selfobj.respond_to?(:inflib=)
79
109
 
80
- ensure_location(inform_library)
110
+ ensure_player_location(inform_library) if persist_player_location?
81
111
 
82
112
  inform_library
83
113
  end
84
114
 
85
- def ensure_location(inflib)
115
+ # rubocop: disable Metrics/CyclomaticComplexity
116
+ def ensure_player_location(inflib)
117
+ return unless persist_player_location?
86
118
  return if inflib.nil?
87
119
 
88
120
  existing_location = inflib.instance_variable_get(:@location)
@@ -94,6 +126,7 @@ module StoryTeller
94
126
 
95
127
  inflib.instance_variable_set(:@location, location) unless location.nil?
96
128
  end
129
+ # rubocop: enable Metrics/CyclomaticComplexity
97
130
 
98
131
  def parser_selfobj
99
132
  return nil unless defined?(Inform::Parser::SelfObj)
@@ -165,7 +198,9 @@ module StoryTeller
165
198
 
166
199
  def prime_dictionary
167
200
  Inform::Object.select_map(:name).compact.each do |n|
168
- StoryTeller::Dictionary.merge(n.split(/[,\s]+/).grep_v(/[^a-z]/i).map(&:downcase).map(&:to_sym))
201
+ Array(n).join(' ').split(/[,\s]+/).grep_v(/[^a-z]/i).map(&:downcase).map(&:to_sym).each do |word|
202
+ StoryTeller::Dictionary.merge([word])
203
+ end
169
204
  end
170
205
  end
171
206
  end
@@ -2,7 +2,7 @@
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: false
4
4
 
5
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
6
  #
7
7
  # This file is part of StoryTeller.
8
8
  #
@@ -2,7 +2,7 @@
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: false
4
4
 
5
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
6
  #
7
7
  # This file is part of the StoryTeller.
8
8
  #
@@ -21,21 +21,6 @@
21
21
 
22
22
  # The Inform module
23
23
  module Inform
24
- # TODO: Testme
25
- # class Object
26
- # def after_initialize
27
- # super
28
- # return if new?
29
- # Inform::Behavior.attach(self)
30
- # apply_instance_behavior!
31
- # end
32
-
33
- # def after_refresh
34
- # super
35
- # apply_instance_behavior!
36
- # end
37
- # end
38
-
39
24
  # The Inform::Behavior module
40
25
  module Behavior
41
26
  class Registry < Hash; end
@@ -2,7 +2,7 @@
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: false
4
4
 
5
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
6
6
  #
7
7
  # This file is part of StoryTeller.
8
8
  #
@@ -1,7 +1,8 @@
1
+ # lib/story_teller/grammar_parser.rb
1
2
  # encoding: utf-8
2
3
  # frozen_string_literal: false
3
4
 
4
- # Copyright Nels Nelson 2008-2025 but freely usable (see license)
5
+ # Copyright Nels Nelson 2008-2026 but freely usable (see license)
5
6
  #
6
7
  # This file is part of StoryTeller.
7
8
  #
@@ -90,7 +91,12 @@ module Inform
90
91
  EmoteString = 'emote'.freeze
91
92
  Newline = "\n".freeze
92
93
 
94
+ # rubocop: disable Metrics/AbcSize
95
+ # rubocop: disable Metrics/MethodLength
93
96
  def to_s
97
+ grammar_lines = grammars.map(&:to_s)
98
+ grammar_lines[-1] = "#{grammar_lines[-1]};" if grammar_lines.any?
99
+
94
100
  VerbString + SpaceString +
95
101
  if meta?
96
102
  MetaString + SpaceString
@@ -100,8 +106,10 @@ module Inform
100
106
  ''
101
107
  end +
102
108
  map { |verb| "'#{verb}'" }.join(SpaceString) + Newline +
103
- grammars.join(Newline)
109
+ grammar_lines.join(Newline)
104
110
  end
111
+ # rubocop: enable Metrics/AbcSize
112
+ # rubocop: enable Metrics/MethodLength
105
113
  alias to_str to_s
106
114
 
107
115
  def hash
@@ -115,15 +123,22 @@ module Inform
115
123
  CustomScopePattern = %r{^(.*)=(.*)$}.freeze
116
124
  attr_reader :source, :action, :expected_tokens
117
125
 
126
+ # rubocop: disable Metrics/MethodLength
118
127
  def initialize(grammar, source, action, reverse = nil)
119
128
  super()
120
- concat(symbolize_grammar(grammar)) unless grammar.empty?
129
+ unless grammar.empty?
130
+ symbolized_grammar = symbolize_grammar(grammar)
131
+ concat(symbolized_grammar) unless symbolized_grammar.empty?
132
+ end
121
133
  push(:end)
122
134
  @action = action
123
- @expected_tokens = grammar.map { |a| CustomScopePattern.match?(a) ? a.to_s.split('=').first.to_sym : a }
135
+ @expected_tokens = symbolized_grammar&.map do |a|
136
+ CustomScopePattern.match?(a) ? a.to_s.split('=').first.to_sym : a
137
+ end
124
138
  @reverse = reverse.nil? ? false : reverse
125
139
  @source = source
126
140
  end
141
+ # rubocop: enable Metrics/MethodLength
127
142
 
128
143
  def empty?
129
144
  length == (include?(:end) ? 1 : 0)
@@ -261,35 +276,64 @@ module Inform
261
276
  BuilderPattern = %r{builder}.freeze
262
277
  Profanity = %w[shit damn fuck sod].freeze
263
278
  ValidModes = %i[admin builder player].freeze
264
- ModePatterns = ValidModes.each_with_object({}) { |key, memo| memo[key] = Regexp.new(key.to_s) }
279
+ ModePatterns = ValidModes.each_with_object({}) do |key, memo|
280
+ memo[key] = Regexp.new(key.to_s)
281
+ end
282
+
283
+ # TODO: Remove (2026-04-30) after confirming replacement below
284
+ # works better.
285
+ # def lookup(key, actor = nil, grammar_index = self)
286
+ # return nil if key.nil? || !grammar_index.include?(key)
287
+ # verb = grammar_index[key].dup
288
+ # return verb if actor.nil?
289
+ # return nil if guarded?(verb, actor)
290
+ # filter_by_permission(verb, actor)
291
+ # verb
292
+ # end
265
293
 
266
294
  def lookup(key, actor = nil, grammar_index = self)
267
295
  return nil if key.nil? || !grammar_index.include?(key)
296
+
268
297
  verb = grammar_index[key].dup
269
298
  return verb if actor.nil?
270
- return nil if guarded?(verb, actor)
299
+
271
300
  filter_by_permission(verb, actor)
301
+ return nil if verb.grammars.empty?
302
+
272
303
  verb
273
304
  end
274
305
 
275
306
  def all(actor = nil, grammar_index = self)
276
307
  return grammar_index.map(&:keys).flatten.sort if actor.nil?
277
308
  mode = role(actor)
278
- init_all_verbs_by_mode(mode, grammar_index, actor) unless Inform::Grammar.partitions.include?(mode)
309
+ unless Inform::Grammar.partitions.include?(mode)
310
+ init_all_verbs_by_mode(mode, grammar_index, actor)
311
+ end
279
312
  Inform::Grammar.partitions[mode]
280
313
  end
281
314
 
282
315
  def by_mode(mode = nil, grammar_index = self)
283
316
  mode = validate(mode)
284
- init_verbs_filtered_by_mode(mode, grammar_index) unless Inform::Grammar.selections.include?(mode)
317
+ unless Inform::Grammar.selections.include?(mode)
318
+ init_verbs_filtered_by_mode(mode, grammar_index)
319
+ end
285
320
  Inform::Grammar.selections[mode]
286
321
  end
287
322
 
288
323
  private
289
324
 
325
+ # TODO: Remove (2026-04-30) after confirming replacement below
326
+ # works better.
327
+ # def init_all_verbs_by_mode(mode, grammar_index, actor, set = verbs_set_instance)
328
+ # verbs = grammar_index.each_with_object(set) do |(key, verb), memo|
329
+ # memo.add(key) unless Profanity.include?(key) || guarded?(verb, actor)
330
+ # end
331
+ # Inform::Grammar.partitions[mode] = verbs.to_a
332
+ # end
333
+
290
334
  def init_all_verbs_by_mode(mode, grammar_index, actor, set = verbs_set_instance)
291
335
  verbs = grammar_index.each_with_object(set) do |(key, verb), memo|
292
- memo.add(key) unless Profanity.include?(key) || guarded?(verb, actor)
336
+ memo.add(key) unless Profanity.include?(key) || !visible_to?(verb, actor)
293
337
  end
294
338
  Inform::Grammar.partitions[mode] = verbs.to_a
295
339
  end
@@ -312,13 +356,16 @@ module Inform
312
356
  end
313
357
 
314
358
  def guarded?(resource, obj)
315
- (AdminPattern.match?(resource.source) && !obj.admin?) ||
316
- (BuilderPattern.match?(resource.source) && !obj.builder?)
359
+ (AdminPattern.match?(resource.source) &&
360
+ !StoryTeller::Privileges.privileged?(obj, :admin)) ||
361
+ (BuilderPattern.match?(resource.source) &&
362
+ !StoryTeller::Privileges.privileged?(obj, :builder))
317
363
  end
318
364
 
319
365
  def role(obj)
320
- return :builder if obj.builder?
321
- return :admin if obj.admin?
366
+ return :admin if StoryTeller::Privileges.privileged?(obj, :admin)
367
+ return :builder if StoryTeller::Privileges.privileged?(obj, :builder)
368
+
322
369
  :player
323
370
  end
324
371