story-teller 1.1.3
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 +188 -0
- data/Rakefile +58 -0
- data/config/database.yml +37 -0
- data/exe/inform.rb +6 -0
- data/game/config.yml +5 -0
- data/game/example.inf +90 -0
- data/game/example.rb +105 -0
- data/game/forms/example_form.rb +2 -0
- data/game/grammar/admin.inf.rb +185 -0
- data/game/grammar/builder.inf.rb +310 -0
- data/game/grammar/game_grammar.inf.rb +6 -0
- data/game/grammar/meta.inf.rb +41 -0
- data/game/languages/english.rb +571 -0
- data/game/models/example_model.rb +2 -0
- data/game/modules/example_module.rb +9 -0
- data/game/modules/parser_extensions.rb +264 -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 +35 -0
- data/game/verbs/metaverbs.rb +2066 -0
- data/lib/story_teller/application.rb +82 -0
- data/lib/story_teller/cli.rb +35 -0
- data/lib/story_teller/color.rb +144 -0
- data/lib/story_teller/config.rb +61 -0
- data/lib/story_teller/curses_adapter.rb +30 -0
- data/lib/story_teller/database.rb +527 -0
- data/lib/story_teller/game/loader.rb +276 -0
- data/lib/story_teller/game.rb +22 -0
- data/lib/story_teller/inform/models.rb +42 -0
- data/lib/story_teller/inform/relational/link.rb +239 -0
- data/lib/story_teller/inform/relational/module.rb +203 -0
- data/lib/story_teller/inform/relational/object.rb +546 -0
- data/lib/story_teller/inform/relational/tag.rb +152 -0
- data/lib/story_teller/options.rb +151 -0
- data/lib/story_teller/persistence.rb +340 -0
- data/lib/story_teller/player_character.rb +99 -0
- data/lib/story_teller/privileges.rb +55 -0
- data/lib/story_teller/runtime.rb +381 -0
- data/lib/story_teller/snapshots.rb +412 -0
- data/lib/story_teller/terminal.rb +58 -0
- data/lib/story_teller/version.rb +24 -0
- data/lib/story_teller_cli.rb +34 -0
- metadata +158 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# lib/story_teller/options.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 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
|
+
require 'logger'
|
|
23
|
+
require 'optparse'
|
|
24
|
+
|
|
25
|
+
require_relative 'version'
|
|
26
|
+
|
|
27
|
+
# The StoryTeller module
|
|
28
|
+
module StoryTeller
|
|
29
|
+
class UserError < StandardError; end
|
|
30
|
+
|
|
31
|
+
# The Options module
|
|
32
|
+
module Options
|
|
33
|
+
# The ArgumentsParser
|
|
34
|
+
class ArgumentsParser
|
|
35
|
+
attr_reader :parser, :options
|
|
36
|
+
|
|
37
|
+
def initialize(args, defaults = {}, option_parser = OptionParser.new, **params)
|
|
38
|
+
@args = args
|
|
39
|
+
@parser = option_parser
|
|
40
|
+
@options = defaults
|
|
41
|
+
flags.each { |method_name| self.method(method_name).call }
|
|
42
|
+
@parser.parse!(args, **params)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def flags
|
|
46
|
+
@flags ||= %i[
|
|
47
|
+
banner game_path admin builder persist word_wrap log_level
|
|
48
|
+
help version]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# rubocop: disable Metrics/MethodLength
|
|
52
|
+
def banner
|
|
53
|
+
@parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [game_path] [options]"
|
|
54
|
+
@parser.separator ''
|
|
55
|
+
@parser.separator 'Arguments:'
|
|
56
|
+
@parser.separator ' game_path Path to game file or directory'
|
|
57
|
+
@parser.separator(
|
|
58
|
+
format(
|
|
59
|
+
' Default: %<game_path>s',
|
|
60
|
+
game_path: @options[:game_path]
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
@parser.separator ''
|
|
64
|
+
@parser.separator 'Options:'
|
|
65
|
+
end
|
|
66
|
+
# rubocop: enable Metrics/MethodLength
|
|
67
|
+
|
|
68
|
+
def game_path
|
|
69
|
+
first = @args.first
|
|
70
|
+
return if first.nil?
|
|
71
|
+
|
|
72
|
+
path = File.expand_path(first)
|
|
73
|
+
|
|
74
|
+
# Only treat it as game_path if it exists
|
|
75
|
+
return unless File.exist?(path)
|
|
76
|
+
|
|
77
|
+
@options[:game_path] = path
|
|
78
|
+
@args.shift
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def admin
|
|
82
|
+
@parser.on_tail('--admin', 'Set player character as admin; default: false') do
|
|
83
|
+
@options[:admin] = true
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def builder
|
|
88
|
+
@parser.on_tail('--builder', 'Set player character as builder; default: false') do
|
|
89
|
+
@options[:builder] = true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def persist
|
|
94
|
+
desc = 'Persist the world tree across sessions; default: false'
|
|
95
|
+
@parser.on_tail('--persist', desc) do
|
|
96
|
+
@options[:persist] = true
|
|
97
|
+
@options[:reset_db_each_session] = false
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def word_wrap
|
|
102
|
+
@parser.on('--word-wrap COLUMNS', Integer, 'Set default line width') do |columns|
|
|
103
|
+
@options[:word_wrap] = columns
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def log_level
|
|
108
|
+
@parser.on_tail('-v', '--verbose', 'Increase verbosity') do
|
|
109
|
+
@options[:log_level] ||= Logger::INFO
|
|
110
|
+
@options[:log_level] -= 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def help
|
|
115
|
+
@parser.on_tail('-?', '--help', 'Show this message') do
|
|
116
|
+
puts @parser
|
|
117
|
+
exit
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def version
|
|
122
|
+
@parser.on_tail('--version', 'Show version') do
|
|
123
|
+
puts "#{$PROGRAM_NAME} version #{StoryTellerCli::VERSION}"
|
|
124
|
+
exit
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
# class ArgumentsParser
|
|
129
|
+
|
|
130
|
+
def demand(options, arg, positional = false)
|
|
131
|
+
return options[arg] unless options[arg].nil?
|
|
132
|
+
required_arg = positional ? "<#{arg}>" : "--#{arg.to_s.gsub(/_/, '-')}"
|
|
133
|
+
raise UserError, "Required argument: #{required_arg}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def parse_arguments(args: ARGV, defaults: {}, _file_path: ARGF)
|
|
137
|
+
arguments_parser = ArgumentsParser.new(args, defaults)
|
|
138
|
+
demand(arguments_parser.options, :game_path)
|
|
139
|
+
arguments_parser.options
|
|
140
|
+
rescue OptionParser::InvalidArgument, OptionParser::InvalidOption,
|
|
141
|
+
OptionParser::MissingArgument, OptionParser::NeedlessArgument => e
|
|
142
|
+
puts e.message
|
|
143
|
+
puts arguments_parser.parser
|
|
144
|
+
exit
|
|
145
|
+
rescue OptionParser::AmbiguousOption => e
|
|
146
|
+
abort e.message
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
# module Options
|
|
150
|
+
end
|
|
151
|
+
# module StoryTeller
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# lib/story_teller/persistence.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 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
|
+
require 'etc'
|
|
23
|
+
require 'logger'
|
|
24
|
+
require 'uri'
|
|
25
|
+
require 'yaml'
|
|
26
|
+
|
|
27
|
+
require 'sequel'
|
|
28
|
+
|
|
29
|
+
require_relative 'database'
|
|
30
|
+
require_relative 'snapshots'
|
|
31
|
+
|
|
32
|
+
Sequel.extension :migration
|
|
33
|
+
Sequel.extension :connection_validator
|
|
34
|
+
|
|
35
|
+
# Simplistic in-memory cache
|
|
36
|
+
class EphemeralCache < Hash
|
|
37
|
+
def get(key)
|
|
38
|
+
self[key]
|
|
39
|
+
end
|
|
40
|
+
def set(key, value, _ttl)
|
|
41
|
+
self[key] = value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The Persistence class
|
|
46
|
+
class Persistence
|
|
47
|
+
unless defined?(Persistence::GlobalCache)
|
|
48
|
+
GlobalCache = defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : EphemeralCache.new
|
|
49
|
+
end
|
|
50
|
+
Plugins = {
|
|
51
|
+
after_initialize: [],
|
|
52
|
+
caching: [Persistence::GlobalCache],
|
|
53
|
+
json_serializer: [],
|
|
54
|
+
xml_serializer: []
|
|
55
|
+
# tactical_eager_loading: [],
|
|
56
|
+
# eager_each: []
|
|
57
|
+
}.freeze
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The SequelLoggers module
|
|
61
|
+
module SequelLoggers
|
|
62
|
+
DatabaseLogging = Struct.new(:memo).new({})
|
|
63
|
+
|
|
64
|
+
def add_logger(database, logger)
|
|
65
|
+
database.loggers << logger unless database.loggers.include?(logger)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def delete_logger(database, logger)
|
|
69
|
+
database.loggers.delete(logger) if database.loggers.include?(logger)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def set_log_level(database, level)
|
|
73
|
+
DatabaseLogging.memo[:preserved_log_levels][database] = database.sql_log_level
|
|
74
|
+
database.sql_log_level = level unless level.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def enable_query_logging
|
|
78
|
+
DatabaseLogging.memo[:logger] ||= Logger.new($stdout)
|
|
79
|
+
DatabaseLogging.memo[:preserved_log_levels] ||= {}
|
|
80
|
+
logger = DatabaseLogging.memo[:logger]
|
|
81
|
+
Sequel::DATABASES.each do |database|
|
|
82
|
+
add_logger(database, logger)
|
|
83
|
+
set_log_level(database, :debug)
|
|
84
|
+
logger.debug "Enabled query logging for #{database}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def disable_query_logging
|
|
89
|
+
logger = DatabaseLogging.memo[:logger]
|
|
90
|
+
Sequel::DATABASES.each do |database|
|
|
91
|
+
logger.debug "Disabling query logging for #{database}"
|
|
92
|
+
delete_logger(database, logger)
|
|
93
|
+
set_log_level(database, DatabaseLogging.memo[:preserved_log_levels][database])
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
# module SequelLoggers
|
|
98
|
+
|
|
99
|
+
# The SequelPlugins module
|
|
100
|
+
module SequelPlugins
|
|
101
|
+
def enable_plugins
|
|
102
|
+
Persistence::Plugins.each { |plugin, parameters| enable_plugin(plugin, parameters) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def enable_plugin(plugin, parameters = [])
|
|
106
|
+
return Sequel::Model.plugin(plugin) if parameters.empty?
|
|
107
|
+
Sequel::Model.plugin(plugin, *parameters)
|
|
108
|
+
rescue LoadError => e
|
|
109
|
+
log.error e.message
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
# module SequelPlugins
|
|
113
|
+
|
|
114
|
+
# module DatabaseResetHelpers
|
|
115
|
+
module DatabaseResetHelpers
|
|
116
|
+
def reset!
|
|
117
|
+
db = Sequel::Model.db
|
|
118
|
+
tables = world_tables(db)
|
|
119
|
+
return if tables.empty?
|
|
120
|
+
|
|
121
|
+
db.from(*tables).truncate(cascade: true, restart: true)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def world_tree?
|
|
125
|
+
db = Sequel::Model.db
|
|
126
|
+
return false unless db.table_exists?(:object)
|
|
127
|
+
|
|
128
|
+
Inform::Object.dataset.any?
|
|
129
|
+
rescue Sequel::Error
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def world_tables(db)
|
|
134
|
+
model_classes = Sequel::Model.descendants
|
|
135
|
+
tables = model_classes.filter { |model| concrete_world_model?(db, model) }
|
|
136
|
+
tables.map(&:table_name).uniq
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def concrete_world_model?(db, model)
|
|
140
|
+
table_name = model.table_name
|
|
141
|
+
table_name && db.table_exists?(table_name)
|
|
142
|
+
rescue Sequel::Error
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def without_committing!(&block)
|
|
147
|
+
Sequel::Model.db.transaction(rollback: :always, auto_savepoint: true, &block)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def world_object_ids
|
|
151
|
+
return Set.new unless Sequel::Model.db.table_exists?(:object)
|
|
152
|
+
|
|
153
|
+
Inform::Object.select_map(:id).to_set
|
|
154
|
+
rescue Sequel::Error
|
|
155
|
+
Set.new
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The StoryTeller module
|
|
160
|
+
module StoryTeller
|
|
161
|
+
# The Persistence class
|
|
162
|
+
class Persistence
|
|
163
|
+
include DatabaseConnectionHelpers
|
|
164
|
+
include DatabaseResetHelpers
|
|
165
|
+
include SequelLoggers
|
|
166
|
+
include SequelPlugins
|
|
167
|
+
|
|
168
|
+
attr_reader :config, :environment
|
|
169
|
+
|
|
170
|
+
DEFAULT_DATABASE_NAME = 'default'.freeze
|
|
171
|
+
|
|
172
|
+
def self.init(env = nil, database_name = DEFAULT_DATABASE_NAME)
|
|
173
|
+
instance(env, database_name)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
@instance_mutex = Mutex.new
|
|
177
|
+
|
|
178
|
+
def self.instance(*args)
|
|
179
|
+
return @instance unless @instance.nil?
|
|
180
|
+
@instance_mutex.synchronize do
|
|
181
|
+
@instance ||= new(*args)
|
|
182
|
+
end
|
|
183
|
+
@instance
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private_class_method :new
|
|
187
|
+
|
|
188
|
+
def initialize(env = nil, database_name = DEFAULT_DATABASE_NAME)
|
|
189
|
+
@environment = env
|
|
190
|
+
@database_name = database_name
|
|
191
|
+
log.debug "Initializing persistence layer for environment: #{@environment}"
|
|
192
|
+
caller[0..4].each { |t| log.trace t }
|
|
193
|
+
@config = database_config.fetch(@environment.to_s, {})
|
|
194
|
+
establish_database_connection
|
|
195
|
+
enable_plugins
|
|
196
|
+
enable_query_logging if ENV['ENABLE_SQL_LOGGING']
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def establish_database_connection
|
|
200
|
+
connect
|
|
201
|
+
rescue Sequel::DatabaseConnectionError
|
|
202
|
+
log.warn "Database requires initialization"
|
|
203
|
+
init_database(@database_name)
|
|
204
|
+
retry if DatabaseConnectionHelpers.connection_attempts < 2
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def database_config
|
|
208
|
+
@database_config ||= YAML.load_file(database_config_file_path)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def database_config_file_path
|
|
212
|
+
@database_config_file_path ||= File.expand_path(
|
|
213
|
+
File.join(StoryTeller::Runtime.project_dir_path, 'config', 'database.yml')
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def init_database(database_name)
|
|
218
|
+
@config = database_config.fetch('default')
|
|
219
|
+
connect
|
|
220
|
+
@database = Greenfield::Database.init(database_name)
|
|
221
|
+
@database.bootstrap.up
|
|
222
|
+
@config = database_config.fetch(environment.to_s)
|
|
223
|
+
connect
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
# class Persistence
|
|
227
|
+
end
|
|
228
|
+
# module StoryTeller
|
|
229
|
+
|
|
230
|
+
# The StoryTeller module
|
|
231
|
+
module StoryTeller
|
|
232
|
+
# The ImplicitMigration module
|
|
233
|
+
module ImplicitMigration
|
|
234
|
+
NoDatabasePattern = %r{No database associated with Sequel::Model}.freeze
|
|
235
|
+
MigrationSetupTemplate = '%<model>sSetup'.freeze
|
|
236
|
+
ModuleNamespaceDelimiterPattern = /::/.freeze
|
|
237
|
+
|
|
238
|
+
def before_inherited(subclass)
|
|
239
|
+
return if self != Sequel::Model
|
|
240
|
+
descendants << subclass
|
|
241
|
+
log.debug "#{subclass} << #{self} [#{descendants}]"
|
|
242
|
+
maybe_migrate(subclass)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def after_inherited(_subclass)
|
|
246
|
+
examine_schema
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# rubocop: disable Metrics/AbcSize
|
|
250
|
+
# rubocop: disable Metrics/MethodLength
|
|
251
|
+
def examine_schema
|
|
252
|
+
descendants.each do |model_class|
|
|
253
|
+
table_name = model_class.table_name
|
|
254
|
+
indexes = self.db.indexes(table_name)
|
|
255
|
+
columns = model_class.columns
|
|
256
|
+
associations = model_class.associations
|
|
257
|
+
|
|
258
|
+
log.debug "Table: #{table_name}"
|
|
259
|
+
log.debug "Columns: #{columns.join(', ')}"
|
|
260
|
+
log.debug "Indexes: #{indexes}"
|
|
261
|
+
|
|
262
|
+
associations.each do |assoc_name, assoc_data|
|
|
263
|
+
log.debug "Association: #{assoc_name} (#{assoc_data[:type]}) to #{assoc_data[:class_name]}"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
log.debug "==========="
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
# rubocop: enable Metrics/AbcSize
|
|
270
|
+
# rubocop: enable Metrics/MethodLength
|
|
271
|
+
|
|
272
|
+
def maybe_migrate(subclass)
|
|
273
|
+
migration_class = migration(subclass)
|
|
274
|
+
log.debug "Found migration class: #{migration_class}"
|
|
275
|
+
migration_class&.up
|
|
276
|
+
rescue Sequel::Error => e
|
|
277
|
+
if NoDatabasePattern.match?(e.message)
|
|
278
|
+
StoryTeller::Persistence.instance.connect
|
|
279
|
+
retry
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def migration(model)
|
|
284
|
+
names = format(MigrationSetupTemplate, model: model).split(ModuleNamespaceDelimiterPattern)
|
|
285
|
+
names.inject(Object) do |mod, class_name|
|
|
286
|
+
mod.const_get(class_name)
|
|
287
|
+
rescue StandardError => e
|
|
288
|
+
log.warn "Error getting reference to migration model class: #{e.message}"
|
|
289
|
+
next
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
# module ImplicitMigration
|
|
294
|
+
end
|
|
295
|
+
# module StoryTeller
|
|
296
|
+
|
|
297
|
+
# The StoryTeller module
|
|
298
|
+
module StoryTeller
|
|
299
|
+
# The InheritanceListener module
|
|
300
|
+
module InheritanceListener
|
|
301
|
+
# The ClassMethods module
|
|
302
|
+
module ClassMethods
|
|
303
|
+
include StoryTeller::ImplicitMigration
|
|
304
|
+
|
|
305
|
+
def inherited(subclass)
|
|
306
|
+
before_inherited(subclass)
|
|
307
|
+
super
|
|
308
|
+
after_inherited(subclass)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Returns the list of Model descendants.
|
|
312
|
+
def descendants
|
|
313
|
+
@descendants ||= []
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def self.included(base)
|
|
318
|
+
base.extend(ClassMethods)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
# module InheritanceListener
|
|
322
|
+
end
|
|
323
|
+
# module StoryTeller
|
|
324
|
+
|
|
325
|
+
# The Sequel module
|
|
326
|
+
module Sequel
|
|
327
|
+
# The Sequel::Model class
|
|
328
|
+
class Model
|
|
329
|
+
include StoryTeller::InheritanceListener
|
|
330
|
+
|
|
331
|
+
def self.implicit_table_name
|
|
332
|
+
underscore(demodulize(name)).to_sym
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
StoryTeller::Persistence.init(
|
|
338
|
+
StoryTeller::Runtime.instance.game.environment,
|
|
339
|
+
StoryTeller::Runtime.instance.game.name
|
|
340
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# lib/story_teller/player_character.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 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
|
+
# module StoryTeller
|
|
23
|
+
module StoryTeller
|
|
24
|
+
def self.PlayerCharacter(name)
|
|
25
|
+
Object(name, Character)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# class Character
|
|
29
|
+
class Character < Inform::Object
|
|
30
|
+
def initialize(name)
|
|
31
|
+
super
|
|
32
|
+
capacity 100
|
|
33
|
+
parse_name 0
|
|
34
|
+
orders 0
|
|
35
|
+
has :concealed, :animate, :proper, :transparent
|
|
36
|
+
end
|
|
37
|
+
def short_name; return L__M(:Miscellany, 18); end
|
|
38
|
+
def description; return L__M(:Miscellany, 19); end
|
|
39
|
+
def before; nil; end
|
|
40
|
+
def after; nil; end
|
|
41
|
+
def life; nil; end
|
|
42
|
+
def each_turn; nil; end
|
|
43
|
+
def time_out; nil; end
|
|
44
|
+
def describe; nil; end
|
|
45
|
+
def add_to_scope; nil; end
|
|
46
|
+
def number; 0; end
|
|
47
|
+
def before_implicit; nil; end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# module StoryTeller
|
|
52
|
+
module StoryTeller
|
|
53
|
+
# module PersistedStartupLocation
|
|
54
|
+
module PersistedStartupLocation
|
|
55
|
+
Key = :story_teller_persisted_startup_location
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
def with(location)
|
|
60
|
+
previous = Thread.current[Key]
|
|
61
|
+
Thread.current[Key] = location
|
|
62
|
+
yield
|
|
63
|
+
ensure
|
|
64
|
+
Thread.current[Key] = previous
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def peek
|
|
68
|
+
Thread.current[Key]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def clear
|
|
72
|
+
Thread.current[Key] = nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# module StoryTeller
|
|
78
|
+
module StoryTeller
|
|
79
|
+
# module PersistedStartupMove
|
|
80
|
+
module PersistedStartupMove
|
|
81
|
+
def move(object, destination)
|
|
82
|
+
persisted_destination = StoryTeller::PersistedStartupLocation.peek
|
|
83
|
+
|
|
84
|
+
if persisted_destination && startup_player_move?(object)
|
|
85
|
+
destination = persisted_destination
|
|
86
|
+
@location = persisted_destination
|
|
87
|
+
StoryTeller::PersistedStartupLocation.clear
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def startup_player_move?(object)
|
|
96
|
+
object == @player || object == selfobj || object == StoryTeller::Engine.player_object
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# lib/story_teller/privileges.rb
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
# frozen_string_literal: false
|
|
4
|
+
|
|
5
|
+
# Copyright Nels Nelson 2008-2025 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
|
+
# module StoryTeller
|
|
23
|
+
module StoryTeller
|
|
24
|
+
# class InvocationIdentity
|
|
25
|
+
class InvocationIdentity
|
|
26
|
+
include StoryTeller::PrivilegedIdentity
|
|
27
|
+
|
|
28
|
+
attr_reader :runtime
|
|
29
|
+
|
|
30
|
+
def initialize(runtime)
|
|
31
|
+
@runtime = runtime
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def identity
|
|
35
|
+
"runtime:#{runtime.identity}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def privilege_label
|
|
39
|
+
'this runtime session'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def name
|
|
43
|
+
privilege_label
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def privileged?(privilege)
|
|
47
|
+
runtime.privileged?(privilege)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
privilege_label
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
# module StoryTeller
|