factorix 0.5.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/completion/_factorix.bash +202 -0
- data/completion/_factorix.fish +197 -0
- data/completion/_factorix.zsh +376 -0
- data/doc/factorix.1 +377 -0
- data/exe/factorix +20 -0
- data/lib/factorix/api/category.rb +69 -0
- data/lib/factorix/api/image.rb +35 -0
- data/lib/factorix/api/license.rb +71 -0
- data/lib/factorix/api/mod_download_api.rb +66 -0
- data/lib/factorix/api/mod_info.rb +166 -0
- data/lib/factorix/api/mod_management_api.rb +237 -0
- data/lib/factorix/api/mod_portal_api.rb +204 -0
- data/lib/factorix/api/release.rb +49 -0
- data/lib/factorix/api/tag.rb +95 -0
- data/lib/factorix/api.rb +7 -0
- data/lib/factorix/api_credential.rb +54 -0
- data/lib/factorix/application.rb +218 -0
- data/lib/factorix/cache/file_system.rb +307 -0
- data/lib/factorix/cli/commands/backup_support.rb +46 -0
- data/lib/factorix/cli/commands/base.rb +90 -0
- data/lib/factorix/cli/commands/cache/evict.rb +180 -0
- data/lib/factorix/cli/commands/cache/stat.rb +201 -0
- data/lib/factorix/cli/commands/command_wrapper.rb +71 -0
- data/lib/factorix/cli/commands/completion.rb +83 -0
- data/lib/factorix/cli/commands/confirmable.rb +53 -0
- data/lib/factorix/cli/commands/download_support.rb +123 -0
- data/lib/factorix/cli/commands/launch.rb +79 -0
- data/lib/factorix/cli/commands/man.rb +29 -0
- data/lib/factorix/cli/commands/mod/check.rb +99 -0
- data/lib/factorix/cli/commands/mod/disable.rb +188 -0
- data/lib/factorix/cli/commands/mod/download.rb +291 -0
- data/lib/factorix/cli/commands/mod/edit.rb +114 -0
- data/lib/factorix/cli/commands/mod/enable.rb +216 -0
- data/lib/factorix/cli/commands/mod/image/add.rb +47 -0
- data/lib/factorix/cli/commands/mod/image/edit.rb +41 -0
- data/lib/factorix/cli/commands/mod/image/list.rb +74 -0
- data/lib/factorix/cli/commands/mod/install.rb +443 -0
- data/lib/factorix/cli/commands/mod/list.rb +372 -0
- data/lib/factorix/cli/commands/mod/search.rb +134 -0
- data/lib/factorix/cli/commands/mod/settings/dump.rb +88 -0
- data/lib/factorix/cli/commands/mod/settings/restore.rb +101 -0
- data/lib/factorix/cli/commands/mod/show.rb +202 -0
- data/lib/factorix/cli/commands/mod/sync.rb +299 -0
- data/lib/factorix/cli/commands/mod/uninstall.rb +325 -0
- data/lib/factorix/cli/commands/mod/update.rb +222 -0
- data/lib/factorix/cli/commands/mod/upload.rb +90 -0
- data/lib/factorix/cli/commands/path.rb +79 -0
- data/lib/factorix/cli/commands/requires_game_stopped.rb +32 -0
- data/lib/factorix/cli/commands/version.rb +25 -0
- data/lib/factorix/cli.rb +42 -0
- data/lib/factorix/dependency/edge.rb +89 -0
- data/lib/factorix/dependency/entry.rb +124 -0
- data/lib/factorix/dependency/graph/builder.rb +108 -0
- data/lib/factorix/dependency/graph.rb +210 -0
- data/lib/factorix/dependency/list.rb +244 -0
- data/lib/factorix/dependency/mod_version_requirement.rb +73 -0
- data/lib/factorix/dependency/node.rb +60 -0
- data/lib/factorix/dependency/parser.rb +148 -0
- data/lib/factorix/dependency/validation_result.rb +138 -0
- data/lib/factorix/dependency/validator.rb +190 -0
- data/lib/factorix/errors.rb +112 -0
- data/lib/factorix/formatting.rb +56 -0
- data/lib/factorix/game_version.rb +98 -0
- data/lib/factorix/http/cache_decorator.rb +106 -0
- data/lib/factorix/http/cached_response.rb +37 -0
- data/lib/factorix/http/client.rb +187 -0
- data/lib/factorix/http/response.rb +31 -0
- data/lib/factorix/http/retry_decorator.rb +59 -0
- data/lib/factorix/http/retry_strategy.rb +80 -0
- data/lib/factorix/info_json.rb +90 -0
- data/lib/factorix/installed_mod.rb +239 -0
- data/lib/factorix/mod.rb +55 -0
- data/lib/factorix/mod_list.rb +174 -0
- data/lib/factorix/mod_settings.rb +278 -0
- data/lib/factorix/mod_state.rb +34 -0
- data/lib/factorix/mod_version.rb +99 -0
- data/lib/factorix/portal.rb +185 -0
- data/lib/factorix/progress/download_handler.rb +46 -0
- data/lib/factorix/progress/multi_presenter.rb +45 -0
- data/lib/factorix/progress/presenter.rb +67 -0
- data/lib/factorix/progress/presenter_adapter.rb +46 -0
- data/lib/factorix/progress/scan_handler.rb +33 -0
- data/lib/factorix/progress/upload_handler.rb +33 -0
- data/lib/factorix/runtime/base.rb +233 -0
- data/lib/factorix/runtime/linux.rb +32 -0
- data/lib/factorix/runtime/mac_os.rb +53 -0
- data/lib/factorix/runtime/user_configurable.rb +69 -0
- data/lib/factorix/runtime/windows.rb +85 -0
- data/lib/factorix/runtime/wsl.rb +118 -0
- data/lib/factorix/runtime.rb +32 -0
- data/lib/factorix/save_file.rb +178 -0
- data/lib/factorix/ser_des/deserializer.rb +198 -0
- data/lib/factorix/ser_des/serializer.rb +231 -0
- data/lib/factorix/ser_des/signed_integer.rb +63 -0
- data/lib/factorix/ser_des/unsigned_integer.rb +65 -0
- data/lib/factorix/service_credential.rb +127 -0
- data/lib/factorix/transfer/downloader.rb +162 -0
- data/lib/factorix/transfer/uploader.rb +232 -0
- data/lib/factorix/version.rb +6 -0
- data/lib/factorix.rb +38 -0
- data/sig/dry/auto_inject.rbs +15 -0
- data/sig/dry/cli.rbs +19 -0
- data/sig/dry/configurable.rbs +13 -0
- data/sig/dry/core/container.rbs +17 -0
- data/sig/dry/events/publisher.rbs +22 -0
- data/sig/dry/logger.rbs +16 -0
- data/sig/factorix/api/category.rbs +15 -0
- data/sig/factorix/api/image.rbs +15 -0
- data/sig/factorix/api/license.rbs +20 -0
- data/sig/factorix/api/mod_download_api.rbs +18 -0
- data/sig/factorix/api/mod_info.rbs +67 -0
- data/sig/factorix/api/mod_management_api.rbs +25 -0
- data/sig/factorix/api/mod_portal_api.rbs +31 -0
- data/sig/factorix/api/release.rbs +27 -0
- data/sig/factorix/api/tag.rbs +15 -0
- data/sig/factorix/api.rbs +8 -0
- data/sig/factorix/api_credential.rbs +17 -0
- data/sig/factorix/application.rbs +86 -0
- data/sig/factorix/cache/file_system.rbs +35 -0
- data/sig/factorix/cli/commands/base.rbs +13 -0
- data/sig/factorix/cli/commands/cache/evict.rbs +17 -0
- data/sig/factorix/cli/commands/cache/stat.rbs +17 -0
- data/sig/factorix/cli/commands/command_wrapper.rbs +13 -0
- data/sig/factorix/cli/commands/completion/zsh.rbs +15 -0
- data/sig/factorix/cli/commands/confirmable.rbs +12 -0
- data/sig/factorix/cli/commands/download_support.rbs +12 -0
- data/sig/factorix/cli/commands/launch.rbs +15 -0
- data/sig/factorix/cli/commands/mod/check.rbs +18 -0
- data/sig/factorix/cli/commands/mod/disable.rbs +20 -0
- data/sig/factorix/cli/commands/mod/download.rbs +18 -0
- data/sig/factorix/cli/commands/mod/edit.rbs +30 -0
- data/sig/factorix/cli/commands/mod/enable.rbs +20 -0
- data/sig/factorix/cli/commands/mod/image/add.rbs +19 -0
- data/sig/factorix/cli/commands/mod/image/edit.rbs +19 -0
- data/sig/factorix/cli/commands/mod/image/list.rbs +19 -0
- data/sig/factorix/cli/commands/mod/install.rbs +19 -0
- data/sig/factorix/cli/commands/mod/list.rbs +30 -0
- data/sig/factorix/cli/commands/mod/search.rbs +18 -0
- data/sig/factorix/cli/commands/mod/settings/dump.rbs +17 -0
- data/sig/factorix/cli/commands/mod/settings/restore.rbs +17 -0
- data/sig/factorix/cli/commands/mod/sync.rbs +19 -0
- data/sig/factorix/cli/commands/mod/uninstall.rbs +20 -0
- data/sig/factorix/cli/commands/mod/update.rbs +19 -0
- data/sig/factorix/cli/commands/mod/upload.rbs +24 -0
- data/sig/factorix/cli/commands/path.rbs +18 -0
- data/sig/factorix/cli/commands/requires_game_stopped.rbs +13 -0
- data/sig/factorix/cli/commands/version.rbs +13 -0
- data/sig/factorix/cli.rbs +11 -0
- data/sig/factorix/dependency/edge.rbs +32 -0
- data/sig/factorix/dependency/entry.rbs +30 -0
- data/sig/factorix/dependency/graph/builder.rbs +17 -0
- data/sig/factorix/dependency/graph.rbs +39 -0
- data/sig/factorix/dependency/list.rbs +69 -0
- data/sig/factorix/dependency/mod_version_requirement.rbs +18 -0
- data/sig/factorix/dependency/node.rbs +24 -0
- data/sig/factorix/dependency/parser.rbs +11 -0
- data/sig/factorix/dependency/validation_result.rbs +56 -0
- data/sig/factorix/dependency/validator.rbs +13 -0
- data/sig/factorix/errors.rbs +132 -0
- data/sig/factorix/formatting.rbs +8 -0
- data/sig/factorix/game_version.rbs +24 -0
- data/sig/factorix/http/cache_decorator.rbs +64 -0
- data/sig/factorix/http/client.rbs +55 -0
- data/sig/factorix/http/response.rbs +28 -0
- data/sig/factorix/http/retry_decorator.rbs +44 -0
- data/sig/factorix/http/retry_strategy.rbs +42 -0
- data/sig/factorix/info_json.rbs +19 -0
- data/sig/factorix/installed_mod.rbs +34 -0
- data/sig/factorix/mod.rbs +20 -0
- data/sig/factorix/mod_list.rbs +44 -0
- data/sig/factorix/mod_settings.rbs +47 -0
- data/sig/factorix/mod_state.rbs +18 -0
- data/sig/factorix/mod_version.rbs +23 -0
- data/sig/factorix/portal.rbs +37 -0
- data/sig/factorix/progress/download_handler.rbs +19 -0
- data/sig/factorix/progress/multi_presenter.rbs +15 -0
- data/sig/factorix/progress/presenter.rbs +17 -0
- data/sig/factorix/progress/presenter_adapter.rbs +17 -0
- data/sig/factorix/progress/scan_handler.rbs +16 -0
- data/sig/factorix/progress/upload_handler.rbs +17 -0
- data/sig/factorix/runtime/base.rbs +45 -0
- data/sig/factorix/runtime/linux.rbs +15 -0
- data/sig/factorix/runtime/mac_os.rbs +15 -0
- data/sig/factorix/runtime/user_configurable.rbs +13 -0
- data/sig/factorix/runtime/windows.rbs +23 -0
- data/sig/factorix/runtime/wsl.rbs +19 -0
- data/sig/factorix/runtime.rbs +9 -0
- data/sig/factorix/save_file.rbs +40 -0
- data/sig/factorix/ser_des/deserializer.rbs +49 -0
- data/sig/factorix/ser_des/serializer.rbs +45 -0
- data/sig/factorix/ser_des/signed_integer.rbs +37 -0
- data/sig/factorix/ser_des/unsigned_integer.rbs +37 -0
- data/sig/factorix/service_credential.rbs +19 -0
- data/sig/factorix/transfer/downloader.rbs +15 -0
- data/sig/factorix/transfer/uploader.rbs +21 -0
- data/sig/factorix.rbs +9 -0
- data/sig/tty/progressbar.rbs +18 -0
- metadata +431 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/executor/fixed_thread_pool"
|
|
4
|
+
require "concurrent/future"
|
|
5
|
+
require "dry/events"
|
|
6
|
+
|
|
7
|
+
module Factorix
|
|
8
|
+
InstalledMOD = Data.define(:mod, :version, :form, :path, :info)
|
|
9
|
+
|
|
10
|
+
# Represents a MOD installed in the MOD directory or data directory
|
|
11
|
+
#
|
|
12
|
+
# InstalledMOD represents an actual MOD package found in either:
|
|
13
|
+
# - The MOD directory (user-installed MODs as ZIP files or directories)
|
|
14
|
+
# - The data directory (base and expansion MODs bundled with the game)
|
|
15
|
+
#
|
|
16
|
+
# This is distinct from MOD (which is just a name identifier) and
|
|
17
|
+
# MODState (which represents desired state in mod-list.json).
|
|
18
|
+
class InstalledMOD
|
|
19
|
+
# @!attribute [r] mod
|
|
20
|
+
# @return [Factorix::MOD] The MOD identifier
|
|
21
|
+
# @!attribute [r] version
|
|
22
|
+
# @return [Factorix::MODVersion] The MOD version
|
|
23
|
+
# @!attribute [r] form
|
|
24
|
+
# @return [Symbol] :zip or :directory
|
|
25
|
+
# @!attribute [r] path
|
|
26
|
+
# @return [Pathname] The path to the ZIP file or directory
|
|
27
|
+
# @!attribute [r] info
|
|
28
|
+
# @return [Factorix::InfoJSON] The parsed info.json metadata
|
|
29
|
+
|
|
30
|
+
include Comparable
|
|
31
|
+
|
|
32
|
+
# Make the class itself enumerable over all installed MODs
|
|
33
|
+
extend Enumerable
|
|
34
|
+
|
|
35
|
+
# Form constants
|
|
36
|
+
ZIP_FORM = :zip
|
|
37
|
+
public_constant :ZIP_FORM
|
|
38
|
+
DIRECTORY_FORM = :directory
|
|
39
|
+
public_constant :DIRECTORY_FORM
|
|
40
|
+
|
|
41
|
+
# Get all installed MODs
|
|
42
|
+
#
|
|
43
|
+
# @param handler [Progress::ScanHandler, nil] optional event handler for progress tracking
|
|
44
|
+
# @return [Array<InstalledMOD>] Array of all installed MODs
|
|
45
|
+
def self.all(handler: nil)
|
|
46
|
+
scanner = Scanner.new
|
|
47
|
+
scanner.subscribe(handler) if handler
|
|
48
|
+
result = scanner.scan
|
|
49
|
+
scanner.unsubscribe(handler) if handler
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Enumerate over all installed MODs
|
|
54
|
+
#
|
|
55
|
+
# @yieldparam [InstalledMOD] mod Each installed MOD
|
|
56
|
+
# @return [Enumerator, Array] Enumerator if no block given, otherwise the result of the block
|
|
57
|
+
def self.each(&) = all.each(&)
|
|
58
|
+
|
|
59
|
+
# Create InstalledMOD from a ZIP file
|
|
60
|
+
#
|
|
61
|
+
# @param path [Pathname] Path to the ZIP file
|
|
62
|
+
# @return [InstalledMOD] New InstalledMOD instance
|
|
63
|
+
# @raise [FileFormatError] if ZIP file is invalid
|
|
64
|
+
def self.from_zip(path)
|
|
65
|
+
info = InfoJSON.from_zip(path)
|
|
66
|
+
|
|
67
|
+
expected_filename = "#{info.name}_#{info.version}.zip"
|
|
68
|
+
actual_filename = path.basename.to_s
|
|
69
|
+
|
|
70
|
+
unless actual_filename == expected_filename
|
|
71
|
+
raise FileFormatError, "Filename mismatch: expected #{expected_filename}, got #{actual_filename}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
new(mod: MOD[name: info.name], version: info.version, form: ZIP_FORM, path:, info:)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Create InstalledMOD from a directory
|
|
78
|
+
#
|
|
79
|
+
# @param path [Pathname] Path to the directory
|
|
80
|
+
# @return [InstalledMOD] New InstalledMOD instance
|
|
81
|
+
# @raise [FileFormatError] if directory is invalid
|
|
82
|
+
def self.from_directory(path)
|
|
83
|
+
info_path = path + "info.json"
|
|
84
|
+
raise FileFormatError, "Missing info.json" unless info_path.file?
|
|
85
|
+
|
|
86
|
+
info = InfoJSON.from_json(info_path.read)
|
|
87
|
+
|
|
88
|
+
dirname = path.basename.to_s
|
|
89
|
+
expected_unversioned = info.name
|
|
90
|
+
expected_versioned = "#{info.name}_#{info.version}"
|
|
91
|
+
|
|
92
|
+
unless dirname == expected_unversioned || dirname == expected_versioned
|
|
93
|
+
raise FileFormatError, "Directory name mismatch: expected #{expected_unversioned} or #{expected_versioned}, got #{dirname}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
new(mod: MOD[name: info.name], version: info.version, form: DIRECTORY_FORM, path:, info:)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Scanner for finding installed MODs
|
|
100
|
+
#
|
|
101
|
+
# Scans MOD directory and data directory for installed MODs.
|
|
102
|
+
# Gets directory paths from Runtime automatically.
|
|
103
|
+
# Publishes progress events during scan.
|
|
104
|
+
class Scanner
|
|
105
|
+
include Import[:runtime, :logger]
|
|
106
|
+
include Dry::Events::Publisher[:scanner]
|
|
107
|
+
|
|
108
|
+
register_event("scan.started")
|
|
109
|
+
register_event("scan.progress")
|
|
110
|
+
register_event("scan.completed")
|
|
111
|
+
|
|
112
|
+
DEFAULT_PARALLEL_JOBS = 4
|
|
113
|
+
private_constant :DEFAULT_PARALLEL_JOBS
|
|
114
|
+
|
|
115
|
+
# Scan directories for installed MODs
|
|
116
|
+
#
|
|
117
|
+
# Scans the MOD directory for both ZIP and directory form MODs.
|
|
118
|
+
# Also scans the data directory for base/expansion MODs.
|
|
119
|
+
# Invalid packages are skipped with debug logging.
|
|
120
|
+
# Publishes scan.started, scan.progress, and scan.completed events.
|
|
121
|
+
#
|
|
122
|
+
# @return [Array<InstalledMOD>] Array of installed MODs
|
|
123
|
+
def scan
|
|
124
|
+
mod_dir = runtime.mod_dir
|
|
125
|
+
data_dir = runtime.data_dir
|
|
126
|
+
|
|
127
|
+
mod_paths = mod_dir.children.select {|path| (path.file? && path.extname == ".zip") || path.directory? }
|
|
128
|
+
data_paths = data_dir.children.select {|path|
|
|
129
|
+
next false unless path.directory?
|
|
130
|
+
|
|
131
|
+
mod_name = path.basename.to_s
|
|
132
|
+
candidate_mod = MOD[name: mod_name]
|
|
133
|
+
candidate_mod.base? || candidate_mod.expansion?
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
total = mod_paths.size + data_paths.size
|
|
137
|
+
current = 0
|
|
138
|
+
mutex = Mutex.new
|
|
139
|
+
|
|
140
|
+
publish("scan.started", total:)
|
|
141
|
+
|
|
142
|
+
pool = Concurrent::FixedThreadPool.new(DEFAULT_PARALLEL_JOBS)
|
|
143
|
+
|
|
144
|
+
begin
|
|
145
|
+
futures = mod_paths.map {|path|
|
|
146
|
+
Concurrent::Future.execute(executor: pool) do
|
|
147
|
+
result = scan_mod_path(path)
|
|
148
|
+
mutex.synchronize do
|
|
149
|
+
current += 1
|
|
150
|
+
publish("scan.progress", current:, total:)
|
|
151
|
+
end
|
|
152
|
+
result
|
|
153
|
+
end
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
installed_mods = futures.filter_map(&:value)
|
|
157
|
+
ensure
|
|
158
|
+
pool.shutdown
|
|
159
|
+
pool.wait_for_termination
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
data_paths.each do |path|
|
|
163
|
+
result = scan_mod_path(path)
|
|
164
|
+
installed_mods << result if result
|
|
165
|
+
current += 1
|
|
166
|
+
publish("scan.progress", current:, total:)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
publish("scan.completed", total: installed_mods.size)
|
|
170
|
+
|
|
171
|
+
resolved = resolve_duplicates(installed_mods)
|
|
172
|
+
resolved.sort_by(&:version).reverse
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Scan a single MOD path (ZIP file or directory)
|
|
176
|
+
#
|
|
177
|
+
# @param path [Pathname] Path to scan
|
|
178
|
+
# @return [InstalledMOD, nil] The installed MOD, or nil if invalid
|
|
179
|
+
private def scan_mod_path(path)
|
|
180
|
+
if path.file? && path.extname == ".zip"
|
|
181
|
+
InstalledMOD.from_zip(path)
|
|
182
|
+
elsif path.directory?
|
|
183
|
+
InstalledMOD.from_directory(path)
|
|
184
|
+
end
|
|
185
|
+
rescue ArgumentError => e
|
|
186
|
+
logger.debug("Skipping invalid MOD package", path: path.to_s, reason: e.message)
|
|
187
|
+
nil
|
|
188
|
+
rescue => e
|
|
189
|
+
logger.debug("Error loading MOD package", path: path.to_s, error: e.message)
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Resolve duplicate MODs (same name and version)
|
|
194
|
+
#
|
|
195
|
+
# When multiple MODs with the same name and version exist, prefer
|
|
196
|
+
# directory form over ZIP form.
|
|
197
|
+
#
|
|
198
|
+
# @param mods [Array<InstalledMOD>] Array of installed MODs
|
|
199
|
+
# @return [Array<InstalledMOD>] Array with duplicates resolved
|
|
200
|
+
private def resolve_duplicates(mods)
|
|
201
|
+
groups = mods.group_by {|mod| [mod.mod, mod.version] }
|
|
202
|
+
groups.map {|_key, group_mods| group_mods.max }
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
private_constant :Scanner
|
|
206
|
+
|
|
207
|
+
# Compare with another InstalledMOD
|
|
208
|
+
#
|
|
209
|
+
# Comparison is by version (ascending), then by form (directory > ZIP)
|
|
210
|
+
#
|
|
211
|
+
# @param other [InstalledMOD] The other InstalledMOD
|
|
212
|
+
# @return [Integer, nil] -1, 0, 1 for less than, equal to, greater than; nil if not comparable
|
|
213
|
+
def <=>(other)
|
|
214
|
+
return nil unless other.is_a?(InstalledMOD)
|
|
215
|
+
return nil unless mod == other.mod
|
|
216
|
+
|
|
217
|
+
# Compare by version (ascending), then by form priority (directory > ZIP)
|
|
218
|
+
(version <=> other.version).nonzero? || form_priority(form) <=> form_priority(other.form)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Check if this is the base MOD
|
|
222
|
+
#
|
|
223
|
+
# @return [Boolean] true if this is the base MOD
|
|
224
|
+
def base? = mod.base?
|
|
225
|
+
|
|
226
|
+
# Check if this is an expansion MOD
|
|
227
|
+
#
|
|
228
|
+
# @return [Boolean] true if this is an expansion MOD
|
|
229
|
+
def expansion? = mod.expansion?
|
|
230
|
+
|
|
231
|
+
private def form_priority(form)
|
|
232
|
+
case form
|
|
233
|
+
when DIRECTORY_FORM then 1
|
|
234
|
+
when ZIP_FORM then 0
|
|
235
|
+
else -1
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
data/lib/factorix/mod.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
MOD = Data.define(:name)
|
|
5
|
+
|
|
6
|
+
# Represents a local MOD
|
|
7
|
+
#
|
|
8
|
+
# This class encapsulates a MOD's name and provides utility methods
|
|
9
|
+
# for MOD identification and comparison.
|
|
10
|
+
class MOD
|
|
11
|
+
include Comparable
|
|
12
|
+
|
|
13
|
+
# @!attribute [r] name
|
|
14
|
+
# @return [String] the name of the MOD
|
|
15
|
+
|
|
16
|
+
# Expansion MOD names
|
|
17
|
+
EXPANSION_MODS = %w[space-age quality elevated-rails].freeze
|
|
18
|
+
private_constant :EXPANSION_MODS
|
|
19
|
+
|
|
20
|
+
# Check if this MOD is the base MOD
|
|
21
|
+
#
|
|
22
|
+
# @return [Boolean] true if this MOD is the base MOD
|
|
23
|
+
# @note The check is case-sensitive, only "base" (not "BASE" or "Base") is considered the base MOD
|
|
24
|
+
def base? = name == "base"
|
|
25
|
+
|
|
26
|
+
# Check if this MOD is an expansion MOD
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true if this MOD is an expansion MOD (space-age, quality, or elevated-rails)
|
|
29
|
+
# @note The check is case-sensitive
|
|
30
|
+
def expansion? = EXPANSION_MODS.include?(name)
|
|
31
|
+
|
|
32
|
+
# Return the name of the MOD
|
|
33
|
+
#
|
|
34
|
+
# @return [String] the name of the MOD
|
|
35
|
+
def to_s = name
|
|
36
|
+
|
|
37
|
+
# Compare this MOD with another MOD by name
|
|
38
|
+
#
|
|
39
|
+
# @param other [MOD] the other MOD
|
|
40
|
+
# @return [Integer] -1 if this MOD precedes the other, 0 if they are equal, 1 if this MOD follows the other
|
|
41
|
+
# @note Comparison is case-sensitive for MOD names.
|
|
42
|
+
# @note The base MOD (exactly "base", case-sensitive) always comes before any other MOD.
|
|
43
|
+
def <=>(other)
|
|
44
|
+
return nil unless other.is_a?(MOD)
|
|
45
|
+
|
|
46
|
+
if base?
|
|
47
|
+
other.base? ? 0 : -1
|
|
48
|
+
elsif other.base?
|
|
49
|
+
1
|
|
50
|
+
else
|
|
51
|
+
name <=> other.name
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
# Represents a list of MODs and their enabled status
|
|
7
|
+
#
|
|
8
|
+
# This class manages the mod-list.json file, which contains the list of MODs
|
|
9
|
+
# and their enabled/disabled states.
|
|
10
|
+
class MODList
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
13
|
+
# Raised when a MOD is not found in the list
|
|
14
|
+
class MODNotInListError < MODNotFoundError; end
|
|
15
|
+
|
|
16
|
+
# Load the MOD list from the given file
|
|
17
|
+
#
|
|
18
|
+
# @param path [Pathname] the path to the file to load the MOD list from (default: runtime.mod_list_path)
|
|
19
|
+
# @return [Factorix::MODList] the loaded MOD list
|
|
20
|
+
# @raise [MODSettingsError] if the base MOD is disabled
|
|
21
|
+
def self.load(path=Application[:runtime].mod_list_path)
|
|
22
|
+
raw_data = JSON.parse(path.read, symbolize_names: true)
|
|
23
|
+
mods_hash = raw_data[:mods].to_h {|entry|
|
|
24
|
+
mod = MOD[name: entry[:name]]
|
|
25
|
+
version = entry[:version] ? MODVersion.from_string(entry[:version]) : nil
|
|
26
|
+
state = MODState[enabled: entry[:enabled], version:]
|
|
27
|
+
|
|
28
|
+
# Validate that base MOD is not disabled
|
|
29
|
+
if mod.base? && !entry[:enabled]
|
|
30
|
+
raise MODSettingsError, "base MOD cannot be disabled"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
[mod, state]
|
|
34
|
+
}
|
|
35
|
+
new(mods_hash)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Initialize the MOD list
|
|
39
|
+
#
|
|
40
|
+
# @param mods [Hash{Factorix::MOD => Factorix::MODState}] the MODs and their state
|
|
41
|
+
# @return [void]
|
|
42
|
+
def initialize(mods={})
|
|
43
|
+
@mods = {}
|
|
44
|
+
mods.each do |mod, state|
|
|
45
|
+
@mods[mod] = state
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Save the MOD list to the given file
|
|
50
|
+
#
|
|
51
|
+
# @param path [Pathname] the path to the file to save the MOD list to (default: runtime.mod_list_path)
|
|
52
|
+
# @return [void]
|
|
53
|
+
def save(path=Application[:runtime].mod_list_path)
|
|
54
|
+
mods_data = @mods.map {|mod, state|
|
|
55
|
+
data = {name: mod.name, enabled: state.enabled?}
|
|
56
|
+
# Only include version in the output if it exists
|
|
57
|
+
data[:version] = state.version.to_s if state.version
|
|
58
|
+
data
|
|
59
|
+
}
|
|
60
|
+
path.write(JSON.pretty_generate({mods: mods_data}))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Iterate through all MOD-state pairs
|
|
64
|
+
#
|
|
65
|
+
# @yieldparam mod [Factorix::MOD] the MOD
|
|
66
|
+
# @yieldparam state [Factorix::MODState] the MOD state
|
|
67
|
+
# @return [Enumerator] if no block is given
|
|
68
|
+
# @return [Factorix::MODList] if a block is given
|
|
69
|
+
def each(&block)
|
|
70
|
+
return @mods.to_enum unless block
|
|
71
|
+
|
|
72
|
+
@mods.each(&block)
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Iterate through all MODs
|
|
77
|
+
#
|
|
78
|
+
# @yieldparam mod [Factorix::MOD] the MOD
|
|
79
|
+
# @return [Enumerator] if no block is given
|
|
80
|
+
# @return [Factorix::MODList] if a block is given
|
|
81
|
+
def each_mod(&block)
|
|
82
|
+
return @mods.keys.to_enum unless block
|
|
83
|
+
|
|
84
|
+
@mods.each_key(&block)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Alias for each_mod
|
|
89
|
+
#
|
|
90
|
+
# @yieldparam mod [Factorix::MOD] the MOD
|
|
91
|
+
# @return [Enumerator] if no block is given
|
|
92
|
+
# @return [Factorix::MODList] if a block is given
|
|
93
|
+
alias each_key each_mod
|
|
94
|
+
|
|
95
|
+
# Add the MOD to the list
|
|
96
|
+
#
|
|
97
|
+
# @param mod [Factorix::MOD] the MOD to add
|
|
98
|
+
# @param enabled [Boolean] the enabled status. Default to true
|
|
99
|
+
# @param version [Factorix::MODVersion, nil] the version of the MOD. Default to nil
|
|
100
|
+
# @return [void]
|
|
101
|
+
# @raise [MODSettingsError] if the MOD is the base MOD and the enabled status is false
|
|
102
|
+
def add(mod, enabled: true, version: nil)
|
|
103
|
+
raise MODSettingsError, "can't disable the base MOD" if mod.base? && enabled == false
|
|
104
|
+
|
|
105
|
+
@mods[mod] = MODState[enabled:, version:]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Remove the MOD from the list
|
|
109
|
+
#
|
|
110
|
+
# @param mod [Factorix::MOD] the MOD to remove
|
|
111
|
+
# @return [void]
|
|
112
|
+
# @raise [MODSettingsError] if the MOD is the base MOD or an expansion MOD
|
|
113
|
+
def remove(mod)
|
|
114
|
+
raise MODSettingsError, "can't remove the base MOD" if mod.base?
|
|
115
|
+
raise MODSettingsError, "can't remove expansion MOD: #{mod}" if mod.expansion?
|
|
116
|
+
|
|
117
|
+
@mods.delete(mod)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if the MOD is in the list
|
|
121
|
+
#
|
|
122
|
+
# @param mod [Factorix::MOD] the MOD to check
|
|
123
|
+
# @return [Boolean] true if the MOD is in the list, false otherwise
|
|
124
|
+
def exist?(mod) = @mods.key?(mod)
|
|
125
|
+
|
|
126
|
+
# Check if the MOD is enabled
|
|
127
|
+
#
|
|
128
|
+
# @param mod [Factorix::MOD] the MOD to check
|
|
129
|
+
# @return [Boolean] true if the MOD is enabled, false otherwise
|
|
130
|
+
# @raise [Factorix::MODList::MODNotInListError] if the MOD is not in the list
|
|
131
|
+
def enabled?(mod)
|
|
132
|
+
raise MODNotInListError, "MOD not in the list: #{mod}" unless exist?(mod)
|
|
133
|
+
|
|
134
|
+
@mods[mod].enabled?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get the version of the MOD
|
|
138
|
+
#
|
|
139
|
+
# @param mod [Factorix::MOD] the MOD to check
|
|
140
|
+
# @return [Factorix::MODVersion, nil] the version of the MOD, or nil if not specified
|
|
141
|
+
# @raise [Factorix::MODList::MODNotInListError] if the MOD is not in the list
|
|
142
|
+
def version(mod)
|
|
143
|
+
raise MODNotInListError, "MOD not in the list: #{mod}" unless exist?(mod)
|
|
144
|
+
|
|
145
|
+
@mods[mod].version
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Enable the MOD
|
|
149
|
+
#
|
|
150
|
+
# @param mod [Factorix::MOD] the MOD to enable
|
|
151
|
+
# @return [void]
|
|
152
|
+
# @raise [Factorix::MODList::MODNotInListError] if the MOD is not in the list
|
|
153
|
+
def enable(mod)
|
|
154
|
+
raise MODNotInListError, "MOD not in the list: #{mod}" unless exist?(mod)
|
|
155
|
+
|
|
156
|
+
current_state = @mods[mod]
|
|
157
|
+
@mods[mod] = MODState[enabled: true, version: current_state.version]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Disable the MOD
|
|
161
|
+
#
|
|
162
|
+
# @param mod [Factorix::MOD] the MOD to disable
|
|
163
|
+
# @return [void]
|
|
164
|
+
# @raise [MODSettingsError] if the MOD is the base MOD
|
|
165
|
+
# @raise [Factorix::MODList::MODNotInListError] if the MOD is not in the list
|
|
166
|
+
def disable(mod)
|
|
167
|
+
raise MODSettingsError, "can't disable the base MOD" if mod.base?
|
|
168
|
+
raise MODNotInListError, "MOD not in the list: #{mod}" unless exist?(mod)
|
|
169
|
+
|
|
170
|
+
current_state = @mods[mod]
|
|
171
|
+
@mods[mod] = MODState[enabled: false, version: current_state.version]
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|