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,266 @@
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 'set'
22
+
23
+ # In order to support atmospheric messages and NPC automation
24
+ # based on timers, I'm trying to come up with an approach for
25
+ # incorporating the Inform 6 version of daemons into a multi-
26
+ # player environment.
27
+ #
28
+ # Perhaps some form of scanner will detect which objects implement
29
+ # the daemon method, and then schedule its invocation once every
30
+ # so often.
31
+ #
32
+ # For scheduled events:
33
+ #
34
+ # def after_initialize
35
+ # schedule('every ten minutes') do
36
+ # do_something
37
+ # end
38
+ # end
39
+ #
40
+ # For daemon events:
41
+ #
42
+ # def daemon
43
+ # do_something
44
+ # end
45
+ #
46
+ # The realtime application turns the Inform each_turn method
47
+ # into a synonym for daemon.
48
+ #
49
+ # Timers must also be supported.
50
+ #
51
+ # Object -> "blue box"
52
+ # with name 'blue' 'box',
53
+ # after [;
54
+ # SwitchOn: StartDaemon(self);
55
+ # SwitchOff: StopDaemon(self);
56
+ # ],
57
+ # daemon [;
58
+ # if (IndirectlyContains(location,self))
59
+ # "^An ominous ticking noise is coming from the box.";
60
+ # ],
61
+ # has switchable ~on;
62
+ #
63
+ # Object -> "red box"
64
+ # with name 'red' 'box',
65
+ # after [;
66
+ # SwitchOn: StartTimer(self,3);
67
+ # SwitchOff: StopTimer(self);
68
+ # ],
69
+ # time_out [;
70
+ # if (IndirectlyContains(location,self))
71
+ # print "^The box explodes into a zillion fragments.^";
72
+ # remove self;
73
+ # ],
74
+ # time_left 0,
75
+ # has switchable ~on;
76
+ #
77
+
78
+ # The Inform module
79
+ module Inform
80
+ def run_daemons
81
+ return if defined?(TURN_BASED)
82
+ Inform::Daemons.start
83
+ end
84
+
85
+ # The Inform::Daemons module
86
+ module Daemons
87
+ def spawn
88
+ @spawn ||= defined?(Java) ? java.util.concurrent.ConcurrentSkipListSet.new : Set.new
89
+ end
90
+
91
+ def heart
92
+ @heart ||= init_heart
93
+ end
94
+
95
+ def init_heart
96
+ # TODO: Figure out ruby-concurrency Executors?
97
+ return unless defined?(Java)
98
+ java.util.concurrent.Executors.newSingleThreadScheduledExecutor()
99
+ end
100
+
101
+ # The Inform::Daemons::Entheogen class encapsulates a heartbeat which
102
+ # is a ScheduledFuture created by the Heart executor and instantiated
103
+ # when Inform::Daemons.start is executed.
104
+ class Entheogen < Inform::System::Object
105
+ attr_accessor :heartbeat
106
+ end
107
+
108
+ def ghost
109
+ @ghost ||= Entheogen.new
110
+ end
111
+
112
+ def schedules
113
+ @schedules ||= defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : {}
114
+ end
115
+
116
+ def on?
117
+ return false if Inform::Daemons.ghost.heartbeat.nil?
118
+ Inform::Daemons.ghost.heartbeat.isCancelled() == false
119
+ end
120
+
121
+ def rescan
122
+ Inform::Daemons.spawn.clear
123
+ Inform::Daemons.scan_objects
124
+ end
125
+
126
+ DaemonScanElapsedMessage = "Scanned %<daemons>s daemons in %<elapsed>0.2f milliseconds".freeze
127
+
128
+ def self.scan_objects
129
+ start = Time.now
130
+ Inform::Object.all.each do |obj|
131
+ Spawn.add obj if obj.respond_to?(:daemon) || obj.respond_to?(:each_turn)
132
+ end
133
+ rescue StandardError => e
134
+ log.error "Error executing daemon method: #{e.message}", e
135
+ ensure
136
+ log.debug format(DaemonScanElapsedMessage, daemons: Spawn.length, elapsed: Time.now - start)
137
+ end
138
+
139
+ # rubocop: disable Metrics/AbcSize
140
+ def self.start
141
+ initial_delay = Inform::Game.config[:daemon_initial_delay]
142
+ period = Inform::Game.config[:daemons_period]
143
+ unit = defined?(Java) ? java.util.concurrent.TimeUnit::MILLISECONDS : 'milliseconds'
144
+ Thread.new do
145
+ Inform::Daemons.rescan
146
+ log.debug "Starting daemons..."
147
+ daemons = Inform::Daemons.method(:daemons).to_proc
148
+ Inform::Daemons.ghost.heartbeat = Inform::Daemons.heart.scheduleAtFixedRate(
149
+ daemons, initial_delay, period, unit)
150
+ end
151
+ end
152
+ # rubocop: enable Metrics/AbcSize
153
+
154
+ def self.stop
155
+ Inform::Daemons.ghost.heartbeat.cancel(false)
156
+ Inform::Daemons.spawn.clear
157
+ end
158
+
159
+ DaemonExecutionElapsedMessage = "\e[1m\e[33mExecuted daemons in %<elapsed>0.2f milliseconds\e[39m\e[22m".freeze
160
+
161
+ def self.daemons
162
+ start = Time.ms
163
+ Inform::Daemons.spawn.each do |daemon|
164
+ # In realtime games, each_turn is a synonym for daemon
165
+ next unless daemon.respond_to?(:daemon) || daemon.respond_to?(:each_turn)
166
+ Inform::Daemons.execute daemon
167
+ end
168
+ ensure
169
+ finish = Time.ms
170
+ elapsed = finish - start
171
+ log.debug format(DaemonExecutionElapsedMessage, elapsed: elapsed) if finish % 300_000 == 0
172
+ end
173
+
174
+ RecordNotFoundPattern = %r{Record not found}.freeze
175
+
176
+ def self.refresh_or_remove(obj)
177
+ return if obj.nil?
178
+ obj.refresh
179
+ rescue StandardError => e
180
+ Spawn.remove obj if RecordNotFoundPattern.match?(e.message)
181
+ end
182
+
183
+ def self.ensure_inform_library(obj)
184
+ return unless obj.inflib.nil?
185
+ # TODO: Use a pool of InformLibraries until
186
+ # comprehensive event contexts can be implemented
187
+ log.warn "Using singleton Ghost InformLibrary for #{obj} daemon"
188
+ obj.inflib = Inform::Runtime.libraries[Inform::Daemons.ghost]
189
+ end
190
+
191
+ # TODO: Potentially go ahead and print any string results
192
+ # from the daemon invocation. For example:
193
+ #
194
+ # obj.println obj.daemon
195
+ def self.execute_daemon(obj)
196
+ refresh_or_remove(obj)
197
+ ensure_inform_library(obj)
198
+ obj.daemon
199
+ end
200
+
201
+ def self.execute_each_turn(obj)
202
+ refresh_or_remove(obj)
203
+ ensure_inform_library(obj)
204
+ obj.each_turn
205
+ end
206
+
207
+ def self.execute(obj)
208
+ return if obj.nil?
209
+ if obj.respond_to? :daemon
210
+ execute_daemon(obj)
211
+ elsif obj.respond_to? :each_turn
212
+ execute_each_turn(obj)
213
+ end
214
+ rescue StandardError => e
215
+ log.error "Error executing daemon for #{obj}", e
216
+ end
217
+
218
+ def StartDaemon(obj)
219
+ Inform::Daemons.spawn.add obj if obj.respond_to?(:daemon) || obj.respond_to?(:each_turn)
220
+ end
221
+
222
+ def StopDaemon(obj)
223
+ return if obj.nil?
224
+ log.warn "Stopping daemon: #{obj}"
225
+ obj.undef_method(:daemon) # TODO: Maybe don't do this
226
+ Inform::Daemons.spawn.remove obj
227
+ end
228
+
229
+ def StartTimer(obj, interval)
230
+ # TODO: Implement
231
+ end
232
+
233
+ def StopTimer(obj)
234
+ # TODO: Implement
235
+ end
236
+
237
+ def daemonize(event)
238
+ Inform::Daemons.schedules[self] = event
239
+ end
240
+
241
+ def daemonic?
242
+ Inform::Daemons.schedules.include? self
243
+ end
244
+
245
+ def before_destroy
246
+ log.debug "#{self}: before_destroy stopping daemon"
247
+ StopDaemon(self) if daemonic?
248
+ super
249
+ end
250
+
251
+ # rubocop: disable Metrics/AbcSize
252
+ def occasionally(time = nil, &block)
253
+ time = Inform::Game.config[:daemons_frequency_default] if time.nil?
254
+ return if active? || daemonic?
255
+ frequency = ((self.&:frequency) || time).to_i
256
+ occasion = rand(frequency) + 1
257
+ log.trace "Scheduling occasional daemonic event for #{self} in #{occasion} seconds"
258
+ (Inform::Daemons.schedules[self] = delay(occasion, &block)).finally do
259
+ Inform::Daemons.schedules.delete self
260
+ end
261
+ end
262
+ end
263
+ # rubocop: enable Metrics/AbcSize
264
+ # module Daemons
265
+ end
266
+ # module Inform