inform-runtime 1.0.4

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +623 -0
  3. data/README.md +185 -0
  4. data/Rakefile +65 -0
  5. data/config/database.yml +37 -0
  6. data/exe/inform.rb +6 -0
  7. data/game/config.yml +5 -0
  8. data/game/example.inf +76 -0
  9. data/game/example.rb +90 -0
  10. data/game/forms/example_form.rb +2 -0
  11. data/game/grammar/game_grammar.inf.rb +11 -0
  12. data/game/languages/english.rb +2 -0
  13. data/game/models/example_model.rb +2 -0
  14. data/game/modules/example_module.rb +9 -0
  15. data/game/rules/example_state.rb +2 -0
  16. data/game/scripts/example_script.rb +2 -0
  17. data/game/topics/example_topic.rb +2 -0
  18. data/game/verbs/game_verbs.rb +15 -0
  19. data/game/verbs/metaverbs.rb +2028 -0
  20. data/lib/runtime/articles.rb +138 -0
  21. data/lib/runtime/builtins.rb +359 -0
  22. data/lib/runtime/color.rb +145 -0
  23. data/lib/runtime/command.rb +470 -0
  24. data/lib/runtime/config.rb +48 -0
  25. data/lib/runtime/context.rb +78 -0
  26. data/lib/runtime/daemon.rb +266 -0
  27. data/lib/runtime/database.rb +500 -0
  28. data/lib/runtime/events.rb +771 -0
  29. data/lib/runtime/experimental/handler_dsl.rb +175 -0
  30. data/lib/runtime/game.rb +74 -0
  31. data/lib/runtime/game_loader.rb +132 -0
  32. data/lib/runtime/grammar_parser.rb +553 -0
  33. data/lib/runtime/helpers.rb +177 -0
  34. data/lib/runtime/history.rb +45 -0
  35. data/lib/runtime/inflector.rb +195 -0
  36. data/lib/runtime/io.rb +174 -0
  37. data/lib/runtime/kernel.rb +450 -0
  38. data/lib/runtime/library.rb +59 -0
  39. data/lib/runtime/library_loader.rb +135 -0
  40. data/lib/runtime/link.rb +158 -0
  41. data/lib/runtime/logging.rb +197 -0
  42. data/lib/runtime/mixins.rb +570 -0
  43. data/lib/runtime/module.rb +202 -0
  44. data/lib/runtime/object.rb +761 -0
  45. data/lib/runtime/options.rb +104 -0
  46. data/lib/runtime/persistence.rb +292 -0
  47. data/lib/runtime/plurals.rb +60 -0
  48. data/lib/runtime/prototype.rb +307 -0
  49. data/lib/runtime/publication.rb +92 -0
  50. data/lib/runtime/runtime.rb +321 -0
  51. data/lib/runtime/session.rb +202 -0
  52. data/lib/runtime/stdlib.rb +604 -0
  53. data/lib/runtime/subscription.rb +47 -0
  54. data/lib/runtime/tag.rb +287 -0
  55. data/lib/runtime/tree.rb +204 -0
  56. data/lib/runtime/version.rb +24 -0
  57. data/lib/runtime/world_tree.rb +69 -0
  58. data/lib/runtime.rb +35 -0
  59. metadata +199 -0
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # Copyright Nels Nelson 2008-2023 but freely usable (see license)
5
+ #
6
+ # This file is part of the Inform Runtime.
7
+ #
8
+ # The Inform Runtime is free software: you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License as published
10
+ # by the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # The Inform Runtime is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
20
+
21
+ require 'logger'
22
+ require 'optparse'
23
+
24
+ require_relative 'version'
25
+
26
+ # The Inform module
27
+ module Inform
28
+ # The Options module
29
+ module Options
30
+ # The ArgumentsParser
31
+ class ArgumentsParser
32
+ attr_reader :parser, :options
33
+
34
+ def initialize(args, option_parser = OptionParser.new, **params)
35
+ @parser = option_parser
36
+ @options = ::Inform::Runtime.default_config.dup
37
+ flags.each { |method_name| self.method(method_name).call }
38
+ @parser.parse!(args, **params)
39
+ end
40
+
41
+ def flags
42
+ @flags ||= %i[banner admin log_level help version]
43
+ end
44
+
45
+ def banner
46
+ @parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [game_path] [options]"
47
+ @parser.separator ''
48
+ @parser.separator 'Arguments:'
49
+ @parser.separator ' game_path Path to game file or directory'
50
+ @parser.separator ''
51
+ @parser.separator 'Options:'
52
+ end
53
+
54
+ def admin
55
+ @parser.on_tail('--admin', 'Set player character as admin; default: false') do
56
+ @options[:admin] = true
57
+ end
58
+ end
59
+
60
+ def log_level
61
+ @parser.on_tail('-v', '--verbose', 'Increase verbosity') do
62
+ @options[:log_level] ||= Logger::INFO
63
+ @options[:log_level] -= 1
64
+ end
65
+ end
66
+
67
+ def help
68
+ @parser.on_tail('-?', '--help', 'Show this message') do
69
+ puts @parser
70
+ exit
71
+ end
72
+ end
73
+
74
+ def version
75
+ @parser.on_tail('--version', 'Show version') do
76
+ puts "#{$PROGRAM_NAME} version #{Inform::VERSION}"
77
+ exit
78
+ end
79
+ end
80
+ end
81
+ # class ArgumentsParser
82
+
83
+ def demand(options, arg, positional = false)
84
+ return options[arg] unless options[arg].nil?
85
+ required_arg = positional ? "<#{arg}>" : "--#{arg.to_s.gsub(/_/, '-')}"
86
+ raise UserError, "Required argument: #{required_arg}"
87
+ end
88
+
89
+ def parse_arguments(args = ARGV, _file_path = ARGF)
90
+ arguments_parser = ArgumentsParser.new(args)
91
+ demand(arguments_parser.options, :game_path)
92
+ arguments_parser.options
93
+ rescue OptionParser::InvalidArgument, OptionParser::InvalidOption,
94
+ OptionParser::MissingArgument, OptionParser::NeedlessArgument => e
95
+ puts e.message
96
+ puts arguments_parser.parser
97
+ exit
98
+ rescue OptionParser::AmbiguousOption => e
99
+ abort e.message
100
+ end
101
+ end
102
+ # module Options
103
+ end
104
+ # module Inform
@@ -0,0 +1,292 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # Copyright Nels Nelson 2008-2023 but freely usable (see license)
5
+ #
6
+ # This file is part of the Inform Runtime.
7
+ #
8
+ # The Inform Runtime is free software: you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License as published
10
+ # by the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # The Inform Runtime is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
20
+
21
+ require 'etc'
22
+ require 'logger'
23
+ require 'uri'
24
+ require 'yaml'
25
+
26
+ require 'sequel'
27
+
28
+ require_relative 'database'
29
+
30
+ Sequel.extension :migration
31
+ Sequel.extension :connection_validator
32
+
33
+ # Re-open the Inform module to define a helper method
34
+ module Inform
35
+ def self.initialize_persistence_layer
36
+ log.debug "Inform.initialize_persistence_layer"
37
+ Inform::Persistence.init
38
+ end
39
+ end
40
+
41
+ # Simplistic in-memory cache
42
+ class EphemeralCache < Hash
43
+ def get(key)
44
+ self[key]
45
+ end
46
+ def set(key, value, _ttl)
47
+ self[key] = value
48
+ end
49
+ end
50
+
51
+ # The Persistence class
52
+ class Persistence
53
+ unless defined?(Persistence::GlobalCache)
54
+ GlobalCache = defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : EphemeralCache.new
55
+ end
56
+ Plugins = {
57
+ after_initialize: [],
58
+ caching: [Persistence::GlobalCache],
59
+ json_serializer: [],
60
+ xml_serializer: []
61
+ # tactical_eager_loading: [],
62
+ # eager_each: []
63
+ }.freeze
64
+ end
65
+
66
+ # The SequelLoggers module
67
+ module SequelLoggers
68
+ DatabaseLogging = Struct.new(:memo).new({})
69
+
70
+ def add_logger(database, logger)
71
+ database.loggers << logger unless database.loggers.include?(logger)
72
+ end
73
+
74
+ def delete_logger(database, logger)
75
+ database.loggers.delete(logger) if database.loggers.include?(logger)
76
+ end
77
+
78
+ def set_log_level(database, level)
79
+ DatabaseLogging.memo[:preserved_log_levels][database] = database.sql_log_level
80
+ database.sql_log_level = level unless level.nil?
81
+ end
82
+
83
+ def enable_query_logging
84
+ DatabaseLogging.memo[:logger] ||= Logger.new($stdout)
85
+ DatabaseLogging.memo[:preserved_log_levels] ||= {}
86
+ logger = DatabaseLogging.memo[:logger]
87
+ Sequel::DATABASES.each do |database|
88
+ add_logger(database, logger)
89
+ set_log_level(database, :debug)
90
+ logger.debug "Enabled query logging for #{database}"
91
+ end
92
+ end
93
+
94
+ def disable_query_logging
95
+ logger = DatabaseLogging.memo[:logger]
96
+ Sequel::DATABASES.each do |database|
97
+ logger.debug "Disabling query logging for #{database}"
98
+ delete_logger(database, logger)
99
+ set_log_level(database, DatabaseLogging.memo[:preserved_log_levels][database])
100
+ end
101
+ end
102
+ end
103
+ # module SequelLoggers
104
+
105
+ # The SequelPlugins module
106
+ module SequelPlugins
107
+ def enable_plugins
108
+ Persistence::Plugins.each { |plugin, parameters| enable_plugin(plugin, parameters) }
109
+ end
110
+
111
+ def enable_plugin(plugin, parameters = [])
112
+ return Sequel::Model.plugin(plugin) if parameters.empty?
113
+ Sequel::Model.plugin(plugin, *parameters)
114
+ rescue LoadError => e
115
+ log.error e.message
116
+ end
117
+ end
118
+ # module SequelPlugins
119
+
120
+ # The Inform module
121
+ module Inform
122
+ # The Persistence class
123
+ class Persistence
124
+ include DatabaseConnectionHelpers
125
+ include SequelLoggers
126
+ include SequelPlugins
127
+
128
+ attr_reader :config, :environment
129
+
130
+ def self.init(env = nil)
131
+ Inform::Persistence.instance(env)
132
+ end
133
+
134
+ @instance_mutex = Mutex.new
135
+
136
+ def self.instance(*args)
137
+ return @instance unless @instance.nil?
138
+ @instance_mutex.synchronize do
139
+ @instance ||= new(*args)
140
+ end
141
+ @instance
142
+ end
143
+
144
+ private_class_method :new
145
+
146
+ def initialize(env = nil)
147
+ @environment = env || Inform::Game.environment
148
+ log.debug "Initializing persistence layer for environment: #{@environment}"
149
+ caller[0..4].each { |t| log.debug t }
150
+ @config = database_config.fetch(@environment.to_s, {})
151
+ establish_database_connection
152
+ enable_plugins
153
+ enable_query_logging if ENV['ENABLE_SQL_LOGGING']
154
+ end
155
+
156
+ def establish_database_connection
157
+ connect
158
+ rescue Sequel::DatabaseConnectionError
159
+ log.warn "Database requires initialization"
160
+ init_database
161
+ retry if ConnectionAttempts.memo < 2
162
+ end
163
+
164
+ def database_config
165
+ @database_config ||= YAML.load_file(database_config_file_path)
166
+ end
167
+
168
+ def database_config_file_path
169
+ @database_config_file_path ||= File.expand_path(
170
+ File.join(Inform::Runtime.project_dir_path, 'config', 'database.yml')
171
+ )
172
+ end
173
+
174
+ def init_database
175
+ @config = database_config.fetch('default')
176
+ connect
177
+ @database = Inform::Database.instance(Inform::Game.config[:database_name])
178
+ @database.bootstrap.up
179
+ @config = database_config.fetch(environment.to_s)
180
+ connect
181
+ end
182
+ end
183
+ # class Persistence
184
+ end
185
+ # module Inform
186
+
187
+ # The Inform module
188
+ module Inform
189
+ # The ImplicitMigration module
190
+ module ImplicitMigration
191
+ NoDatabasePattern = %r{No database associated with Sequel::Model}.freeze
192
+ MigrationSetupTemplate = '%<model>sSetup'.freeze
193
+ ModuleNamespaceDelimiterPattern = /::/.freeze
194
+
195
+ def before_inherited(subclass)
196
+ return if self != Sequel::Model
197
+ descendants << subclass
198
+ log.debug "#{subclass} << #{self} [#{descendants}]"
199
+ maybe_migrate(subclass)
200
+ end
201
+
202
+ def after_inherited(_subclass)
203
+ examine_schema
204
+ end
205
+
206
+ # rubocop: disable Metrics/AbcSize
207
+ # rubocop: disable Metrics/MethodLength
208
+ def examine_schema
209
+ descendants.each do |model_class|
210
+ table_name = model_class.table_name
211
+ indexes = self.db.indexes(table_name)
212
+ columns = model_class.columns
213
+ associations = model_class.associations
214
+
215
+ log.debug "Table: #{table_name}"
216
+ log.debug "Columns: #{columns.join(', ')}"
217
+ log.debug "Indexes: #{indexes}"
218
+
219
+ associations.each do |assoc_name, assoc_data|
220
+ log.debug "Association: #{assoc_name} (#{assoc_data[:type]}) to #{assoc_data[:class_name]}"
221
+ end
222
+
223
+ log.debug "==========="
224
+ end
225
+ end
226
+ # rubocop: enable Metrics/AbcSize
227
+ # rubocop: enable Metrics/MethodLength
228
+
229
+ def maybe_migrate(subclass)
230
+ migration_class = migration(subclass)
231
+ log.debug "Found migration class: #{migration_class}"
232
+ migration_class&.up
233
+ rescue Sequel::Error => e
234
+ if NoDatabasePattern.match?(e.message)
235
+ Inform::Persistence.instance.connect
236
+ retry
237
+ end
238
+ end
239
+
240
+ def migration(model)
241
+ names = format(MigrationSetupTemplate, model: model).split(ModuleNamespaceDelimiterPattern)
242
+ names.inject(Object) do |mod, class_name|
243
+ mod.const_get(class_name)
244
+ rescue StandardError => e
245
+ log.warn "Error getting reference to migration model class: #{e.message}"
246
+ next
247
+ end
248
+ end
249
+ end
250
+ # module ImplicitMigration
251
+ end
252
+ # module Inform
253
+
254
+ # The Inform module
255
+ module Inform
256
+ # The InheritanceListener module
257
+ module InheritanceListener
258
+ # The ClassMethods module
259
+ module ClassMethods
260
+ include Inform::ImplicitMigration
261
+
262
+ def inherited(subclass)
263
+ before_inherited(subclass)
264
+ super
265
+ after_inherited(subclass)
266
+ end
267
+
268
+ # Returns the list of Model descendants.
269
+ def descendants
270
+ @descendants ||= []
271
+ end
272
+ end
273
+
274
+ def self.included(base)
275
+ base.extend(ClassMethods)
276
+ end
277
+ end
278
+ # module InheritanceListener
279
+ end
280
+ # module Inform
281
+
282
+ # The Sequel module
283
+ module Sequel
284
+ # The Sequel::Model class
285
+ class Model
286
+ include Inform::InheritanceListener
287
+
288
+ def self.implicit_table_name
289
+ underscore(demodulize(name)).to_sym
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,60 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # Copyright Nels Nelson 2008-2023 but freely usable (see license)
5
+ #
6
+ # This file is part of the Inform Runtime.
7
+ #
8
+ # The Inform Runtime is free software: you can redistribute it and/or
9
+ # modify it under the terms of the GNU General Public License as published
10
+ # by the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # The Inform Runtime is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
20
+
21
+ require_relative 'inflector'
22
+
23
+ # The Inform module
24
+ module Inform
25
+ # The Plurals module
26
+ module Plurals
27
+ # Returns the plural form of the word in the string.
28
+ def pluralize(word, n = nil)
29
+ return word if n == 1
30
+ apply_inflections(word, Inform::Inflector.inflections.plurals)
31
+ end
32
+
33
+ # The reverse of +pluralize+, returns the singular form of a word in a
34
+ # string.
35
+ def singularize(word)
36
+ apply_inflections(word, Inform::Inflector.inflections.singulars)
37
+ end
38
+
39
+ def singular?(s)
40
+ singularize(s) == s
41
+ end
42
+
43
+ def plural?(s)
44
+ pluralize(s) == s
45
+ end
46
+
47
+ # Applies inflection rules for +singularize+ and +pluralize+.
48
+ def apply_inflections(word, rules)
49
+ return "" if word.empty?
50
+ result = word.to_s.dup
51
+ return result if Inform::Inflector.inflections.uncountables.include?(result.downcase[/\b\w+\Z/])
52
+ for (rule, replacement) in rules
53
+ break if result.sub!(rule, replacement)
54
+ end
55
+ result
56
+ end
57
+ end
58
+ # module Plurals
59
+ end
60
+ # module Inform