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.
- checksums.yaml +7 -0
- data/LICENSE +623 -0
- data/README.md +185 -0
- data/Rakefile +65 -0
- data/config/database.yml +37 -0
- data/exe/inform.rb +6 -0
- data/game/config.yml +5 -0
- data/game/example.inf +76 -0
- data/game/example.rb +90 -0
- data/game/forms/example_form.rb +2 -0
- data/game/grammar/game_grammar.inf.rb +11 -0
- data/game/languages/english.rb +2 -0
- data/game/models/example_model.rb +2 -0
- data/game/modules/example_module.rb +9 -0
- data/game/rules/example_state.rb +2 -0
- data/game/scripts/example_script.rb +2 -0
- data/game/topics/example_topic.rb +2 -0
- data/game/verbs/game_verbs.rb +15 -0
- data/game/verbs/metaverbs.rb +2028 -0
- data/lib/runtime/articles.rb +138 -0
- data/lib/runtime/builtins.rb +359 -0
- data/lib/runtime/color.rb +145 -0
- data/lib/runtime/command.rb +470 -0
- data/lib/runtime/config.rb +48 -0
- data/lib/runtime/context.rb +78 -0
- data/lib/runtime/daemon.rb +266 -0
- data/lib/runtime/database.rb +500 -0
- data/lib/runtime/events.rb +771 -0
- data/lib/runtime/experimental/handler_dsl.rb +175 -0
- data/lib/runtime/game.rb +74 -0
- data/lib/runtime/game_loader.rb +132 -0
- data/lib/runtime/grammar_parser.rb +553 -0
- data/lib/runtime/helpers.rb +177 -0
- data/lib/runtime/history.rb +45 -0
- data/lib/runtime/inflector.rb +195 -0
- data/lib/runtime/io.rb +174 -0
- data/lib/runtime/kernel.rb +450 -0
- data/lib/runtime/library.rb +59 -0
- data/lib/runtime/library_loader.rb +135 -0
- data/lib/runtime/link.rb +158 -0
- data/lib/runtime/logging.rb +197 -0
- data/lib/runtime/mixins.rb +570 -0
- data/lib/runtime/module.rb +202 -0
- data/lib/runtime/object.rb +761 -0
- data/lib/runtime/options.rb +104 -0
- data/lib/runtime/persistence.rb +292 -0
- data/lib/runtime/plurals.rb +60 -0
- data/lib/runtime/prototype.rb +307 -0
- data/lib/runtime/publication.rb +92 -0
- data/lib/runtime/runtime.rb +321 -0
- data/lib/runtime/session.rb +202 -0
- data/lib/runtime/stdlib.rb +604 -0
- data/lib/runtime/subscription.rb +47 -0
- data/lib/runtime/tag.rb +287 -0
- data/lib/runtime/tree.rb +204 -0
- data/lib/runtime/version.rb +24 -0
- data/lib/runtime/world_tree.rb +69 -0
- data/lib/runtime.rb +35 -0
- metadata +199 -0
|
@@ -0,0 +1,771 @@
|
|
|
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 Time class
|
|
22
|
+
class Time
|
|
23
|
+
def to_ms
|
|
24
|
+
(self.to_f * 1000.0).to_i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.ns
|
|
28
|
+
self.now.to_f
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.ms
|
|
32
|
+
self.now.to_ms
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.s
|
|
36
|
+
self.now.to_i
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Re-open the Proc class to define #to_lambda
|
|
41
|
+
class Proc
|
|
42
|
+
WRONG_NUMBER_ARGUMENTS_MESSAGE =
|
|
43
|
+
"Wrong number of arguments (given %<args>s, expected %<arity>s)".freeze
|
|
44
|
+
|
|
45
|
+
# rubocop: disable Metrics/MethodLength
|
|
46
|
+
# rubocop: disable Style/Lambda
|
|
47
|
+
def to_lambda
|
|
48
|
+
return self if lambda?
|
|
49
|
+
# Save local reference to self so we can use it in the following lambda scope
|
|
50
|
+
source_proc = self
|
|
51
|
+
->(*args, **kwargs, &block) do
|
|
52
|
+
# Enforce strict arity like a lambda
|
|
53
|
+
source_arity = source_proc.arity
|
|
54
|
+
if source_arity >= 0 && args.length != source_arity
|
|
55
|
+
raise ArgumentError, format(
|
|
56
|
+
WRONG_NUMBER_ARGUMENTS_MESSAGE, args: args.length, arity: source_arity)
|
|
57
|
+
elsif args.length < (min = -source_arity - 1)
|
|
58
|
+
raise ArgumentError, format(
|
|
59
|
+
WRONG_NUMBER_ARGUMENTS_MESSAGE, args: args.length, arity: "#{min}+")
|
|
60
|
+
end
|
|
61
|
+
# Preserve original binding/locals/self
|
|
62
|
+
source_proc.call(*args, **kwargs, &block)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
# rubocop: enable Metrics/MethodLength
|
|
66
|
+
# rubocop: enable Style/Lambda
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The Inform module
|
|
70
|
+
module Inform
|
|
71
|
+
# The EventCauseRecordNotFoundError class
|
|
72
|
+
class EventCauseRecordNotFoundError < StandardError; end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
module EventConstants
|
|
76
|
+
EventNameTemplate = 'event:%<time>s'.freeze
|
|
77
|
+
EventCauseIdentityTemplate = '%<identity>s:%<name>s'.freeze
|
|
78
|
+
EventSourceTemplate = ':%<source_location>s:%<source_line_number>s'.freeze
|
|
79
|
+
EventSourceFullTemplate = ':%<method_name>s:%<source_location>s:%<source_line_number>s'.freeze
|
|
80
|
+
EventActivityTemplate = ':%<activity>s'.freeze
|
|
81
|
+
EventAntecedantTemplate = ' < %<antecedent>s'.freeze
|
|
82
|
+
CallerPattern = %r{([^:]+):([^:]+):in `([^']+)'}.freeze
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The PublicEventMethods module
|
|
86
|
+
module PublicEventMethods
|
|
87
|
+
include EventConstants
|
|
88
|
+
|
|
89
|
+
def contextualize(from)
|
|
90
|
+
# log.info "Initializing context for event #{self} from #{from} (#{from.class})"
|
|
91
|
+
Inform::Context.each_attribute(from) do |attribute, value|
|
|
92
|
+
warn_when_nilling_noun(from, self, attribute, value)
|
|
93
|
+
send(Inform::Context.setter_method(attribute), value)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# TODO: Remove
|
|
98
|
+
def warn_when_nilling_noun(from, to, attribute, value)
|
|
99
|
+
return unless attribute == :noun
|
|
100
|
+
return unless value.nil?
|
|
101
|
+
return if (current_value = to.send(attribute)).nil?
|
|
102
|
+
return if attribute == :parameters && current_value <= 1
|
|
103
|
+
log.debug "Overwriting #{to}.#{attribute} (#{current_value}) with #{from}.#{attribute} (nil)!"
|
|
104
|
+
log.debug " #{to}.context: #{to.context.inspect}"
|
|
105
|
+
maybe_log_nilling_warning(to.callstack)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# TODO: Remove
|
|
109
|
+
def maybe_log_nilling_warning(callstack)
|
|
110
|
+
return if callstack.grep('channel_read')
|
|
111
|
+
['===========', (caller[-4...] + callstack), '==========='].each { |t| log.warn t unless t.nil? }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def <<(event)
|
|
115
|
+
@successors.add event
|
|
116
|
+
event.antecedent = self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def origin(x = self)
|
|
120
|
+
x = origin x.antecedent if x.antecedent
|
|
121
|
+
x.object? ? x : x.cause
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def termination(x = self)
|
|
125
|
+
event = x.successors.first
|
|
126
|
+
event || x.terminus
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def elemental?
|
|
130
|
+
@type == :elemental
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def integral?
|
|
134
|
+
@type == :integral
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def immediately?
|
|
138
|
+
@when == :immediately
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def cancelled?
|
|
142
|
+
return true if future.nil?
|
|
143
|
+
future.cancelled?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def antecedent_cancelled?
|
|
147
|
+
return false if self.antecedent.nil?
|
|
148
|
+
return false unless self.antecedent.cancelled?
|
|
149
|
+
return false if self.terminus.nil?
|
|
150
|
+
log.warn "Event antecedent #{self.antecedent} was cancelled"
|
|
151
|
+
true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def concluded?
|
|
155
|
+
semaphore.synchronize { concluded == true }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def get
|
|
159
|
+
if future.respond_to?(:get)
|
|
160
|
+
future.get
|
|
161
|
+
elsif future.respond_to?(:call)
|
|
162
|
+
future.call
|
|
163
|
+
end
|
|
164
|
+
future
|
|
165
|
+
rescue SystemExit => e
|
|
166
|
+
raise e
|
|
167
|
+
# rescue EventCancelled => e
|
|
168
|
+
# log.warn "Cancelled #{event}"
|
|
169
|
+
# log.debug "Reason for event cancellation: #{e}"
|
|
170
|
+
rescue StandardError => e
|
|
171
|
+
log.error "Event execution exception: #{e.message}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def java_get
|
|
175
|
+
self.future.get
|
|
176
|
+
self.future
|
|
177
|
+
rescue SystemExit => e
|
|
178
|
+
raise e
|
|
179
|
+
rescue Java::JavaUtilConcurrent::CancellationException => e
|
|
180
|
+
log.warn "Cancelled #{event}"
|
|
181
|
+
log.debug "Reason for event cancellation: #{e}"
|
|
182
|
+
rescue Java::JavaUtilConcurrent::ExecutionException => e
|
|
183
|
+
log.error "Event execution exception: #{e.message}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def now(params = {}, &block)
|
|
187
|
+
# TODO: Determine if :cause should be overwritten
|
|
188
|
+
# by any :cause params given by the params Hash
|
|
189
|
+
Inform::Event.new({ cause: cause, antecedent: self }.merge(params), &block)
|
|
190
|
+
end
|
|
191
|
+
alias add_event now
|
|
192
|
+
alias event now
|
|
193
|
+
alias and_then now
|
|
194
|
+
alias chain now
|
|
195
|
+
alias eventually now
|
|
196
|
+
alias later now
|
|
197
|
+
|
|
198
|
+
# Shortcut for using delays
|
|
199
|
+
def delay(delay, &block)
|
|
200
|
+
Inform::Event.new({ cause: cause, antecedent: self, delay: delay }, &block)
|
|
201
|
+
end
|
|
202
|
+
alias after_delay delay
|
|
203
|
+
|
|
204
|
+
def finally(&block)
|
|
205
|
+
Inform::Event.new({ cause: cause, antecedent: self, terminus: true }, &block)
|
|
206
|
+
end
|
|
207
|
+
alias on_cancel finally
|
|
208
|
+
alias when_completed finally
|
|
209
|
+
alias when_done finally
|
|
210
|
+
alias when_finished finally
|
|
211
|
+
alias when_over finally
|
|
212
|
+
alias ultimately finally
|
|
213
|
+
|
|
214
|
+
def parse_activity
|
|
215
|
+
self.cause.parse(self.activity)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def call_activity
|
|
219
|
+
return self.activity.call(self) if self.activity.arity == 1
|
|
220
|
+
self.activity.call(*self.args)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# java_signature 'V call()'
|
|
224
|
+
def call
|
|
225
|
+
process(self)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# java_signature 'void run()'
|
|
229
|
+
def run
|
|
230
|
+
call
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def on_failure(_cause)
|
|
234
|
+
cancelled
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def on_success(_result)
|
|
238
|
+
completed
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def on_cancelled_antecedent
|
|
242
|
+
schedule self unless terminus.nil?
|
|
243
|
+
successors.each(&:on_cancelled_antecedent)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def to_s
|
|
247
|
+
self.identity
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def inspect
|
|
251
|
+
format(EventCauseIdentityTemplate, identity: @cause.identity, name: @cause.name)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
RecordNotFoundErrorPattern = %r{Record not found}.freeze
|
|
255
|
+
|
|
256
|
+
def mandate_cause_record_exists!
|
|
257
|
+
self.cause.refresh
|
|
258
|
+
rescue StandardError => e
|
|
259
|
+
if RecordNotFoundErrorPattern.match?(e.message)
|
|
260
|
+
message = e.message + ' for cause of event ' + self.to_s
|
|
261
|
+
raise Inform::EventCauseRecordNotFoundError, message
|
|
262
|
+
end
|
|
263
|
+
log.warn "Unexpected error refreshing event cause object: #{e.message}"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
# module PublicEventMethods
|
|
267
|
+
|
|
268
|
+
# The PrivateEventMethods module
|
|
269
|
+
module PrivateEventMethods
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
def to_java_future(future)
|
|
273
|
+
future.to_java(com.google.common.util.concurrent.ListenableFuture)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# TODO: Implement a mechanism by which new event scheduling may
|
|
277
|
+
# be deferred or discarded entirely. The reason for this is that
|
|
278
|
+
# during shutdown, there will certainly be ongoing events which
|
|
279
|
+
# require some form of state resolution. The spinning top, for
|
|
280
|
+
# instance -- once spun, is given the spinning attribute. Now,
|
|
281
|
+
# perhaps this is unnecessary, but the case remains that the state
|
|
282
|
+
# of the top must be returned to its resting state even though the
|
|
283
|
+
# game is being shutdown. Perhaps a special on_cancel hook could
|
|
284
|
+
# be provided for such events which require such state resolution.
|
|
285
|
+
# Otherwise, at this point, some way of deciding if incoming
|
|
286
|
+
# events are newly spawned, or if they are part of a chain of
|
|
287
|
+
# events which must be allowed to transpire for the sake of an
|
|
288
|
+
# object's state consistency.
|
|
289
|
+
# rubocop: disable Metrics/AbcSize
|
|
290
|
+
def schedule(event, delay = 0)
|
|
291
|
+
raise "The event parameter may not be nil" if event.nil?
|
|
292
|
+
delay = event.time_delay.to_f * 1000 if event.time_delay > 0
|
|
293
|
+
schedule_event(event, delay)
|
|
294
|
+
add_future_callback(event, Inform::Events.scheduler)
|
|
295
|
+
event.cause.events << event
|
|
296
|
+
Inform::Events.active_objects << event.cause unless active?(event.cause)
|
|
297
|
+
event.future
|
|
298
|
+
end
|
|
299
|
+
# rubocop: enable Metrics/AbcSize
|
|
300
|
+
|
|
301
|
+
def active?(obj)
|
|
302
|
+
Inform::Events.active_objects.include?(obj)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def schedule_event(event, delay)
|
|
306
|
+
event.future = if defined?(Java)
|
|
307
|
+
Inform::Events.scheduler.schedule(event, delay, TimeUnit::MILLISECONDS)
|
|
308
|
+
else
|
|
309
|
+
# TODO: Implement
|
|
310
|
+
Inform::Events.scheduler.schedule(event, delay)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def add_future_callback(event, executor)
|
|
315
|
+
if defined?(java)
|
|
316
|
+
Futures.addCallback(to_java_future(event.future), event, executor)
|
|
317
|
+
else
|
|
318
|
+
# TODO: Implement
|
|
319
|
+
event.future
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# rubocop: disable Metrics/AbcSize
|
|
324
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
325
|
+
# rubocop: disable Metrics/MethodLength
|
|
326
|
+
def process(event)
|
|
327
|
+
raise "The event parameter may not be nil" if event.nil?
|
|
328
|
+
Thread.current[:event] = event
|
|
329
|
+
event.mandate_cause_record_exists!
|
|
330
|
+
# TODO: This is probably racey so consider wrapping this in a semaphore.
|
|
331
|
+
return if event.antecedent_cancelled?
|
|
332
|
+
case event.activity
|
|
333
|
+
when String
|
|
334
|
+
result = event.parse_activity
|
|
335
|
+
# event.cause.inflib&.println(result)
|
|
336
|
+
# event.cause.println(result)
|
|
337
|
+
handle(event, result)
|
|
338
|
+
when Proc
|
|
339
|
+
result = event.call_activity
|
|
340
|
+
# event.cause.inflib&.print_evented_zmachine_result(event.call_activity, isolate: event.elemental?)
|
|
341
|
+
# event.cause.print_evented_zmachine_result(result, isolate: event.elemental?)
|
|
342
|
+
handle(event, result)
|
|
343
|
+
else
|
|
344
|
+
log.error "Event activity type is unknown: #{event.activity} (#{event.activity.class})"
|
|
345
|
+
end
|
|
346
|
+
rescue Inform::EventCauseRecordNotFoundError => e
|
|
347
|
+
log.error "Error refreshing cause object: #{e.message}"
|
|
348
|
+
rescue LocalJumpError => e
|
|
349
|
+
# It is a shame that this happens. It used to not happen, so long
|
|
350
|
+
# as event.activity was wrapped in lambda(event.activity).
|
|
351
|
+
# Then one day, this exception began to be thrown again.
|
|
352
|
+
# TODO: Either use this kludgey hack to fix it:
|
|
353
|
+
# https://stackoverflow.com/questions/2946603/ruby-convert-proc-to-lambda
|
|
354
|
+
# https://stackoverflow.com/a/24965981/328469
|
|
355
|
+
# Or, try to get jruby fixed:
|
|
356
|
+
# https://github.com/jruby/jruby/issues/4369 (2016)
|
|
357
|
+
# https://github.com/jruby/jruby/pull/4423 (2017)
|
|
358
|
+
# https://github.com/jruby/jruby/issues/4577 (2017)
|
|
359
|
+
log.warn "Event block used return keyword: #{e.message}"
|
|
360
|
+
log.debug "Local jump error: #{e.message}"
|
|
361
|
+
e.backtrace.each { |t| log.warn t }
|
|
362
|
+
rescue SystemExit => e
|
|
363
|
+
raise e
|
|
364
|
+
rescue StandardError => e
|
|
365
|
+
log << "\n"
|
|
366
|
+
log.error "Unexpected error processing #{event}: #{e.message}", e
|
|
367
|
+
event.callstack.each { |t| log.error t }
|
|
368
|
+
ensure
|
|
369
|
+
Thread.current[:event] = nil
|
|
370
|
+
end
|
|
371
|
+
# rubocop: enable Metrics/AbcSize
|
|
372
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
373
|
+
# rubocop: enable Metrics/MethodLength
|
|
374
|
+
|
|
375
|
+
# rubocop: disable Metrics/AbcSize
|
|
376
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
377
|
+
# rubocop: disable Metrics/MethodLength
|
|
378
|
+
def invoke(event)
|
|
379
|
+
raise "The event parameter may not be nil" if event.nil?
|
|
380
|
+
event.antecedent << event if !event.antecedent.nil? && !event.antecedent.concluded?
|
|
381
|
+
se = Thread.current[:event]
|
|
382
|
+
Thread.current[:event] = event
|
|
383
|
+
log.debug "Queueing #{event}"
|
|
384
|
+
invoke_event(event)
|
|
385
|
+
add_future_callback(event, Inform::Events.pool)
|
|
386
|
+
event.cause.events << event
|
|
387
|
+
Inform::Events.active_objects << event.cause unless Inform::Events.active_objects.include?(event.cause)
|
|
388
|
+
return defined?(Java) ? event.java_get : event.get
|
|
389
|
+
rescue StandardError => e
|
|
390
|
+
log.error "Unexpected error invoking #{event}: #{e.message}", e
|
|
391
|
+
event.callstack.each { |t| log.error t }
|
|
392
|
+
ensure
|
|
393
|
+
Thread.current[:event] = se
|
|
394
|
+
end
|
|
395
|
+
# rubocop: enable Metrics/AbcSize
|
|
396
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
397
|
+
# rubocop: enable Metrics/MethodLength
|
|
398
|
+
|
|
399
|
+
def invoke_event(event)
|
|
400
|
+
event.future = if defined?(Java)
|
|
401
|
+
Inform::Events.pool.java_send(:submit, [java.lang.Runnable], event)
|
|
402
|
+
else
|
|
403
|
+
-> { event.call }
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def handle(event, result)
|
|
408
|
+
case result
|
|
409
|
+
when String
|
|
410
|
+
event.cause.println result
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def deactivate(obj)
|
|
415
|
+
raise "The obj parameter may not be nil" if obj.nil?
|
|
416
|
+
obj.events.remove self
|
|
417
|
+
Inform::Events.active_objects.remove obj if obj.events.empty?
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def cancelled
|
|
421
|
+
successors.each(&:on_cancelled_antecedent)
|
|
422
|
+
rescue StandardError => e
|
|
423
|
+
log.error "Error cancelling #{self}: #{e.message}", e
|
|
424
|
+
ensure
|
|
425
|
+
deactivate cause
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# rubocop: disable Metrics/AbcSize
|
|
429
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
430
|
+
def completed
|
|
431
|
+
semaphore.synchronize { self.concluded = true }
|
|
432
|
+
return if !antecedent.nil? && antecedent.cancelled? && terminus.nil?
|
|
433
|
+
return if cancelled? && terminus.nil?
|
|
434
|
+
successors.each { |event| schedule event }
|
|
435
|
+
rescue StandardError => e
|
|
436
|
+
log.error "Error completing #{self}: #{e.message}", e
|
|
437
|
+
ensure
|
|
438
|
+
deactivate cause
|
|
439
|
+
end
|
|
440
|
+
# rubocop: enable Metrics/AbcSize
|
|
441
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
442
|
+
end
|
|
443
|
+
# module PrivateEventMethods
|
|
444
|
+
|
|
445
|
+
# The EventInitializationMethods module
|
|
446
|
+
module EventInitializationMethods
|
|
447
|
+
include EventConstants
|
|
448
|
+
|
|
449
|
+
# rubocop: disable Metrics/AbcSize
|
|
450
|
+
# rubocop: disable Metrics/MethodLength
|
|
451
|
+
def init_fields(params, &block)
|
|
452
|
+
@successors = defined?(Java) ? java.util.concurrent.CopyOnWriteArrayList.new : []
|
|
453
|
+
@time = Time.ms
|
|
454
|
+
@cause = params[:cause]
|
|
455
|
+
@name = params.fetch(:name, format(EventNameTemplate, time: @time))
|
|
456
|
+
@args = params[:args]
|
|
457
|
+
# Converting a given block to a lambda supposedly enables the use of return
|
|
458
|
+
# statements inside the event activity blocks.
|
|
459
|
+
# TODO: Cite documentation source.
|
|
460
|
+
# TODO: Implement tests.
|
|
461
|
+
# TODO: Verify this change prevents the error:
|
|
462
|
+
# Event block used return keyword: unexpected return
|
|
463
|
+
@activity = params.fetch(:activity, block_given? ? block.to_lambda : @name)
|
|
464
|
+
@antecedent = params[:antecedent]
|
|
465
|
+
@time_delay = params.fetch(:delay, 0)
|
|
466
|
+
@terminus = params[:terminus] ? self : nil
|
|
467
|
+
@type = params.fetch(:type, :integral)
|
|
468
|
+
@when = params.fetch(:when, :eventually)
|
|
469
|
+
@callstack = caller.slice((%i[immediately elementally].include?(@when) ? 4 : 3)..-1)
|
|
470
|
+
@concluded = false
|
|
471
|
+
# @cause = @cause.player if @cause.is_a? InformLibrary # TODO: Test
|
|
472
|
+
@identity = generate_identity
|
|
473
|
+
@context = params[:context]
|
|
474
|
+
end
|
|
475
|
+
# rubocop: enable Metrics/AbcSize
|
|
476
|
+
# rubocop: enable Metrics/MethodLength
|
|
477
|
+
|
|
478
|
+
# rubocop: disable Metrics/AbcSize
|
|
479
|
+
# rubocop: disable Metrics/MethodLength
|
|
480
|
+
def generate_identity
|
|
481
|
+
s = format(EventCauseIdentityTemplate, identity: @cause.identity, name: @cause.name)
|
|
482
|
+
if !@callstack.empty?
|
|
483
|
+
trace = @callstack.first.gsub(Inform::Runtime.project_dir_path, '')
|
|
484
|
+
location, line_number, method_name = CallerPattern.match(trace)&.captures
|
|
485
|
+
s << format(EventSourceFullTemplate,
|
|
486
|
+
method_name: method_name, source_location: location, source_line_number: line_number)
|
|
487
|
+
elsif @activity.respond_to?(:source_location) || @activity.is_a?(Proc)
|
|
488
|
+
activity_source_location_enumerator = @activity.source_location.each
|
|
489
|
+
source_location = activity_source_location_enumerator.next.gsub(Inform::Runtime.project_dir_path, '')
|
|
490
|
+
source_line_number = activity_source_location_enumerator.next
|
|
491
|
+
s << format(
|
|
492
|
+
EventSourceTemplate, source_location: source_location, source_line_number: source_line_number)
|
|
493
|
+
elsif name != @activity.to_s
|
|
494
|
+
s << format(EventActivityTemplate, activity: @activity)
|
|
495
|
+
end
|
|
496
|
+
s << format(EventAntecedantTemplate, antecedent: @antecedent) unless @antecedent.nil?
|
|
497
|
+
s
|
|
498
|
+
end
|
|
499
|
+
# rubocop: enable Metrics/AbcSize
|
|
500
|
+
# rubocop: enable Metrics/MethodLength
|
|
501
|
+
|
|
502
|
+
def init_context(context, antecedent, cause)
|
|
503
|
+
return contextualize(context) unless context.nil?
|
|
504
|
+
if antecedent&.cause.respond_to?(:inflib)
|
|
505
|
+
return contextualize(antecedent.cause.inflib) unless antecedent&.cause&.inflib.nil?
|
|
506
|
+
end
|
|
507
|
+
contextualize(cause) unless cause.nil? # TODO: Maybe remove
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def defer(event)
|
|
511
|
+
event.antecedent << event
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def schedule_or_defer(event)
|
|
515
|
+
return defer(event) if !event.antecedent.nil? && !event.antecedent.concluded?
|
|
516
|
+
schedule(event) if event.antecedent.nil? || event.antecedent.cancelled?
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
# module EventInitializationMethods
|
|
520
|
+
|
|
521
|
+
# The Inform module
|
|
522
|
+
module Inform
|
|
523
|
+
# The Inform::Event class
|
|
524
|
+
class Event
|
|
525
|
+
include PublicEventMethods
|
|
526
|
+
include PrivateEventMethods
|
|
527
|
+
|
|
528
|
+
attr_reader :cause, :time, :time_delay, :name, :args, :type, :context,
|
|
529
|
+
:activity, :terminus, :semaphore, :identity
|
|
530
|
+
attr_accessor :antecedent, :successors, :future, :concluded, :callstack
|
|
531
|
+
|
|
532
|
+
def initialize(params = {}, &block)
|
|
533
|
+
@semaphore = Mutex.new
|
|
534
|
+
init_fields(params, &block)
|
|
535
|
+
init_context(@context, @antecedent, @cause)
|
|
536
|
+
invoke(self)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
private
|
|
540
|
+
|
|
541
|
+
include EventInitializationMethods
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# The Inform::Events module
|
|
545
|
+
module Events
|
|
546
|
+
REGISTRY = Struct.new(:memo).new({})
|
|
547
|
+
THREADS_PER_PROCESSOR = 10
|
|
548
|
+
|
|
549
|
+
def active_objects
|
|
550
|
+
@active_objects ||= defined?(Java) ? java.util.concurrent.ConcurrentLinkedQueue.new : []
|
|
551
|
+
end
|
|
552
|
+
module_function :active_objects
|
|
553
|
+
|
|
554
|
+
def possibilities
|
|
555
|
+
@possibilities ||= defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : {}
|
|
556
|
+
end
|
|
557
|
+
module_function :possibilities
|
|
558
|
+
|
|
559
|
+
def object_events
|
|
560
|
+
Inform::Events.possibilities[identity] ||=
|
|
561
|
+
defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : {}
|
|
562
|
+
Inform::Events.possibilities[identity]
|
|
563
|
+
end
|
|
564
|
+
module_function :object_events
|
|
565
|
+
|
|
566
|
+
def events(klass = Inform::Event)
|
|
567
|
+
object_events[klass] ||= defined?(ConcurrentLinkedQueue) ? ConcurrentLinkedQueue.new : []
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def each(event_class = Inform::Event, &block)
|
|
571
|
+
events[event_class].each { |event| block.call(event) } if block_given?
|
|
572
|
+
end
|
|
573
|
+
module_function :each
|
|
574
|
+
|
|
575
|
+
# rubocop: disable Metrics/AbcSize
|
|
576
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
577
|
+
# rubocop: disable Metrics/MethodLength
|
|
578
|
+
def available_processors
|
|
579
|
+
return java.lang.Runtime.runtime.available_processors if defined?(Java)
|
|
580
|
+
case RbConfig::CONFIG['host_os']
|
|
581
|
+
when /darwin9/
|
|
582
|
+
if File.exist?(`which hwprefs`)
|
|
583
|
+
`hwprefs cpu_count`.strip.to_i
|
|
584
|
+
else
|
|
585
|
+
# Better than nothing
|
|
586
|
+
require 'etc'
|
|
587
|
+
Etc.nprocessors
|
|
588
|
+
end
|
|
589
|
+
when /darwin/
|
|
590
|
+
if File.exist?(`which hwprefs`)
|
|
591
|
+
`hwprefs thread_count`.strip.to_i
|
|
592
|
+
else
|
|
593
|
+
`sysctl -n hw.ncpu`.strip.to_i
|
|
594
|
+
end
|
|
595
|
+
when /linux/
|
|
596
|
+
`grep -c processor /proc/cpuinfo`.strip.to_i
|
|
597
|
+
when /freebsd/
|
|
598
|
+
`sysctl -n hw.ncpu`.strip.to_i
|
|
599
|
+
when /mswin|mingw/
|
|
600
|
+
require 'win32ole'
|
|
601
|
+
cpu = WIN32OLE.connect("winmgmts://").ExecQuery("select NumberOfCores from Win32_Processor")
|
|
602
|
+
cpu.to_enum.first.NumberOfCores
|
|
603
|
+
else
|
|
604
|
+
# Better than nothing
|
|
605
|
+
require 'etc'
|
|
606
|
+
Etc.nprocessors
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
# rubocop: enable Metrics/AbcSize
|
|
610
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
611
|
+
# rubocop: enable Metrics/MethodLength
|
|
612
|
+
module_function :available_processors
|
|
613
|
+
|
|
614
|
+
def scheduled_executor
|
|
615
|
+
REGISTRY.memo[:scheduled_executor] ||=
|
|
616
|
+
defined?(Java) ? init_java_scheduled_executor : init_scheduled_executor
|
|
617
|
+
end
|
|
618
|
+
module_function :scheduled_executor
|
|
619
|
+
|
|
620
|
+
def init_scheduled_executor
|
|
621
|
+
# TODO: Implement
|
|
622
|
+
# require 'ruby-concurrency'
|
|
623
|
+
# Concurrent::ScheduledThreadPoolExecutor.new(available_processors * THREADS_PER_PROCESSOR)
|
|
624
|
+
end
|
|
625
|
+
module_function :init_scheduled_executor
|
|
626
|
+
|
|
627
|
+
def init_java_scheduled_executor
|
|
628
|
+
executor = java.util.concurrent.Executors.newScheduledThreadPool(
|
|
629
|
+
available_processors * THREADS_PER_PROCESSOR)
|
|
630
|
+
executor.setRemoveOnCancelPolicy(true)
|
|
631
|
+
executor
|
|
632
|
+
end
|
|
633
|
+
module_function :init_java_scheduled_executor
|
|
634
|
+
|
|
635
|
+
def pooled_executor
|
|
636
|
+
REGISTRY.memo[:pooled_executor] ||=
|
|
637
|
+
defined?(Java) ? init_java_pooled_executor : init_pooled_executor
|
|
638
|
+
end
|
|
639
|
+
module_function :pooled_executor
|
|
640
|
+
|
|
641
|
+
def init_java_pooled_executor
|
|
642
|
+
Executors.newFixedThreadPool(available_processors * THREADS_PER_PROCESSOR)
|
|
643
|
+
end
|
|
644
|
+
module_function :init_java_pooled_executor
|
|
645
|
+
|
|
646
|
+
def scheduler
|
|
647
|
+
REGISTRY.memo[:scheduler] ||=
|
|
648
|
+
defined?(Java) ? init_java_scheduler : init_scheduler
|
|
649
|
+
end
|
|
650
|
+
module_function :scheduler
|
|
651
|
+
|
|
652
|
+
def init_scheduler
|
|
653
|
+
# TODO: Implement
|
|
654
|
+
# require 'ruby-concurrency'
|
|
655
|
+
# Concurrent::ThreadPoolExecutor.new(available_processors * THREADS_PER_PROCESSOR)
|
|
656
|
+
end
|
|
657
|
+
module_function :init_scheduler
|
|
658
|
+
|
|
659
|
+
def init_java_scheduler
|
|
660
|
+
com.google.common.util.concurrent.MoreExecutors.listeningDecorator(ScheduledExecutor)
|
|
661
|
+
end
|
|
662
|
+
module_function :init_java_scheduler
|
|
663
|
+
|
|
664
|
+
def pool
|
|
665
|
+
REGISTRY.memo[:pool] ||= defined?(Java) ? init_java_pool : init_pool
|
|
666
|
+
end
|
|
667
|
+
module_function :pool
|
|
668
|
+
|
|
669
|
+
def init_pool
|
|
670
|
+
# TODO: Implement
|
|
671
|
+
# require 'ruby-concurrency'
|
|
672
|
+
# Concurrent::ThreadPoolExecutor.new(available_processors * THREADS_PER_PROCESSOR)
|
|
673
|
+
end
|
|
674
|
+
module_function :init_pool
|
|
675
|
+
|
|
676
|
+
def init_java_pool
|
|
677
|
+
com.google.common.util.concurrent.MoreExecutors.listeningDecorator(pooled_executor)
|
|
678
|
+
end
|
|
679
|
+
module_function :init_java_pool
|
|
680
|
+
end
|
|
681
|
+
# module Events
|
|
682
|
+
end
|
|
683
|
+
# module Inform
|
|
684
|
+
|
|
685
|
+
# The Inform module
|
|
686
|
+
module Inform
|
|
687
|
+
# The Inform::Events module
|
|
688
|
+
module Events
|
|
689
|
+
DEFAULT_ADD_EVENT_PARAMS = { delay: 0 }.freeze
|
|
690
|
+
|
|
691
|
+
def register_callback(ctx, *args, &callback)
|
|
692
|
+
e = Thread.current[:event]
|
|
693
|
+
return true if e.nil?
|
|
694
|
+
e.chain({ context: ctx, args: args }, &callback)
|
|
695
|
+
true
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def add_event(params = {}, event_params = { cause: self }, &block)
|
|
699
|
+
if (e = Thread.current[:event]).nil?
|
|
700
|
+
params = { command: params } unless params.respond_to?(:merge)
|
|
701
|
+
event_params[:name] = params.delete(:command) if params.include?(:command)
|
|
702
|
+
Inform::Event.new(event_params.merge(params), &block)
|
|
703
|
+
else
|
|
704
|
+
event_params[:name] = params.delete(:command) if params.include?(:command)
|
|
705
|
+
e.chain(event_params.merge(DEFAULT_ADD_EVENT_PARAMS.merge(event_params)), &block)
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
alias eventually add_event
|
|
709
|
+
alias later add_event
|
|
710
|
+
alias queue add_event
|
|
711
|
+
alias enqueue add_event
|
|
712
|
+
|
|
713
|
+
# TODO: Figure out a way to determine if an existing event is
|
|
714
|
+
# already finished. That is, find a way to implicitly
|
|
715
|
+
# determine if any existing event is in the react_after phase
|
|
716
|
+
# instead of the react_before phase.
|
|
717
|
+
def event(params = {}, &block)
|
|
718
|
+
params = { command: params } unless params.respond_to?(:merge)
|
|
719
|
+
event_params = { cause: self }
|
|
720
|
+
event_params[:name] = params.delete(:command) if params.include?(:command)
|
|
721
|
+
Inform::Event.new(event_params.merge(params), &block)
|
|
722
|
+
end
|
|
723
|
+
alias new_event event
|
|
724
|
+
alias now event
|
|
725
|
+
alias react event
|
|
726
|
+
alias respond event
|
|
727
|
+
|
|
728
|
+
def delay(delay, &block)
|
|
729
|
+
Inform::Event.new({ cause: self, delay: delay }, &block)
|
|
730
|
+
end
|
|
731
|
+
alias after_delay delay
|
|
732
|
+
alias delayed_event delay
|
|
733
|
+
|
|
734
|
+
def immediately(ctx, &block)
|
|
735
|
+
Inform::Event.new({ cause: self, context: ctx, when: :immediately }, &block)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def elementally(ctx, &block)
|
|
739
|
+
Inform::Event.new({ cause: self, context: ctx, type: :elemental, when: :immediately }, &block)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def contextualize
|
|
743
|
+
Thread.current[:event]&.contextualize(self)
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def active?
|
|
747
|
+
!events.empty?
|
|
748
|
+
end
|
|
749
|
+
alias already_active? active?
|
|
750
|
+
|
|
751
|
+
def interrupt(event_class = nil)
|
|
752
|
+
interruption = Thread.current[:event]
|
|
753
|
+
Inform::Events.each(event_class) do |event|
|
|
754
|
+
# Do not artificially terminate the source of an interruption.
|
|
755
|
+
next if event == interruption
|
|
756
|
+
|
|
757
|
+
# Parameterizing Future#cancel with false is supposed to
|
|
758
|
+
# abort the future without interrupting the executing thread.
|
|
759
|
+
# Not interrupting the event future's thread may have the
|
|
760
|
+
# consequence of a variety of data consistency issues.
|
|
761
|
+
# event.future.cancel false if event != interruption
|
|
762
|
+
|
|
763
|
+
# TODO: Test
|
|
764
|
+
event.future.cancel false
|
|
765
|
+
end
|
|
766
|
+
false
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
# module Events
|
|
770
|
+
end
|
|
771
|
+
# module Inform
|