factorix 0.10.0 → 0.11.1
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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +1 -0
- data/completion/_factorix.bash +24 -2
- data/completion/_factorix.fish +20 -8
- data/completion/_factorix.zsh +42 -0
- data/doc/factorix.1 +4 -0
- data/lib/factorix/blueprint.rb +62 -0
- data/lib/factorix/cli/commands/blueprint/decode.rb +40 -0
- data/lib/factorix/cli/commands/blueprint/encode.rb +42 -0
- data/lib/factorix/cli/commands/mod/sync.rb +245 -89
- data/lib/factorix/cli.rb +2 -0
- data/lib/factorix/errors.rb +4 -0
- data/lib/factorix/version.rb +1 -1
- data/sig/factorix/blueprint.rbs +14 -0
- data/sig/factorix/cli/commands/blueprint/decode.rbs +15 -0
- data/sig/factorix/cli/commands/blueprint/encode.rbs +15 -0
- data/sig/factorix/errors.rbs +6 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff57824d3ac0e73ae53844437c03f98bd10537f724a0285986b473b5136559f7
|
|
4
|
+
data.tar.gz: ed520b42e6f293f4e8021a71515f58348882093b634285d96c6d04164550d633
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36b43e73b76ec6e769ffaa7ec98dbcef83329490a01d6980518786fbb5699661db5e804fc3a0475455196aa0a1f50db2fd42ae233616a17dfef94b4578f71a31
|
|
7
|
+
data.tar.gz: d4f869b4cf329d391d59af1db92082e7acd18c0b6603334949f6fd7ffa22e11cc4536f16f3d0fdfefc1c3eeefa0653a8d0b207dd9eedaac201ad20ffca763fc6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.11.1] - 2026-03-03
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Fix `mod sync` incorrectly saving `mod-list.json` and `mod-settings.dat` when nothing changed (#74)
|
|
8
|
+
- Include startup settings changes in the `mod sync` plan and confirmation flow (#74)
|
|
9
|
+
|
|
10
|
+
## [0.11.0] - 2026-03-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Add `blueprint decode` command to decode Factorio blueprint strings to JSON (#76)
|
|
15
|
+
- Add `blueprint encode` command to encode JSON to Factorio blueprint strings (#76)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `mod sync` now disables enabled MODs (including expansion MODs) not listed in the save file by default (#70)
|
|
20
|
+
- Add `--keep-unlisted` option to `mod sync` to preserve MODs not listed in the save file (#70)
|
|
21
|
+
|
|
3
22
|
## [0.10.0] - 2026-02-21
|
|
4
23
|
|
|
5
24
|
### Changed
|
data/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Factorix simplifies Factorio MOD management by providing:
|
|
|
18
18
|
- **Installation & Uninstallation**: Install MODs directly from the portal or uninstall existing MODs
|
|
19
19
|
- **Save File Sync**: Synchronize MOD states and startup settings from Factorio save files
|
|
20
20
|
- **Settings Management**: Export/import MOD settings in JSON format
|
|
21
|
+
- **Blueprint Conversion**: Decode/encode Factorio blueprint strings to/from JSON
|
|
21
22
|
- **MOD Portal Integration**: Upload new MODs or update existing ones, edit metadata
|
|
22
23
|
- **Game Control**: Launch Factorio from the command line
|
|
23
24
|
- **Game Download**: Download Factorio game files (alpha, expansion, demo, headless)
|
data/completion/_factorix.bash
CHANGED
|
@@ -17,7 +17,10 @@ _factorix() {
|
|
|
17
17
|
local confirmable_opts="-y --yes"
|
|
18
18
|
|
|
19
19
|
# Top-level commands
|
|
20
|
-
local commands="version man launch path download mod cache completion"
|
|
20
|
+
local commands="version man launch path download blueprint mod cache completion"
|
|
21
|
+
|
|
22
|
+
# blueprint subcommands
|
|
23
|
+
local blueprint_commands="decode encode"
|
|
21
24
|
|
|
22
25
|
# mod subcommands
|
|
23
26
|
local mod_commands="changelog check list show enable disable install uninstall update download upload edit search sync image settings"
|
|
@@ -66,6 +69,25 @@ _factorix() {
|
|
|
66
69
|
fi
|
|
67
70
|
return
|
|
68
71
|
;;
|
|
72
|
+
blueprint)
|
|
73
|
+
if [[ $cword -eq 2 ]]; then
|
|
74
|
+
COMPREPLY=($(compgen -W "$blueprint_commands" -- "$cur"))
|
|
75
|
+
else
|
|
76
|
+
case "${words[2]}" in
|
|
77
|
+
decode|encode)
|
|
78
|
+
if [[ "$cur" == -* ]]; then
|
|
79
|
+
COMPREPLY=($(compgen -W "$global_opts -o --output" -- "$cur"))
|
|
80
|
+
else
|
|
81
|
+
COMPREPLY=($(compgen -f -- "$cur"))
|
|
82
|
+
fi
|
|
83
|
+
;;
|
|
84
|
+
*)
|
|
85
|
+
COMPREPLY=($(compgen -W "$global_opts" -- "$cur"))
|
|
86
|
+
;;
|
|
87
|
+
esac
|
|
88
|
+
fi
|
|
89
|
+
return
|
|
90
|
+
;;
|
|
69
91
|
cache)
|
|
70
92
|
if [[ $cword -eq 2 ]]; then
|
|
71
93
|
COMPREPLY=($(compgen -W "$cache_commands" -- "$cur"))
|
|
@@ -165,7 +187,7 @@ _factorix() {
|
|
|
165
187
|
;;
|
|
166
188
|
sync)
|
|
167
189
|
if [[ "$cur" == -* ]]; then
|
|
168
|
-
COMPREPLY=($(compgen -W "$global_opts $confirmable_opts -j --jobs" -- "$cur"))
|
|
190
|
+
COMPREPLY=($(compgen -W "$global_opts $confirmable_opts -j --jobs --keep-unlisted" -- "$cur"))
|
|
169
191
|
else
|
|
170
192
|
COMPREPLY=($(compgen -f -X '!*.zip' -- "$cur"))
|
|
171
193
|
fi
|
data/completion/_factorix.fish
CHANGED
|
@@ -107,14 +107,25 @@ complete -c factorix -l log-level -d 'Set log level' -xa 'debug info warn error
|
|
|
107
107
|
complete -c factorix -s q -l quiet -d 'Suppress non-essential output'
|
|
108
108
|
|
|
109
109
|
# Top-level commands
|
|
110
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a version -d 'Display Factorix version'
|
|
111
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a man -d 'Display the Factorix manual page'
|
|
112
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a launch -d 'Launch Factorio game'
|
|
113
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a path -d 'Display Factorio and Factorix paths'
|
|
114
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a download -d 'Download Factorio game files'
|
|
115
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a
|
|
116
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a
|
|
117
|
-
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a
|
|
110
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a version -d 'Display Factorix version'
|
|
111
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a man -d 'Display the Factorix manual page'
|
|
112
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a launch -d 'Launch Factorio game'
|
|
113
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a path -d 'Display Factorio and Factorix paths'
|
|
114
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a download -d 'Download Factorio game files'
|
|
115
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a blueprint -d 'Blueprint decode/encode commands'
|
|
116
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a mod -d 'MOD management commands'
|
|
117
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a cache -d 'Cache management commands'
|
|
118
|
+
complete -c factorix -n "not __factorix_using_command version; and not __factorix_using_command man; and not __factorix_using_command launch; and not __factorix_using_command path; and not __factorix_using_command download; and not __factorix_using_command blueprint; and not __factorix_using_command mod; and not __factorix_using_command cache; and not __factorix_using_command completion" -a completion -d 'Generate shell completion script'
|
|
119
|
+
|
|
120
|
+
# blueprint subcommands
|
|
121
|
+
complete -c factorix -n "__factorix_using_command blueprint" -a decode -d 'Decode a Factorio blueprint string to JSON'
|
|
122
|
+
complete -c factorix -n "__factorix_using_command blueprint" -a encode -d 'Encode JSON to a Factorio blueprint string'
|
|
123
|
+
|
|
124
|
+
# blueprint decode/encode options
|
|
125
|
+
complete -c factorix -n "__factorix_using_subcommand blueprint decode" -s o -l output -d 'Output file path' -rF
|
|
126
|
+
complete -c factorix -n "__factorix_using_subcommand blueprint decode" -rF
|
|
127
|
+
complete -c factorix -n "__factorix_using_subcommand blueprint encode" -s o -l output -d 'Output file path' -rF
|
|
128
|
+
complete -c factorix -n "__factorix_using_subcommand blueprint encode" -rF
|
|
118
129
|
|
|
119
130
|
# launch options
|
|
120
131
|
complete -c factorix -n "__factorix_using_command launch" -s w -l wait -d 'Wait for the game to finish'
|
|
@@ -228,6 +239,7 @@ complete -c factorix -n "__factorix_using_subcommand mod search" -l json -d 'Out
|
|
|
228
239
|
# mod sync options
|
|
229
240
|
complete -c factorix -n "__factorix_using_subcommand mod sync" -s y -l yes -d 'Skip confirmation prompts'
|
|
230
241
|
complete -c factorix -n "__factorix_using_subcommand mod sync" -s j -l jobs -d 'Number of parallel downloads' -r
|
|
242
|
+
complete -c factorix -n "__factorix_using_subcommand mod sync" -l keep-unlisted -d 'Keep MODs not listed in save file enabled'
|
|
231
243
|
complete -c factorix -n "__factorix_using_subcommand mod sync" -ra '(__fish_complete_suffix .zip)'
|
|
232
244
|
|
|
233
245
|
# mod changelog subcommands
|
data/completion/_factorix.zsh
CHANGED
|
@@ -40,6 +40,7 @@ _factorix() {
|
|
|
40
40
|
'launch:Launch Factorio game'
|
|
41
41
|
'path:Display Factorio and Factorix paths'
|
|
42
42
|
'download:Download Factorio game files'
|
|
43
|
+
'blueprint:Blueprint decode/encode commands'
|
|
43
44
|
'mod:MOD management commands'
|
|
44
45
|
'cache:Cache management commands'
|
|
45
46
|
'completion:Generate shell completion script'
|
|
@@ -62,6 +63,9 @@ _factorix() {
|
|
|
62
63
|
$global_opts \
|
|
63
64
|
'--json[Output in JSON format]'
|
|
64
65
|
;;
|
|
66
|
+
blueprint)
|
|
67
|
+
_factorix_blueprint
|
|
68
|
+
;;
|
|
65
69
|
download)
|
|
66
70
|
_factorix_download
|
|
67
71
|
;;
|
|
@@ -79,6 +83,43 @@ _factorix() {
|
|
|
79
83
|
esac
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
_factorix_blueprint() {
|
|
87
|
+
local context state state_descr line
|
|
88
|
+
typeset -A opt_args
|
|
89
|
+
|
|
90
|
+
local -a global_opts
|
|
91
|
+
global_opts=(
|
|
92
|
+
'(-c --config-path)'{-c,--config-path}'[Path to configuration file]:config file:_files'
|
|
93
|
+
'--log-level[Set log level]:level:(debug info warn error fatal)'
|
|
94
|
+
'(-q --quiet)'{-q,--quiet}'[Suppress non-essential output]'
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
_arguments -C \
|
|
98
|
+
'1:subcommand:->subcommand' \
|
|
99
|
+
'*::arg:->args'
|
|
100
|
+
|
|
101
|
+
case $state in
|
|
102
|
+
subcommand)
|
|
103
|
+
local -a subcommands
|
|
104
|
+
subcommands=(
|
|
105
|
+
'decode:Decode a Factorio blueprint string to JSON'
|
|
106
|
+
'encode:Encode JSON to a Factorio blueprint string'
|
|
107
|
+
)
|
|
108
|
+
_describe -t subcommands 'blueprint subcommand' subcommands
|
|
109
|
+
;;
|
|
110
|
+
args)
|
|
111
|
+
case $line[1] in
|
|
112
|
+
decode|encode)
|
|
113
|
+
_arguments \
|
|
114
|
+
$global_opts \
|
|
115
|
+
'(-o --output)'{-o,--output}'[Output file path]:output file:_files' \
|
|
116
|
+
'1:input file:_files'
|
|
117
|
+
;;
|
|
118
|
+
esac
|
|
119
|
+
;;
|
|
120
|
+
esac
|
|
121
|
+
}
|
|
122
|
+
|
|
82
123
|
_factorix_completion() {
|
|
83
124
|
local -a global_opts
|
|
84
125
|
global_opts=(
|
|
@@ -256,6 +297,7 @@ _factorix_mod() {
|
|
|
256
297
|
$global_opts \
|
|
257
298
|
$confirmable_opts \
|
|
258
299
|
'(-j --jobs)'{-j,--jobs}'[Number of parallel downloads]:jobs:' \
|
|
300
|
+
'--keep-unlisted[Keep MODs not listed in save file enabled]' \
|
|
259
301
|
'1:save file:_files -g "*.zip"'
|
|
260
302
|
;;
|
|
261
303
|
changelog)
|
data/doc/factorix.1
CHANGED
|
@@ -178,12 +178,16 @@ Disable all MOD(s) except base.
|
|
|
178
178
|
Validate MOD dependencies. Reports missing, disabled, or incompatible dependencies.
|
|
179
179
|
.SS factorix mod sync SAVE_FILE
|
|
180
180
|
Sync MOD states and startup settings from a save file.
|
|
181
|
+
Enabled MODs not listed in the save file are disabled by default.
|
|
181
182
|
.TP
|
|
182
183
|
.BR \-y ", " \-\-yes
|
|
183
184
|
Skip confirmation prompts.
|
|
184
185
|
.TP
|
|
185
186
|
.BR \-j ", " \-\-jobs =\fIVALUE\fR
|
|
186
187
|
Number of parallel downloads (default: 4).
|
|
188
|
+
.TP
|
|
189
|
+
.B \-\-keep\-unlisted
|
|
190
|
+
Keep MODs not listed in the save file enabled.
|
|
187
191
|
.SS factorix mod changelog add ENTRY
|
|
188
192
|
Add an entry to MOD changelog.
|
|
189
193
|
.TP
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "zlib"
|
|
6
|
+
|
|
7
|
+
module Factorix
|
|
8
|
+
# Represents a Factorio blueprint
|
|
9
|
+
#
|
|
10
|
+
# A blueprint string has the format: version_byte + Base64(zlib(JSON))
|
|
11
|
+
# Only version byte '0' is supported.
|
|
12
|
+
class Blueprint
|
|
13
|
+
# The only supported version byte
|
|
14
|
+
SUPPORTED_VERSION = "0"
|
|
15
|
+
private_constant :SUPPORTED_VERSION
|
|
16
|
+
|
|
17
|
+
# @return [Hash] The blueprint data
|
|
18
|
+
attr_reader :data
|
|
19
|
+
|
|
20
|
+
# Decode a blueprint string into a Blueprint
|
|
21
|
+
#
|
|
22
|
+
# @param string [String] The blueprint string
|
|
23
|
+
# @return [Blueprint]
|
|
24
|
+
# @raise [UnsupportedBlueprintVersionError] if the version byte is not '0'
|
|
25
|
+
# @raise [BlueprintFormatError] if the string is not a valid blueprint
|
|
26
|
+
def self.decode(string)
|
|
27
|
+
version = string[0]
|
|
28
|
+
raise UnsupportedBlueprintVersionError, "Unsupported blueprint version: #{version.inspect}" unless version == SUPPORTED_VERSION
|
|
29
|
+
|
|
30
|
+
compressed = Base64.strict_decode64(string[1..])
|
|
31
|
+
json_string = Zlib::Inflate.inflate(compressed)
|
|
32
|
+
new(JSON.parse(json_string))
|
|
33
|
+
rescue ArgumentError => e
|
|
34
|
+
raise BlueprintFormatError, "Invalid Base64 encoding: #{e.message}"
|
|
35
|
+
rescue Zlib::Error => e
|
|
36
|
+
raise BlueprintFormatError, "Invalid zlib data: #{e.message}"
|
|
37
|
+
rescue JSON::ParserError => e
|
|
38
|
+
raise BlueprintFormatError, "Invalid JSON: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param data [Hash] The blueprint data
|
|
42
|
+
def initialize(data)
|
|
43
|
+
@data = data
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Encode this blueprint to a blueprint string
|
|
47
|
+
#
|
|
48
|
+
# @return [String]
|
|
49
|
+
def encode
|
|
50
|
+
json_string = JSON.generate(@data)
|
|
51
|
+
compressed = Zlib::Deflate.deflate(json_string, Zlib::BEST_COMPRESSION)
|
|
52
|
+
SUPPORTED_VERSION + Base64.strict_encode64(compressed)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Serialize this blueprint to pretty-printed JSON
|
|
56
|
+
#
|
|
57
|
+
# @return [String]
|
|
58
|
+
def to_json(*)
|
|
59
|
+
JSON.pretty_generate(@data)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Factorix
|
|
4
|
+
class CLI
|
|
5
|
+
module Commands
|
|
6
|
+
module Blueprint
|
|
7
|
+
# Decode a Factorio blueprint string to JSON
|
|
8
|
+
class Decode < Base
|
|
9
|
+
desc "Decode a Factorio blueprint string to JSON"
|
|
10
|
+
|
|
11
|
+
example [
|
|
12
|
+
" # Decode from stdin",
|
|
13
|
+
"blueprint.txt # Decode from file",
|
|
14
|
+
"blueprint.txt -o decoded.json # Decode to file"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
argument :file, required: false, desc: "Path to file containing blueprint string (default: stdin)"
|
|
18
|
+
option :output, aliases: ["-o"], desc: "Output file path (default: stdout)"
|
|
19
|
+
|
|
20
|
+
# Execute the decode command
|
|
21
|
+
#
|
|
22
|
+
# @param file [String, nil] Path to file containing blueprint string
|
|
23
|
+
# @param output [String, nil] Output file path
|
|
24
|
+
# @return [void]
|
|
25
|
+
def call(file: nil, output: nil, **)
|
|
26
|
+
blueprint_string = file ? Pathname(file).read.strip : $stdin.read.strip
|
|
27
|
+
blueprint = Factorix::Blueprint.decode(blueprint_string)
|
|
28
|
+
json_string = blueprint.to_json
|
|
29
|
+
|
|
30
|
+
if output
|
|
31
|
+
Pathname(output).write(json_string)
|
|
32
|
+
else
|
|
33
|
+
out.puts json_string
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module Blueprint
|
|
9
|
+
# Encode JSON to a Factorio blueprint string
|
|
10
|
+
class Encode < Base
|
|
11
|
+
desc "Encode JSON to a Factorio blueprint string"
|
|
12
|
+
|
|
13
|
+
example [
|
|
14
|
+
" # Encode from stdin",
|
|
15
|
+
"decoded.json # Encode from file",
|
|
16
|
+
"decoded.json -o blueprint.txt # Encode to file"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
argument :file, required: false, desc: "Path to JSON file (default: stdin)"
|
|
20
|
+
option :output, aliases: ["-o"], desc: "Output file path (default: stdout)"
|
|
21
|
+
|
|
22
|
+
# Execute the encode command
|
|
23
|
+
#
|
|
24
|
+
# @param file [String, nil] Path to JSON file
|
|
25
|
+
# @param output [String, nil] Output file path
|
|
26
|
+
# @return [void]
|
|
27
|
+
def call(file: nil, output: nil, **)
|
|
28
|
+
json_string = file ? Pathname(file).read : $stdin.read
|
|
29
|
+
blueprint = Factorix::Blueprint.new(JSON.parse(json_string))
|
|
30
|
+
blueprint_string = blueprint.encode
|
|
31
|
+
|
|
32
|
+
if output
|
|
33
|
+
Pathname(output).write(blueprint_string)
|
|
34
|
+
else
|
|
35
|
+
out.print blueprint_string
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -26,26 +26,27 @@ module Factorix
|
|
|
26
26
|
desc "Sync MOD states and startup settings from a save file"
|
|
27
27
|
|
|
28
28
|
example [
|
|
29
|
-
"save.zip
|
|
30
|
-
"-j 8 save.zip
|
|
29
|
+
"save.zip # Sync MOD(s) from save file",
|
|
30
|
+
"-j 8 save.zip # Use 8 parallel downloads",
|
|
31
|
+
"--keep-unlisted save.zip # Keep MOD(s) not in save file enabled"
|
|
31
32
|
]
|
|
32
33
|
|
|
33
34
|
argument :save_file, required: true, desc: "Path to Factorio save file (.zip)"
|
|
34
35
|
option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
|
|
36
|
+
option :keep_unlisted, type: :flag, default: false, desc: "Keep MOD(s) not listed in save file enabled"
|
|
35
37
|
|
|
36
38
|
# Execute the sync command
|
|
37
39
|
#
|
|
38
40
|
# @param save_file [String] Path to save file
|
|
39
41
|
# @param jobs [Integer] Number of parallel downloads
|
|
42
|
+
# @param keep_unlisted [Boolean] Whether to keep unlisted MODs enabled
|
|
40
43
|
# @return [void]
|
|
41
|
-
def call(save_file:, jobs: "4", **)
|
|
44
|
+
def call(save_file:, jobs: "4", keep_unlisted: false, **)
|
|
42
45
|
jobs = Integer(jobs)
|
|
43
|
-
# Load save file
|
|
44
46
|
say "Loading save file: #{save_file}", prefix: :info
|
|
45
47
|
save_data = SaveFile.load(Pathname(save_file))
|
|
46
48
|
say "Loaded save file (version: #{save_data.version}, MOD(s): #{save_data.mods.size})", prefix: :info
|
|
47
49
|
|
|
48
|
-
# Load current state
|
|
49
50
|
mod_list = MODList.load
|
|
50
51
|
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Scanning MOD(s)", output: err)
|
|
51
52
|
handler = Progress::ScanHandler.new(presenter)
|
|
@@ -54,48 +55,49 @@ module Factorix
|
|
|
54
55
|
|
|
55
56
|
raise DirectoryNotFoundError, "MOD directory does not exist: #{runtime.mod_dir}" unless runtime.mod_dir.exist?
|
|
56
57
|
|
|
57
|
-
#
|
|
58
|
+
# Plan phase (no side effects)
|
|
58
59
|
mods_to_install = find_mods_to_install(save_data.mods, installed_mods)
|
|
60
|
+
install_targets = mods_to_install.any? ? plan_installation(mods_to_install, graph, jobs) : []
|
|
61
|
+
conflict_mods = find_conflict_mods(mod_list, save_data.mods, graph)
|
|
62
|
+
changes = plan_mod_list_changes(mod_list, save_data.mods)
|
|
63
|
+
unlisted_mods = keep_unlisted ? [] : find_unlisted_mods(mod_list, save_data.mods, conflict_mods)
|
|
64
|
+
mod_list_changed = needs_confirmation?(install_targets, conflict_mods, changes, unlisted_mods)
|
|
65
|
+
settings_changed = startup_settings_changed?(save_data.startup_settings)
|
|
66
|
+
|
|
67
|
+
# Show combined plan and ask once
|
|
68
|
+
unless mod_list_changed || settings_changed
|
|
69
|
+
say "Nothing to change", prefix: :info
|
|
70
|
+
return
|
|
71
|
+
end
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
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)?")
|
|
73
|
+
show_sync_plan(install_targets, conflict_mods, changes, unlisted_mods, settings_changed)
|
|
74
|
+
return unless confirm?("Do you want to apply these changes?")
|
|
69
75
|
|
|
70
|
-
|
|
76
|
+
# Execute phase
|
|
77
|
+
if install_targets.any?
|
|
71
78
|
execute_installation(install_targets, jobs)
|
|
72
79
|
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
80
|
end
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
mod_list.save
|
|
84
|
-
say "Updated mod-list.json", prefix: :success
|
|
82
|
+
if mod_list_changed
|
|
83
|
+
apply_mod_list_changes(mod_list, conflict_mods, changes, unlisted_mods)
|
|
84
|
+
backup_if_exists(runtime.mod_list_path)
|
|
85
|
+
mod_list.save
|
|
86
|
+
say "Updated mod-list.json", prefix: :success
|
|
87
|
+
end
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
if settings_changed
|
|
90
|
+
update_mod_settings(save_data.startup_settings, save_data.version)
|
|
91
|
+
say "Updated mod-settings.dat", prefix: :success
|
|
92
|
+
end
|
|
89
93
|
|
|
90
94
|
say "Sync completed successfully", prefix: :success
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
private def find_mods_to_install(save_mods, installed_mods)
|
|
94
98
|
save_mods.reject do |mod_name, _mod_state|
|
|
95
|
-
# Skip base MOD (always installed)
|
|
96
99
|
next true if mod_name == "base"
|
|
97
100
|
|
|
98
|
-
# Check if MOD is installed
|
|
99
101
|
mod = Factorix::MOD[name: mod_name]
|
|
100
102
|
installed_mods.any? {|installed| installed.mod == mod }
|
|
101
103
|
end
|
|
@@ -108,18 +110,13 @@ module Factorix
|
|
|
108
110
|
# @param jobs [Integer] Number of parallel jobs
|
|
109
111
|
# @return [Array<Hash>] Installation targets with MOD info and releases
|
|
110
112
|
private def plan_installation(mods_to_install, graph, jobs)
|
|
111
|
-
# Create progress presenter for info fetching
|
|
112
113
|
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: err)
|
|
113
|
-
|
|
114
|
-
# Fetch info for MODs to install
|
|
115
114
|
target_infos = fetch_target_mod_info(mods_to_install, jobs, presenter)
|
|
116
115
|
|
|
117
|
-
# Add to graph
|
|
118
116
|
target_infos.each do |info|
|
|
119
117
|
graph.add_uninstalled_mod(info[:mod_info], info[:release])
|
|
120
118
|
end
|
|
121
119
|
|
|
122
|
-
# Build install targets
|
|
123
120
|
build_install_targets(target_infos, runtime.mod_dir)
|
|
124
121
|
end
|
|
125
122
|
|
|
@@ -155,10 +152,7 @@ module Factorix
|
|
|
155
152
|
# @param version [MODVersion] Target version
|
|
156
153
|
# @return [Hash] {mod_name:, mod_info:, release:, version:}
|
|
157
154
|
private def fetch_single_mod_info(mod_name, version)
|
|
158
|
-
# Fetch full MOD info from portal
|
|
159
155
|
mod_info = portal.get_mod_full(mod_name)
|
|
160
|
-
|
|
161
|
-
# Find the specific version release
|
|
162
156
|
release = mod_info.releases.find {|r| r.version == version }
|
|
163
157
|
|
|
164
158
|
unless release
|
|
@@ -168,100 +162,238 @@ module Factorix
|
|
|
168
162
|
{mod_name:, mod_info:, release:, version:}
|
|
169
163
|
end
|
|
170
164
|
|
|
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
165
|
# Execute the installation
|
|
183
166
|
#
|
|
184
167
|
# @param targets [Array<Hash>] Installation targets
|
|
185
168
|
# @param jobs [Integer] Number of parallel jobs
|
|
186
169
|
# @return [void]
|
|
187
170
|
private def execute_installation(targets, jobs)
|
|
188
|
-
# Download all MODs
|
|
189
171
|
download_mods(targets, jobs)
|
|
190
172
|
end
|
|
191
173
|
|
|
192
|
-
#
|
|
174
|
+
# Find MODs that conflict with enabled MODs from the save file
|
|
193
175
|
#
|
|
194
176
|
# @param mod_list [MODList] Current MOD list
|
|
195
177
|
# @param save_mods [Hash<String, MODState>] MODs from save file
|
|
196
178
|
# @param graph [Dependency::Graph] Dependency graph
|
|
197
|
-
# @return [
|
|
198
|
-
private def
|
|
179
|
+
# @return [Array<Hash>] Conflict entries: {mod:, conflicts_with:}
|
|
180
|
+
private def find_conflict_mods(mod_list, save_mods, graph)
|
|
181
|
+
conflicts = []
|
|
182
|
+
seen = Set.new
|
|
183
|
+
|
|
199
184
|
save_mods.each do |mod_name, mod_state|
|
|
200
185
|
next unless mod_state.enabled?
|
|
201
186
|
|
|
202
|
-
|
|
187
|
+
save_mod = Factorix::MOD[name: mod_name]
|
|
203
188
|
|
|
204
|
-
graph.edges_from(
|
|
189
|
+
graph.edges_from(save_mod).each do |edge|
|
|
205
190
|
next unless edge.incompatible?
|
|
206
191
|
|
|
207
192
|
conflicting_mod = edge.to_mod
|
|
208
193
|
next unless mod_list.exist?(conflicting_mod) && mod_list.enabled?(conflicting_mod)
|
|
194
|
+
next unless seen.add?(conflicting_mod)
|
|
209
195
|
|
|
210
|
-
|
|
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)
|
|
196
|
+
conflicts << {mod: conflicting_mod, conflicts_with: save_mod}
|
|
213
197
|
end
|
|
214
198
|
|
|
215
|
-
graph.edges_to(
|
|
199
|
+
graph.edges_to(save_mod).each do |edge|
|
|
216
200
|
next unless edge.incompatible?
|
|
217
201
|
|
|
218
202
|
conflicting_mod = edge.from_mod
|
|
219
203
|
next unless mod_list.exist?(conflicting_mod) && mod_list.enabled?(conflicting_mod)
|
|
204
|
+
next unless seen.add?(conflicting_mod)
|
|
220
205
|
|
|
221
|
-
|
|
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)
|
|
206
|
+
conflicts << {mod: conflicting_mod, conflicts_with: save_mod}
|
|
224
207
|
end
|
|
225
208
|
end
|
|
209
|
+
|
|
210
|
+
conflicts
|
|
226
211
|
end
|
|
227
212
|
|
|
228
|
-
#
|
|
213
|
+
# Compute changes needed to bring mod-list.json in sync with save file
|
|
229
214
|
#
|
|
230
215
|
# @param mod_list [MODList] Current MOD list
|
|
231
216
|
# @param save_mods [Hash<String, MODState>] MODs from save file
|
|
232
|
-
# @return [
|
|
233
|
-
private def
|
|
217
|
+
# @return [Array<Hash>] Change entries: {mod:, action:, ...}
|
|
218
|
+
private def plan_mod_list_changes(mod_list, save_mods)
|
|
219
|
+
changes = []
|
|
220
|
+
|
|
234
221
|
save_mods.each do |mod_name, mod_state|
|
|
235
222
|
mod = Factorix::MOD[name: mod_name]
|
|
223
|
+
next if mod.base?
|
|
236
224
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
225
|
+
if mod_list.exist?(mod)
|
|
226
|
+
changes.concat(plan_existing_mod_changes(mod_list, mod, mod_state))
|
|
227
|
+
else
|
|
228
|
+
# Not in list: add it (silently if disabled)
|
|
229
|
+
changes << {mod:, action: :add, to_enabled: mod_state.enabled?, to_version: mod_state.version}
|
|
241
230
|
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
changes
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Plan changes for a single MOD that already exists in mod-list.json
|
|
237
|
+
#
|
|
238
|
+
# @param mod_list [MODList] Current MOD list
|
|
239
|
+
# @param mod [MOD] The MOD to plan changes for
|
|
240
|
+
# @param mod_state [MODState] MOD state from save file
|
|
241
|
+
# @return [Array<Hash>] Change entries for this MOD
|
|
242
|
+
private def plan_existing_mod_changes(mod_list, mod, mod_state)
|
|
243
|
+
current_enabled = mod_list.enabled?(mod)
|
|
244
|
+
return plan_expansion_mod_changes(mod, mod_state, current_enabled) if mod.expansion?
|
|
245
|
+
|
|
246
|
+
current_version = mod_list.version(mod)
|
|
247
|
+
to_enabled = mod_state.enabled?
|
|
248
|
+
to_version = mod_state.version
|
|
249
|
+
enabled_changed = current_enabled != to_enabled
|
|
250
|
+
version_changed = current_version && current_version != to_version
|
|
251
|
+
|
|
252
|
+
if enabled_changed
|
|
253
|
+
action = to_enabled ? :enable : :disable
|
|
254
|
+
[{mod:, action:, from_version: current_version, to_version:, from_enabled: current_enabled}]
|
|
255
|
+
elsif version_changed
|
|
256
|
+
[{mod:, action: :update, from_version: current_version, to_version:, from_enabled: current_enabled}]
|
|
257
|
+
else
|
|
258
|
+
[]
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Plan enable/disable changes for an expansion MOD
|
|
263
|
+
#
|
|
264
|
+
# @param mod [MOD] The expansion MOD
|
|
265
|
+
# @param mod_state [MODState] MOD state from save file
|
|
266
|
+
# @param current_enabled [Boolean] Current enabled state in mod-list.json
|
|
267
|
+
# @return [Array<Hash>] Change entries (0 or 1 element)
|
|
268
|
+
private def plan_expansion_mod_changes(mod, mod_state, current_enabled)
|
|
269
|
+
if mod_state.enabled? && !current_enabled
|
|
270
|
+
[{mod:, action: :enable}]
|
|
271
|
+
elsif !mod_state.enabled? && current_enabled
|
|
272
|
+
[{mod:, action: :disable}]
|
|
273
|
+
else
|
|
274
|
+
[]
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Find enabled MODs not listed in the save file (excluding conflict MODs)
|
|
279
|
+
#
|
|
280
|
+
# @param mod_list [MODList] Current MOD list
|
|
281
|
+
# @param save_mods [Hash<String, MODState>] MODs from save file
|
|
282
|
+
# @param conflict_mods [Array<Hash>] Already-planned conflict disables
|
|
283
|
+
# @return [Array<MOD>] MODs to disable
|
|
284
|
+
private def find_unlisted_mods(mod_list, save_mods, conflict_mods)
|
|
285
|
+
conflict_mod_set = Set.new(conflict_mods.map {|c| c[:mod] })
|
|
286
|
+
|
|
287
|
+
mod_list.each_mod.select do |mod|
|
|
288
|
+
!mod.base? &&
|
|
289
|
+
mod_list.enabled?(mod) &&
|
|
290
|
+
!save_mods.key?(mod.name) &&
|
|
291
|
+
!conflict_mod_set.include?(mod)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Show the combined sync plan
|
|
296
|
+
#
|
|
297
|
+
# @param install_targets [Array<Hash>] MODs to install
|
|
298
|
+
# @param conflict_mods [Array<Hash>] MODs to disable due to conflicts
|
|
299
|
+
# @param changes [Array<Hash>] MOD list changes from save file
|
|
300
|
+
# @param unlisted_mods [Array<MOD>] MODs to disable as unlisted
|
|
301
|
+
# @param settings_changed [Boolean] Whether startup settings will be updated
|
|
302
|
+
# @return [void]
|
|
303
|
+
private def show_sync_plan(install_targets, conflict_mods, changes, unlisted_mods, settings_changed)
|
|
304
|
+
say "Planning to sync MOD(s):", prefix: :info
|
|
305
|
+
|
|
306
|
+
if install_targets.any?
|
|
307
|
+
say " Install:"
|
|
308
|
+
install_targets.each {|t| say " - #{t[:mod]}@#{t[:release].version}" }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
enable_changes = changes.select {|c| c[:action] == :enable }
|
|
312
|
+
if enable_changes.any?
|
|
313
|
+
say " Enable:"
|
|
314
|
+
enable_changes.each {|c| say " - #{c[:mod]}" }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
disable_changes = changes.select {|c| c[:action] == :disable }
|
|
318
|
+
all_disables = conflict_mods.map {|c| {mod: c[:mod], reason: "(conflicts with #{c[:conflicts_with]})"} } +
|
|
319
|
+
disable_changes.map {|c| {mod: c[:mod], reason: "(disabled in save file)"} } +
|
|
320
|
+
unlisted_mods.map {|m| {mod: m, reason: "(not listed in save file)"} }
|
|
321
|
+
if all_disables.any?
|
|
322
|
+
say " Disable:"
|
|
323
|
+
all_disables.each {|d| say " - #{d[:mod]} #{d[:reason]}" }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
update_changes = changes.select {|c| c[:action] == :update }
|
|
327
|
+
if update_changes.any?
|
|
328
|
+
say " Update:"
|
|
329
|
+
update_changes.each {|c| say " - #{c[:mod]} (#{c[:from_version]} \u2192 #{c[:to_version]})" }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
say " Update startup settings" if settings_changed
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Apply all mod-list.json changes
|
|
336
|
+
#
|
|
337
|
+
# @param mod_list [MODList] MOD list to modify
|
|
338
|
+
# @param conflict_mods [Array<Hash>] Conflict entries to disable
|
|
339
|
+
# @param changes [Array<Hash>] MOD list changes
|
|
340
|
+
# @param unlisted_mods [Array<MOD>] Unlisted MODs to disable
|
|
341
|
+
# @return [void]
|
|
342
|
+
private def apply_mod_list_changes(mod_list, conflict_mods, changes, unlisted_mods)
|
|
343
|
+
conflict_mods.each do |conflict|
|
|
344
|
+
mod_list.disable(conflict[:mod])
|
|
345
|
+
logger.debug("Disabled conflicting MOD", mod_name: conflict[:mod].name, conflicts_with: conflict[:conflicts_with].name)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
changes.each {|change| apply_single_change(mod_list, change) }
|
|
349
|
+
|
|
350
|
+
unlisted_mods.each do |mod|
|
|
351
|
+
mod_list.disable(mod)
|
|
352
|
+
logger.debug("Disabled unlisted MOD", mod_name: mod.name)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
242
355
|
|
|
356
|
+
# Apply a single change entry to mod-list.json
|
|
357
|
+
#
|
|
358
|
+
# @param mod_list [MODList] MOD list to modify
|
|
359
|
+
# @param change [Hash] Change entry from plan_mod_list_changes
|
|
360
|
+
# @return [void]
|
|
361
|
+
private def apply_single_change(mod_list, change)
|
|
362
|
+
mod = change[:mod]
|
|
363
|
+
case change[:action]
|
|
364
|
+
when :enable
|
|
243
365
|
if mod_list.exist?(mod)
|
|
244
|
-
# expansion MOD: only update enabled state (not version)
|
|
245
366
|
if mod.expansion?
|
|
246
|
-
|
|
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
|
|
367
|
+
mod_list.enable(mod)
|
|
253
368
|
else
|
|
254
|
-
# Regular MOD: update both version and enabled state
|
|
255
|
-
# Remove and re-add to update version
|
|
256
369
|
mod_list.remove(mod)
|
|
257
|
-
mod_list.add(mod, enabled:
|
|
258
|
-
logger.debug("Updated MOD in mod-list.json", mod_name:, version: mod_state.version&.to_s, enabled: mod_state.enabled?)
|
|
370
|
+
mod_list.add(mod, enabled: true, version: change[:to_version])
|
|
259
371
|
end
|
|
260
372
|
else
|
|
261
|
-
|
|
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)
|
|
373
|
+
mod_list.add(mod, enabled: true, version: change[:to_version])
|
|
264
374
|
end
|
|
375
|
+
logger.debug("Enabled MOD in mod-list.json", mod_name: mod.name)
|
|
376
|
+
when :disable
|
|
377
|
+
if mod_list.exist?(mod)
|
|
378
|
+
if mod.expansion?
|
|
379
|
+
mod_list.disable(mod)
|
|
380
|
+
else
|
|
381
|
+
mod_list.remove(mod)
|
|
382
|
+
mod_list.add(mod, enabled: false, version: change[:to_version])
|
|
383
|
+
end
|
|
384
|
+
else
|
|
385
|
+
mod_list.add(mod, enabled: false, version: change[:to_version])
|
|
386
|
+
end
|
|
387
|
+
logger.debug("Disabled MOD in mod-list.json", mod_name: mod.name)
|
|
388
|
+
when :update
|
|
389
|
+
mod_list.remove(mod)
|
|
390
|
+
mod_list.add(mod, enabled: change[:from_enabled], version: change[:to_version])
|
|
391
|
+
logger.debug("Updated MOD in mod-list.json", mod_name: mod.name, version: change[:to_version]&.to_s)
|
|
392
|
+
when :add
|
|
393
|
+
mod_list.add(mod, enabled: change[:to_enabled], version: change[:to_version])
|
|
394
|
+
logger.debug("Added MOD to mod-list.json", mod_name: mod.name, version: change[:to_version]&.to_s, enabled: change[:to_enabled])
|
|
395
|
+
else
|
|
396
|
+
raise ArgumentError, "Unexpected change action: #{change[:action]}"
|
|
265
397
|
end
|
|
266
398
|
end
|
|
267
399
|
|
|
@@ -271,27 +403,51 @@ module Factorix
|
|
|
271
403
|
# @param game_version [GameVersion] Game version from save file
|
|
272
404
|
# @return [void]
|
|
273
405
|
private def update_mod_settings(startup_settings, game_version)
|
|
274
|
-
# Load existing settings or create new
|
|
275
406
|
mod_settings = if runtime.mod_settings_path.exist?
|
|
276
407
|
MODSettings.load(runtime.mod_settings_path)
|
|
277
408
|
else
|
|
278
|
-
# Create new MODSettings with all sections
|
|
279
409
|
sections = MODSettings::VALID_SECTIONS.to_h {|section_name|
|
|
280
410
|
[section_name, MODSettings::Section.new(section_name)]
|
|
281
411
|
}
|
|
282
412
|
MODSettings.new(game_version, sections)
|
|
283
413
|
end
|
|
284
414
|
|
|
285
|
-
# Merge startup settings from save file
|
|
286
415
|
startup_section = mod_settings["startup"]
|
|
287
416
|
startup_settings.each do |key, value|
|
|
288
417
|
startup_section[key] = value
|
|
289
418
|
end
|
|
290
419
|
|
|
291
|
-
# Save updated settings
|
|
292
420
|
backup_if_exists(runtime.mod_settings_path)
|
|
293
421
|
mod_settings.save(runtime.mod_settings_path)
|
|
294
422
|
end
|
|
423
|
+
|
|
424
|
+
# Check whether user-visible changes exist that require confirmation
|
|
425
|
+
#
|
|
426
|
+
# @param install_targets [Array<Hash>] MODs to install
|
|
427
|
+
# @param conflict_mods [Array<Hash>] MODs to disable due to conflicts
|
|
428
|
+
# @param changes [Array<Hash>] MOD list changes
|
|
429
|
+
# @param unlisted_mods [Array<MOD>] MODs to disable as unlisted
|
|
430
|
+
# @return [Boolean]
|
|
431
|
+
private def needs_confirmation?(install_targets, conflict_mods, changes, unlisted_mods)
|
|
432
|
+
install_targets.any? ||
|
|
433
|
+
conflict_mods.any? ||
|
|
434
|
+
changes.any? {|c| c[:action] != :add || c[:to_enabled] } ||
|
|
435
|
+
unlisted_mods.any?
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Check whether startup settings from the save file differ from the current mod-settings.dat
|
|
439
|
+
#
|
|
440
|
+
# @param startup_settings [MODSettings::Section] Startup settings from save file
|
|
441
|
+
# @return [Boolean]
|
|
442
|
+
private def startup_settings_changed?(startup_settings)
|
|
443
|
+
return true unless runtime.mod_settings_path.exist?
|
|
444
|
+
|
|
445
|
+
mod_settings = MODSettings.load(runtime.mod_settings_path)
|
|
446
|
+
startup_section = mod_settings["startup"]
|
|
447
|
+
startup_settings.any? do |key, value|
|
|
448
|
+
startup_section[key] != value
|
|
449
|
+
end
|
|
450
|
+
end
|
|
295
451
|
end
|
|
296
452
|
end
|
|
297
453
|
end
|
data/lib/factorix/cli.rb
CHANGED
|
@@ -13,6 +13,8 @@ module Factorix
|
|
|
13
13
|
class CLI
|
|
14
14
|
extend Dry::CLI::Registry
|
|
15
15
|
|
|
16
|
+
register "blueprint decode", Commands::Blueprint::Decode
|
|
17
|
+
register "blueprint encode", Commands::Blueprint::Encode
|
|
16
18
|
register "version", Commands::Version
|
|
17
19
|
register "man", Commands::Man
|
|
18
20
|
register "launch", Commands::Launch
|
data/lib/factorix/errors.rb
CHANGED
|
@@ -71,6 +71,10 @@ module Factorix
|
|
|
71
71
|
|
|
72
72
|
class ChangelogParseError < FileFormatError; end
|
|
73
73
|
|
|
74
|
+
# Blueprint format errors
|
|
75
|
+
class BlueprintFormatError < FileFormatError; end
|
|
76
|
+
class UnsupportedBlueprintVersionError < BlueprintFormatError; end
|
|
77
|
+
|
|
74
78
|
# =====================================
|
|
75
79
|
# Domain layer errors
|
|
76
80
|
# =====================================
|
data/lib/factorix/version.rb
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# RBS type signature file
|
|
2
|
+
# NOTE: Do not include private method definitions in RBS files.
|
|
3
|
+
# Only public interfaces should be documented here.
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class Blueprint
|
|
7
|
+
attr_reader data: Hash[String, untyped]
|
|
8
|
+
|
|
9
|
+
def initialize: (Hash[String, untyped] data) -> void
|
|
10
|
+
def self.decode: (String string) -> Blueprint
|
|
11
|
+
def encode: () -> String
|
|
12
|
+
def to_json: (*untyped) -> String
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# RBS type signature file
|
|
2
|
+
# NOTE: Do not include private method definitions in RBS files.
|
|
3
|
+
# Only public interfaces should be documented here.
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module Blueprint
|
|
9
|
+
class Decode < Base
|
|
10
|
+
def call: (?file: String?, ?output: String?, **untyped) -> void
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# RBS type signature file
|
|
2
|
+
# NOTE: Do not include private method definitions in RBS files.
|
|
3
|
+
# Only public interfaces should be documented here.
|
|
4
|
+
|
|
5
|
+
module Factorix
|
|
6
|
+
class CLI
|
|
7
|
+
module Commands
|
|
8
|
+
module Blueprint
|
|
9
|
+
class Encode < Base
|
|
10
|
+
def call: (?file: String?, ?output: String?, **untyped) -> void
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/sig/factorix/errors.rbs
CHANGED
|
@@ -79,6 +79,12 @@ module Factorix
|
|
|
79
79
|
class ChangelogParseError < FileFormatError
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
class BlueprintFormatError < FileFormatError
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class UnsupportedBlueprintVersionError < BlueprintFormatError
|
|
86
|
+
end
|
|
87
|
+
|
|
82
88
|
# =====================================
|
|
83
89
|
# Domain layer errors
|
|
84
90
|
# =====================================
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: factorix
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OZAWA Sakuro
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-03-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -250,6 +250,7 @@ files:
|
|
|
250
250
|
- lib/factorix/api/release.rb
|
|
251
251
|
- lib/factorix/api/tag.rb
|
|
252
252
|
- lib/factorix/api_credential.rb
|
|
253
|
+
- lib/factorix/blueprint.rb
|
|
253
254
|
- lib/factorix/cache/base.rb
|
|
254
255
|
- lib/factorix/cache/entry.rb
|
|
255
256
|
- lib/factorix/cache/file_system.rb
|
|
@@ -259,6 +260,8 @@ files:
|
|
|
259
260
|
- lib/factorix/cli.rb
|
|
260
261
|
- lib/factorix/cli/commands/backup_support.rb
|
|
261
262
|
- lib/factorix/cli/commands/base.rb
|
|
263
|
+
- lib/factorix/cli/commands/blueprint/decode.rb
|
|
264
|
+
- lib/factorix/cli/commands/blueprint/encode.rb
|
|
262
265
|
- lib/factorix/cli/commands/cache/evict.rb
|
|
263
266
|
- lib/factorix/cli/commands/cache/stat.rb
|
|
264
267
|
- lib/factorix/cli/commands/command_wrapper.rb
|
|
@@ -362,6 +365,7 @@ files:
|
|
|
362
365
|
- sig/factorix/api/release.rbs
|
|
363
366
|
- sig/factorix/api/tag.rbs
|
|
364
367
|
- sig/factorix/api_credential.rbs
|
|
368
|
+
- sig/factorix/blueprint.rbs
|
|
365
369
|
- sig/factorix/cache/base.rbs
|
|
366
370
|
- sig/factorix/cache/entry.rbs
|
|
367
371
|
- sig/factorix/cache/file_system.rbs
|
|
@@ -370,6 +374,8 @@ files:
|
|
|
370
374
|
- sig/factorix/changelog.rbs
|
|
371
375
|
- sig/factorix/cli.rbs
|
|
372
376
|
- sig/factorix/cli/commands/base.rbs
|
|
377
|
+
- sig/factorix/cli/commands/blueprint/decode.rbs
|
|
378
|
+
- sig/factorix/cli/commands/blueprint/encode.rbs
|
|
373
379
|
- sig/factorix/cli/commands/cache/evict.rbs
|
|
374
380
|
- sig/factorix/cli/commands/cache/stat.rbs
|
|
375
381
|
- sig/factorix/cli/commands/command_wrapper.rbs
|