inform-runtime 1.4.0 → 1.6.0

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