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,175 @@
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
+ # The Inform module
22
+ module Inform
23
+ # TODO: Testme
24
+ # class Object
25
+ # def after_initialize
26
+ # super
27
+ # return if new?
28
+ # Inform::Behavior.attach(self)
29
+ # apply_instance_behavior!
30
+ # end
31
+
32
+ # def after_refresh
33
+ # super
34
+ # apply_instance_behavior!
35
+ # end
36
+ # end
37
+
38
+ # The Inform::Behavior module
39
+ module Behavior
40
+ Registry = Class.new(Hash)
41
+ REGISTRY = Registry.new
42
+
43
+ def self.key_for(obj)
44
+ return obj.identity if obj.respond_to?(:identity) &&
45
+ !(obj.identity.nil? || obj.identity.empty?)
46
+ "<##{obj.class}:#{obj.name}>"
47
+ end
48
+
49
+ def self.register(obj, mod)
50
+ Inform::Behavior::REGISTRY[key_for(obj)] = mod
51
+ end
52
+
53
+ def self.attach(obj)
54
+ if (mod = Inform::Behavior::REGISTRY[key_for(obj)])
55
+ obj.extend(mod)
56
+ end
57
+ obj
58
+ end
59
+ end
60
+
61
+ # The BehaviorError class
62
+ class BehaviorError < TypeError
63
+ MESSAGE = 'Behavior registry for %<key>s is not a Module (got %<clazz>s)'.freeze
64
+ def initialize(key, behavior)
65
+ super(format(MESSAGE, key: key, clazz: behavior.class))
66
+ end
67
+ end
68
+
69
+ # rubocop: disable Metrics/AbcSize
70
+ # rubocop: disable Metrics/MethodLength
71
+ def with(context = nil, key = Inform::Behavior.key_for(self), &block)
72
+ log.debug "with.self=#{self.inspect} (#{self.identity})"
73
+ behavior = Inform::Behavior::REGISTRY[key] ||= ::Module.new
74
+ ensure_behavior_module!(key, behavior)
75
+
76
+ unless block.nil?
77
+ proxy = context_proxy(context || self, block.binding)
78
+ b = builder(self, behavior, proxy)
79
+ klass = b.class
80
+ before = klass.instance_methods(false)
81
+
82
+ b.instance_exec(proxy, &block)
83
+
84
+ added = klass.instance_methods(false) - before
85
+ added.each do |m|
86
+ behavior.define_method(m, klass.instance_method(m))
87
+ end
88
+ end
89
+
90
+ extend_and_persist(behavior)
91
+ end
92
+ # rubocop: enable Metrics/AbcSize
93
+ # rubocop: enable Metrics/MethodLength
94
+
95
+ # rubocop: disable Metrics/MethodLength
96
+ def context_proxy(target, context, proxy = Object.new)
97
+ return target if context.nil?
98
+
99
+ proxy.define_singleton_method(:method_missing) do |method_name, *args, &block|
100
+ if context.local_variable_defined?(method_name)
101
+ context.local_variable_get(method_name)
102
+ elsif target.respond_to?(method_name)
103
+ target.public_send(method_name, *args, &block)
104
+ else
105
+ super(method_name, *args, &block)
106
+ end
107
+ end
108
+
109
+ proxy.define_singleton_method(:respond_to_missing?) do |method_name, include_private = false|
110
+ context.local_variable_defined?(method_name) || target.respond_to?(method_name, include_private)
111
+ end
112
+
113
+ proxy
114
+ end
115
+ # rubocop: enable Metrics/MethodLength
116
+
117
+ def extend_and_persist(behavior)
118
+ self.extend(behavior)
119
+ self.save_changes if self.respond_to?(:save_changes)
120
+ self
121
+ end
122
+
123
+ def ensure_behavior_module!(key, behavior)
124
+ raise BehaviorError.new(key, behavior) unless behavior.is_a?(::Module)
125
+ end
126
+
127
+ # The BuilderInstanceMethods module
128
+ module BuilderInstanceMethods
129
+ # Unknown method with a block => define it on behavior.
130
+ # def method_missing(method_name, *args, &block)
131
+ # return @target.public_send(method_name, *args) if block.nil?
132
+ # local_context = @context
133
+ # @behavior.send(:define_method, method_name) do |*call_args, &_block|
134
+ # instance_exec(local_context, *call_args, &block)
135
+ # end
136
+ # method_name
137
+ # rescue NoMethodError
138
+ # super
139
+ # end
140
+
141
+ def method_missing(method_name, *args, &block)
142
+ # 1) Try the real receiver first (even when a block is present)
143
+ log.debug "@target: #{@target.inspect}"
144
+ return @target.public_send(method_name, *args) if block.nil?
145
+ return @target.public_send(method_name, *args, &block) if @target.respond_to?(method_name)
146
+
147
+ # 2) Unknown method + block => define DSL method on behavior
148
+ return super if block.nil?
149
+
150
+ local_context = @context
151
+ @behavior.send(:define_method, method_name) do |*call_args, &_block|
152
+ instance_exec(local_context, *call_args, &block)
153
+ end
154
+ method_name
155
+ end
156
+
157
+ def respond_to_missing?(_method_name, _include_private = false)
158
+ true
159
+ end
160
+
161
+ def define_singleton_method(*args, &blk)
162
+ @target.define_singleton_method(*args, &blk)
163
+ end
164
+ end
165
+
166
+ def builder(target, behavior, context)
167
+ Class.new do
168
+ def initialize(target, behavior, context)
169
+ @target, @behavior, @context = target, behavior, context
170
+ self.extend(BuilderInstanceMethods)
171
+ end
172
+ end.new(target, behavior, context)
173
+ end
174
+ end
175
+ # module Inform
@@ -0,0 +1,74 @@
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 'yaml'
22
+
23
+ require_relative 'game_loader'
24
+
25
+ # The Inform module
26
+ module Inform
27
+ # The Game module
28
+ module Game
29
+ @config = {}
30
+
31
+ def init(options)
32
+ Inform::Game.config[:game_path] = game_dir_path(options[:game_path])
33
+ Inform::Game.config[:game_config_file_path] =
34
+ game_config_file_path(Inform::Game.config[:game_path])
35
+ Inform::Game.config.update(
36
+ YAML.load_file(Inform::Game.config[:game_config_file_path], symbolize_names: true)
37
+ )
38
+ end
39
+ module_function :init
40
+
41
+ def config
42
+ @config
43
+ end
44
+ module_function :config
45
+
46
+ def environment
47
+ (ENV['GAME_ENVIRONMENT'] || config[:environment] || Inform::Runtime.default_environment).to_sym
48
+ end
49
+ module_function :environment
50
+
51
+ def environment=(env)
52
+ config[:environment] = env.to_sym
53
+ end
54
+ module_function :environment=
55
+
56
+ private
57
+
58
+ def game_config_file_path(path)
59
+ File.join(path, 'config.yml')
60
+ end
61
+ module_function :game_config_file_path
62
+
63
+ def game_dir_path(path)
64
+ return path if File.directory?(path)
65
+ Inform::Game.config[:game_file_path] = path
66
+ game_dir_path = File.expand_path(File.dirname(path))
67
+ return game_dir_path if File.exist?(game_config_file_path(game_dir_path))
68
+ Inform::Runtime.default_game_dir_path
69
+ end
70
+ module_function :game_dir_path
71
+ end
72
+ # module Game
73
+ end
74
+ # module Inform
@@ -0,0 +1,132 @@
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
+ # The Inform module
22
+ module Inform
23
+ # The Game module
24
+ module Game
25
+ class GameLoadError < StandardError; end
26
+
27
+ # The Game::Loader module loads a game from a given directory.
28
+ module Loader
29
+ def load_game(game_dir_path = Inform::Game.config[:game_path])
30
+ self.load_path = game_dir_path
31
+ require_relative 'world_tree'
32
+ load_game_subcomponents
33
+ load_game_files
34
+ load_grammars
35
+ reset_constants
36
+ load_library
37
+ rescue StandardError => e
38
+ handle_load_failure(e)
39
+ end
40
+
41
+ def first_orphan_object_with_description_that_has_light
42
+ Inform::Object.all.find { |o| !o.description.nil? && o.has?(:light) }&.name
43
+ end
44
+
45
+ def load_path=(path)
46
+ @game_dir_path = path
47
+ $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
48
+ end
49
+
50
+ def load_game_subcomponents
51
+ game_components.each { |component| load_game_sub(component) }
52
+ end
53
+
54
+ def reset_constants
55
+ reset_constant(:HDR_GAMESERIAL, defined?(Serial) ? Serial : File.mtime(@game_dir_path).strftime('%y%m%d'))
56
+ reset_constant(:HDR_GAMERELEASE, defined?(Release) ? Release : 0)
57
+ end
58
+
59
+ def load_game_sub(component, start = Time.now)
60
+ game_component_path = File.join(@game_dir_path, component.to_s)
61
+ Dir.glob(File.join(game_component_path, '*')).sort.each do |file|
62
+ next unless File.file?(file)
63
+ log.debug "Loading file: #{file}"
64
+ require file
65
+ end
66
+ ensure
67
+ elapsed = (Time.now - start) * 1000
68
+ log.debug format("Loaded #{component} in %0.2f milliseconds", elapsed)
69
+ end
70
+
71
+ def load_game_files(game_path = @game_dir_path)
72
+ log.info "Loading game: #{game_path}"
73
+ Dir.glob(File.join(game_path, '*.rb')).sort.each do |file|
74
+ next unless File.file?(file)
75
+ log.debug "Loading file: #{file}"
76
+ require file
77
+ end
78
+ end
79
+
80
+ def handle_load_failure(e)
81
+ case e.message
82
+ when /PG::UndefinedTable/
83
+ log.error "Fatal: Database initialization is required"
84
+ abort
85
+ else
86
+ log.error "Error loading game: #{e.class.name}: #{e.message}"
87
+ e.backtrace.each { |t| log.error t }
88
+ end
89
+ end
90
+
91
+ # Move to the front of the list any element matching the given condition
92
+ def prioritize_if(list, &condition)
93
+ element = list.find(&condition)
94
+ return list if element.nil?
95
+ list.unshift(list.delete(element))
96
+ end
97
+
98
+ EnsureFirst = [/emotes.inf/, /grammar.inf/].freeze
99
+
100
+ def grammar_files
101
+ files = Dir.glob(File.join(grammar_module_path, '*'))
102
+ EnsureFirst.each do |pattern|
103
+ prioritize_if(files) { |file_path| file_path.match?(pattern) }
104
+ end
105
+ files
106
+ end
107
+
108
+ GrammarParsedInfoMessage = 'Parsed grammar %<file>s in %0.2<elapsed>f milliseconds'.freeze
109
+
110
+ def load_grammars
111
+ grammar_files.each do |file_path|
112
+ start = Time.now
113
+ Inform.load_grammar_by_path(file_path)
114
+ elapsed = (Time.now - start) * 1000
115
+ log.debug format(GrammarParsedInfoMessage, file: File.basename(file_path), elapsed: elapsed)
116
+ end
117
+ end
118
+
119
+ def load_game_states
120
+ states = Inform::Game.config.fetch(:additional_promiscuous_states, []).map(&:to_sym)
121
+ Session.add_promiscuous_states(states)
122
+ end
123
+
124
+ def reload_game
125
+ load_game
126
+ end
127
+ end
128
+ # module Loader
129
+ end
130
+ # module Game
131
+ end
132
+ # module Inform