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,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tint_me"
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module MOD
|
|
9
|
+
# Show detailed MOD information from portal
|
|
10
|
+
class Show < Base
|
|
11
|
+
# Style for MOD title (bold + underline)
|
|
12
|
+
TITLE_STYLE = TIntMe[:bold, :underline]
|
|
13
|
+
private_constant :TITLE_STYLE
|
|
14
|
+
|
|
15
|
+
# Style for section headers (bold)
|
|
16
|
+
HEADER_STYLE = TIntMe[:bold]
|
|
17
|
+
private_constant :HEADER_STYLE
|
|
18
|
+
|
|
19
|
+
# Style for incompatible MODs (red)
|
|
20
|
+
INCOMPATIBLE_MOD_STYLE = TIntMe[:red]
|
|
21
|
+
private_constant :INCOMPATIBLE_MOD_STYLE
|
|
22
|
+
# @!parse
|
|
23
|
+
# # @return [Portal]
|
|
24
|
+
# attr_reader :portal
|
|
25
|
+
# # @return [Runtime]
|
|
26
|
+
# attr_reader :runtime
|
|
27
|
+
include Import[:portal, :runtime]
|
|
28
|
+
|
|
29
|
+
desc "Show MOD details from Factorio MOD Portal"
|
|
30
|
+
|
|
31
|
+
example [
|
|
32
|
+
"some-mod # Show details for some-mod"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
argument :mod_name, type: :string, required: true, desc: "MOD name to show"
|
|
36
|
+
|
|
37
|
+
# Execute the show command
|
|
38
|
+
#
|
|
39
|
+
# @param mod_name [String] MOD name to show details for
|
|
40
|
+
# @return [void]
|
|
41
|
+
# @raise [BundledMODError] if mod_name is base or an expansion MOD
|
|
42
|
+
def call(mod_name:, **)
|
|
43
|
+
mod = Factorix::MOD[mod_name]
|
|
44
|
+
raise BundledMODError, "Cannot show base MOD" if mod.base?
|
|
45
|
+
raise BundledMODError, "Cannot show expansion MOD: #{mod_name}" if mod.expansion?
|
|
46
|
+
|
|
47
|
+
mod_info = portal.get_mod_full(mod_name)
|
|
48
|
+
local_status = fetch_local_status(mod_name)
|
|
49
|
+
|
|
50
|
+
display_header(mod_info)
|
|
51
|
+
display_basic_info(mod_info, local_status)
|
|
52
|
+
display_links(mod_info)
|
|
53
|
+
display_dependencies(mod_info)
|
|
54
|
+
display_incompatibilities(mod_info)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private def fetch_local_status(mod_name)
|
|
58
|
+
mod_list = MODList.load
|
|
59
|
+
mod = Factorix::MOD[mod_name]
|
|
60
|
+
installed_mod = find_installed_mod(mod_name)
|
|
61
|
+
|
|
62
|
+
enabled = mod_list.exist?(mod) && mod_list.enabled?(mod)
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
installed: !installed_mod.nil?,
|
|
66
|
+
enabled:,
|
|
67
|
+
local_version: installed_mod&.version
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private def find_installed_mod(mod_name)
|
|
72
|
+
InstalledMOD.all.find {|m| m.mod.name == mod_name }
|
|
73
|
+
rescue
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private def display_header(mod_info)
|
|
78
|
+
puts TITLE_STYLE[mod_info.title]
|
|
79
|
+
puts
|
|
80
|
+
puts mod_info.summary unless mod_info.summary.empty?
|
|
81
|
+
puts
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private def display_basic_info(mod_info, local_status)
|
|
85
|
+
latest_release = mod_info.latest_release || mod_info.releases.max_by(&:version)
|
|
86
|
+
factorio_version = latest_release&.info_json&.dig(:factorio_version)
|
|
87
|
+
|
|
88
|
+
rows = []
|
|
89
|
+
rows << ["Status", format_status(local_status)]
|
|
90
|
+
rows << ["Version", latest_release&.version&.to_s || "N/A"]
|
|
91
|
+
if local_status[:installed] && local_status[:local_version]
|
|
92
|
+
local_ver = local_status[:local_version].to_s
|
|
93
|
+
latest_ver = latest_release&.version&.to_s
|
|
94
|
+
if latest_ver && local_ver != latest_ver
|
|
95
|
+
rows << ["Installed Version", "#{local_ver} (update available)"]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
rows << ["Author", mod_info.owner]
|
|
99
|
+
rows << ["Category", mod_info.category.name]
|
|
100
|
+
rows << ["License", format_license(mod_info)]
|
|
101
|
+
rows << ["Factorio Version", factorio_version || "N/A"]
|
|
102
|
+
rows << ["Downloads", mod_info.downloads_count.to_s]
|
|
103
|
+
|
|
104
|
+
max_label_width = rows.map {|label, _| label.length }.max
|
|
105
|
+
rows.each do |label, value|
|
|
106
|
+
puts "#{label.ljust(max_label_width)} #{value}"
|
|
107
|
+
end
|
|
108
|
+
puts
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private def format_status(local_status)
|
|
112
|
+
if local_status[:installed]
|
|
113
|
+
local_status[:enabled] ? "Enabled" : "Disabled"
|
|
114
|
+
else
|
|
115
|
+
"Not installed"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private def format_license(mod_info)
|
|
120
|
+
return "N/A" unless mod_info.detail&.license
|
|
121
|
+
|
|
122
|
+
mod_info.detail.license.title
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private def display_links(mod_info)
|
|
126
|
+
puts HEADER_STYLE["Links"]
|
|
127
|
+
puts " MOD Portal: https://mods.factorio.com/mod/#{mod_info.name}"
|
|
128
|
+
|
|
129
|
+
if mod_info.detail
|
|
130
|
+
if mod_info.detail.source_url
|
|
131
|
+
puts " Source: #{mod_info.detail.source_url}"
|
|
132
|
+
end
|
|
133
|
+
if mod_info.detail.homepage
|
|
134
|
+
puts " Homepage: #{mod_info.detail.homepage}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
puts
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private def display_dependencies(mod_info)
|
|
141
|
+
latest_release = mod_info.latest_release || mod_info.releases.max_by(&:version)
|
|
142
|
+
return unless latest_release
|
|
143
|
+
|
|
144
|
+
dependencies = latest_release.info_json[:dependencies] || []
|
|
145
|
+
return if dependencies.empty?
|
|
146
|
+
|
|
147
|
+
parsed = dependencies.filter_map {|dep_str| parse_dependency(dep_str) }
|
|
148
|
+
required = parsed.select {|d| d[:type] == :required }
|
|
149
|
+
optional = parsed.select {|d| d[:type] == :optional }
|
|
150
|
+
|
|
151
|
+
unless required.empty?
|
|
152
|
+
puts HEADER_STYLE["Dependencies"]
|
|
153
|
+
required.each {|dep| display_dependency(dep) }
|
|
154
|
+
puts
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
return if optional.empty?
|
|
158
|
+
|
|
159
|
+
puts HEADER_STYLE["Optional Dependencies"]
|
|
160
|
+
optional.each {|dep| display_dependency(dep) }
|
|
161
|
+
puts
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private def parse_dependency(dep_str)
|
|
165
|
+
# Handle prefixes: ! (incompatible), ? (optional), (?) (hidden optional), ~ (load neutral)
|
|
166
|
+
case dep_str
|
|
167
|
+
when /\A!\s*(.+)/
|
|
168
|
+
{type: :incompatible, spec: ::Regexp.last_match(1).strip}
|
|
169
|
+
when /\A\(\?\)\s*(.+)/
|
|
170
|
+
{type: :hidden_optional, spec: ::Regexp.last_match(1).strip}
|
|
171
|
+
when /\A\?\s*(.+)/
|
|
172
|
+
{type: :optional, spec: ::Regexp.last_match(1).strip}
|
|
173
|
+
when /\A~\s*(.+)/
|
|
174
|
+
{type: :load_neutral, spec: ::Regexp.last_match(1).strip}
|
|
175
|
+
else
|
|
176
|
+
{type: :required, spec: dep_str.strip}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private def display_dependency(dep)
|
|
181
|
+
puts " #{dep[:spec]}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private def display_incompatibilities(mod_info)
|
|
185
|
+
latest_release = mod_info.latest_release || mod_info.releases.max_by(&:version)
|
|
186
|
+
return unless latest_release
|
|
187
|
+
|
|
188
|
+
dependencies = latest_release.info_json[:dependencies] || []
|
|
189
|
+
parsed = dependencies.filter_map {|dep_str| parse_dependency(dep_str) }
|
|
190
|
+
incompatible = parsed.select {|d| d[:type] == :incompatible }
|
|
191
|
+
|
|
192
|
+
return if incompatible.empty?
|
|
193
|
+
|
|
194
|
+
puts HEADER_STYLE["Incompatibilities"]
|
|
195
|
+
incompatible.each {|dep| puts " #{INCOMPATIBLE_MOD_STYLE[dep[:spec]]}" }
|
|
196
|
+
puts
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/executor/fixed_thread_pool"
|
|
4
|
+
require "concurrent/future"
|
|
5
|
+
|
|
6
|
+
module Factorix
|
|
7
|
+
class CLI
|
|
8
|
+
module Commands
|
|
9
|
+
module MOD
|
|
10
|
+
# Sync MOD states and startup settings from a save file
|
|
11
|
+
class Sync < Base
|
|
12
|
+
confirmable!
|
|
13
|
+
require_game_stopped!
|
|
14
|
+
backup_support!
|
|
15
|
+
|
|
16
|
+
include DownloadSupport
|
|
17
|
+
|
|
18
|
+
# @!parse
|
|
19
|
+
# # @return [Portal]
|
|
20
|
+
# attr_reader :portal
|
|
21
|
+
# # @return [Dry::Logger::Dispatcher]
|
|
22
|
+
# attr_reader :logger
|
|
23
|
+
# # @return [Factorix::Runtime]
|
|
24
|
+
# attr_reader :runtime
|
|
25
|
+
include Import[:portal, :logger, :runtime]
|
|
26
|
+
|
|
27
|
+
desc "Sync MOD states and startup settings from a save file"
|
|
28
|
+
|
|
29
|
+
example [
|
|
30
|
+
"save.zip # Sync MOD(s) from save file",
|
|
31
|
+
"-j 8 save.zip # Use 8 parallel downloads"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
argument :save_file, type: :string, required: true, desc: "Path to Factorio save file (.zip)"
|
|
35
|
+
option :jobs, type: :integer, aliases: ["-j"], default: 4, desc: "Number of parallel downloads"
|
|
36
|
+
|
|
37
|
+
# Execute the sync command
|
|
38
|
+
#
|
|
39
|
+
# @param save_file [String] Path to save file
|
|
40
|
+
# @param jobs [Integer] Number of parallel downloads
|
|
41
|
+
# @return [void]
|
|
42
|
+
def call(save_file:, jobs: 4, **)
|
|
43
|
+
# Load save file
|
|
44
|
+
say "Loading save file: #{save_file}", prefix: :info
|
|
45
|
+
save_data = SaveFile.load(Pathname(save_file))
|
|
46
|
+
say "Loaded save file (version: #{save_data.version}, MOD(s): #{save_data.mods.size})", prefix: :info
|
|
47
|
+
|
|
48
|
+
# Load current state
|
|
49
|
+
mod_list = MODList.load
|
|
50
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: $stderr)
|
|
51
|
+
handler = Progress::ScanHandler.new(presenter)
|
|
52
|
+
installed_mods = InstalledMOD.all(handler:)
|
|
53
|
+
graph = Dependency::Graph::Builder.build(installed_mods:, mod_list:)
|
|
54
|
+
|
|
55
|
+
raise DirectoryNotFoundError, "MOD directory does not exist: #{runtime.mod_dir}" unless runtime.mod_dir.exist?
|
|
56
|
+
|
|
57
|
+
# Find MODs that need to be installed
|
|
58
|
+
mods_to_install = find_mods_to_install(save_data.mods, installed_mods)
|
|
59
|
+
|
|
60
|
+
if mods_to_install.any?
|
|
61
|
+
say "#{mods_to_install.size} MOD(s) need to be installed", prefix: :info
|
|
62
|
+
|
|
63
|
+
# Plan installation
|
|
64
|
+
install_targets = plan_installation(mods_to_install, graph, jobs)
|
|
65
|
+
|
|
66
|
+
# Show plan
|
|
67
|
+
show_install_plan(install_targets)
|
|
68
|
+
return unless confirm?("Do you want to install these MOD(s)?")
|
|
69
|
+
|
|
70
|
+
# Execute installation
|
|
71
|
+
execute_installation(install_targets, jobs)
|
|
72
|
+
say "Installed #{install_targets.size} MOD(s)", prefix: :success
|
|
73
|
+
else
|
|
74
|
+
say "All MOD(s) from save file are already installed", prefix: :info
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Resolve conflicts: disable existing MODs that conflict with new ones
|
|
78
|
+
resolve_conflicts(mod_list, save_data.mods, graph)
|
|
79
|
+
|
|
80
|
+
# Update mod-list.json
|
|
81
|
+
update_mod_list(mod_list, save_data.mods)
|
|
82
|
+
backup_if_exists(runtime.mod_list_path)
|
|
83
|
+
mod_list.save
|
|
84
|
+
say "Updated mod-list.json", prefix: :success
|
|
85
|
+
|
|
86
|
+
# Update mod-settings.dat
|
|
87
|
+
update_mod_settings(save_data.startup_settings, save_data.version)
|
|
88
|
+
say "Updated mod-settings.dat", prefix: :success
|
|
89
|
+
|
|
90
|
+
say "Sync completed successfully", prefix: :success
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private def find_mods_to_install(save_mods, installed_mods)
|
|
94
|
+
save_mods.reject do |mod_name, _mod_state|
|
|
95
|
+
# Skip base MOD (always installed)
|
|
96
|
+
next true if mod_name == "base"
|
|
97
|
+
|
|
98
|
+
# Check if MOD is installed
|
|
99
|
+
mod = Factorix::MOD[name: mod_name]
|
|
100
|
+
installed_mods.any? {|installed| installed.mod == mod }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Plan the installation by fetching MOD info and extending the graph
|
|
105
|
+
#
|
|
106
|
+
# @param mods_to_install [Hash<String, MODState>] MODs to install
|
|
107
|
+
# @param graph [Dependency::Graph] Current dependency graph
|
|
108
|
+
# @param jobs [Integer] Number of parallel jobs
|
|
109
|
+
# @return [Array<Hash>] Installation targets with MOD info and releases
|
|
110
|
+
private def plan_installation(mods_to_install, graph, jobs)
|
|
111
|
+
# Create progress presenter for info fetching
|
|
112
|
+
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: $stderr)
|
|
113
|
+
|
|
114
|
+
# Fetch info for MODs to install
|
|
115
|
+
target_infos = fetch_target_mod_info(mods_to_install, jobs, presenter)
|
|
116
|
+
|
|
117
|
+
# Add to graph
|
|
118
|
+
target_infos.each do |info|
|
|
119
|
+
graph.add_uninstalled_mod(info[:mod_info], info[:release])
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Build install targets
|
|
123
|
+
build_install_targets(target_infos, runtime.mod_dir)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Fetch MOD information for MODs to install
|
|
127
|
+
#
|
|
128
|
+
# @param mods_to_install [Hash<String, MODState>] MODs to install
|
|
129
|
+
# @param jobs [Integer] Number of parallel jobs
|
|
130
|
+
# @param presenter [Progress::Presenter] Progress presenter
|
|
131
|
+
# @return [Array<Hash>] Array of {mod_name:, mod_info:, release:, version:}
|
|
132
|
+
private def fetch_target_mod_info(mods_to_install, jobs, presenter)
|
|
133
|
+
presenter.start(total: mods_to_install.size)
|
|
134
|
+
|
|
135
|
+
pool = Concurrent::FixedThreadPool.new(jobs)
|
|
136
|
+
|
|
137
|
+
futures = mods_to_install.map {|mod_name, mod_state|
|
|
138
|
+
Concurrent::Future.execute(executor: pool) do
|
|
139
|
+
result = fetch_single_mod_info(mod_name, mod_state.version)
|
|
140
|
+
presenter.update
|
|
141
|
+
result
|
|
142
|
+
end
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
results = futures.map(&:value!)
|
|
146
|
+
results
|
|
147
|
+
ensure
|
|
148
|
+
pool&.shutdown
|
|
149
|
+
pool&.wait_for_termination
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Fetch information for a single MOD
|
|
153
|
+
#
|
|
154
|
+
# @param mod_name [String] MOD name
|
|
155
|
+
# @param version [MODVersion] Target version
|
|
156
|
+
# @return [Hash] {mod_name:, mod_info:, release:, version:}
|
|
157
|
+
private def fetch_single_mod_info(mod_name, version)
|
|
158
|
+
# Fetch full MOD info from portal
|
|
159
|
+
mod_info = portal.get_mod_full(mod_name)
|
|
160
|
+
|
|
161
|
+
# Find the specific version release
|
|
162
|
+
release = mod_info.releases.find {|r| r.version == version }
|
|
163
|
+
|
|
164
|
+
unless release
|
|
165
|
+
raise MODNotOnPortalError, "Release not found for #{mod_name}@#{version}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
{mod_name:, mod_info:, release:, version:}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Show the installation plan
|
|
172
|
+
#
|
|
173
|
+
# @param targets [Array<Hash>] Installation targets
|
|
174
|
+
# @return [void]
|
|
175
|
+
private def show_install_plan(targets)
|
|
176
|
+
say "Planning to install #{targets.size} MOD(s):", prefix: :info
|
|
177
|
+
targets.each do |target|
|
|
178
|
+
say " - #{target[:mod]}@#{target[:release].version}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Execute the installation
|
|
183
|
+
#
|
|
184
|
+
# @param targets [Array<Hash>] Installation targets
|
|
185
|
+
# @param jobs [Integer] Number of parallel jobs
|
|
186
|
+
# @return [void]
|
|
187
|
+
private def execute_installation(targets, jobs)
|
|
188
|
+
# Download all MODs
|
|
189
|
+
download_mods(targets, jobs)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Resolve conflicts between save file MODs and existing enabled MODs
|
|
193
|
+
#
|
|
194
|
+
# @param mod_list [MODList] Current MOD list
|
|
195
|
+
# @param save_mods [Hash<String, MODState>] MODs from save file
|
|
196
|
+
# @param graph [Dependency::Graph] Dependency graph
|
|
197
|
+
# @return [void]
|
|
198
|
+
private def resolve_conflicts(mod_list, save_mods, graph)
|
|
199
|
+
save_mods.each do |mod_name, mod_state|
|
|
200
|
+
next unless mod_state.enabled?
|
|
201
|
+
|
|
202
|
+
mod = Factorix::MOD[name: mod_name]
|
|
203
|
+
|
|
204
|
+
graph.edges_from(mod).each do |edge|
|
|
205
|
+
next unless edge.incompatible?
|
|
206
|
+
|
|
207
|
+
conflicting_mod = edge.to_mod
|
|
208
|
+
next unless mod_list.exist?(conflicting_mod) && mod_list.enabled?(conflicting_mod)
|
|
209
|
+
|
|
210
|
+
mod_list.disable(conflicting_mod)
|
|
211
|
+
say "Disabled #{conflicting_mod} (conflicts with #{mod} from save file)", prefix: :warn
|
|
212
|
+
logger.debug("Disabled conflicting MOD", mod_name: conflicting_mod.name, conflicts_with: mod.name)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
graph.edges_to(mod).each do |edge|
|
|
216
|
+
next unless edge.incompatible?
|
|
217
|
+
|
|
218
|
+
conflicting_mod = edge.from_mod
|
|
219
|
+
next unless mod_list.exist?(conflicting_mod) && mod_list.enabled?(conflicting_mod)
|
|
220
|
+
|
|
221
|
+
mod_list.disable(conflicting_mod)
|
|
222
|
+
say "Disabled #{conflicting_mod} (conflicts with #{mod} from save file)", prefix: :warn
|
|
223
|
+
logger.debug("Disabled conflicting MOD", mod_name: conflicting_mod.name, conflicts_with: mod.name)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Update mod-list.json with MODs from save file
|
|
229
|
+
#
|
|
230
|
+
# @param mod_list [MODList] Current MOD list
|
|
231
|
+
# @param save_mods [Hash<String, MODState>] MODs from save file
|
|
232
|
+
# @return [void]
|
|
233
|
+
private def update_mod_list(mod_list, save_mods)
|
|
234
|
+
save_mods.each do |mod_name, mod_state|
|
|
235
|
+
mod = Factorix::MOD[name: mod_name]
|
|
236
|
+
|
|
237
|
+
# base MOD: don't update version or enabled state
|
|
238
|
+
if mod.base?
|
|
239
|
+
logger.debug("Skipping base MOD (no changes allowed)", mod_name:)
|
|
240
|
+
next
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
if mod_list.exist?(mod)
|
|
244
|
+
# expansion MOD: only update enabled state (not version)
|
|
245
|
+
if mod.expansion?
|
|
246
|
+
if mod_state.enabled? && !mod_list.enabled?(mod)
|
|
247
|
+
mod_list.enable(mod)
|
|
248
|
+
logger.debug("Enabled expansion MOD in mod-list.json", mod_name:)
|
|
249
|
+
elsif !mod_state.enabled? && mod_list.enabled?(mod)
|
|
250
|
+
mod_list.disable(mod)
|
|
251
|
+
logger.debug("Disabled expansion MOD in mod-list.json", mod_name:)
|
|
252
|
+
end
|
|
253
|
+
else
|
|
254
|
+
# Regular MOD: update both version and enabled state
|
|
255
|
+
# Remove and re-add to update version
|
|
256
|
+
mod_list.remove(mod)
|
|
257
|
+
mod_list.add(mod, enabled: mod_state.enabled?, version: mod_state.version)
|
|
258
|
+
logger.debug("Updated MOD in mod-list.json", mod_name:, version: mod_state.version&.to_s, enabled: mod_state.enabled?)
|
|
259
|
+
end
|
|
260
|
+
else
|
|
261
|
+
# Add new entry (version from save file)
|
|
262
|
+
mod_list.add(mod, enabled: mod_state.enabled?, version: mod_state.version)
|
|
263
|
+
logger.debug("Added to mod-list.json", mod_name:, version: mod_state.version&.to_s)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Update mod-settings.dat with startup settings from save file
|
|
269
|
+
#
|
|
270
|
+
# @param startup_settings [MODSettings::Section] Startup settings from save file
|
|
271
|
+
# @param game_version [GameVersion] Game version from save file
|
|
272
|
+
# @return [void]
|
|
273
|
+
private def update_mod_settings(startup_settings, game_version)
|
|
274
|
+
# Load existing settings or create new
|
|
275
|
+
mod_settings = if runtime.mod_settings_path.exist?
|
|
276
|
+
MODSettings.load(runtime.mod_settings_path)
|
|
277
|
+
else
|
|
278
|
+
# Create new MODSettings with all sections
|
|
279
|
+
sections = MODSettings::VALID_SECTIONS.to_h {|section_name|
|
|
280
|
+
[section_name, MODSettings::Section.new(section_name)]
|
|
281
|
+
}
|
|
282
|
+
MODSettings.new(game_version, sections)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Merge startup settings from save file
|
|
286
|
+
startup_section = mod_settings["startup"]
|
|
287
|
+
startup_settings.each do |key, value|
|
|
288
|
+
startup_section[key] = value
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Save updated settings
|
|
292
|
+
backup_if_exists(runtime.mod_settings_path)
|
|
293
|
+
mod_settings.save(runtime.mod_settings_path)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|