factorix 0.9.1 → 0.11.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 +4 -4
- data/CHANGELOG.md +20 -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 +51 -0
- data/lib/factorix/api/category.rb +3 -0
- data/lib/factorix/api/license.rb +75 -19
- data/lib/factorix/api/tag.rb +3 -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/edit.rb +1 -1
- data/lib/factorix/cli/commands/mod/sync.rb +218 -83
- data/lib/factorix/cli.rb +2 -0
- data/lib/factorix/errors.rb +4 -0
- data/lib/factorix/version.rb +1 -1
- data/sig/factorix/api/category.rbs +1 -0
- data/sig/factorix/api/license.rbs +2 -1
- data/sig/factorix/api/tag.rbs +1 -0
- 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: f93238fc1b419e6adf7ec45ac04331f84aba675b94cf4d4e57fd7c8555687678
|
|
4
|
+
data.tar.gz: 2e7d4ed3b5c0b4be85fd5f1012eb4a73b84b160d5f31c1ed326a5830cba41c42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 254733ebd3ad1b896a84c341d35a49a2d8943114d4fe0f90d0b83efae5399d9f862aedaeb0f7a68d42cdfedad9ab0dae9a4d77625646540de111e77567b74ab6
|
|
7
|
+
data.tar.gz: 733b7da2d624bbf225e9bcbd586a0aa394c593d79743456c2a9b1a8ef06daf05603f1f4091da657038f7bacfdb766061a48026084a1d72290cc3350df2e8e694
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.11.0] - 2026-03-03
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `blueprint decode` command to decode Factorio blueprint strings to JSON (#76)
|
|
8
|
+
- Add `blueprint encode` command to encode JSON to Factorio blueprint strings (#76)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- `mod sync` now disables enabled MODs (including expansion MODs) not listed in the save file by default (#70)
|
|
13
|
+
- Add `--keep-unlisted` option to `mod sync` to preserve MODs not listed in the save file (#70)
|
|
14
|
+
|
|
15
|
+
## [0.10.0] - 2026-02-21
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Refactor `License` to use flyweight pattern for standard licenses with `.for(id)` method
|
|
20
|
+
- Add `.identifiers` class method to `Category`, `License`, and `Tag`
|
|
21
|
+
- Replace `License.identifier_values` with `License.identifiers`
|
|
22
|
+
|
|
3
23
|
## [0.9.1] - 2026-02-20
|
|
4
24
|
|
|
5
25
|
### Added
|
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,63 @@ 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.
|
|
191
|
+
.SS factorix mod changelog add ENTRY
|
|
192
|
+
Add an entry to MOD changelog.
|
|
193
|
+
.TP
|
|
194
|
+
.BR \-\-version =\fIVALUE\fR
|
|
195
|
+
Version (X.Y.Z or Unreleased, default: Unreleased).
|
|
196
|
+
.TP
|
|
197
|
+
.BR \-\-category =\fIVALUE\fR
|
|
198
|
+
Category (e.g., Features, Bugfixes). Required.
|
|
199
|
+
.TP
|
|
200
|
+
.BR \-\-changelog =\fIVALUE\fR
|
|
201
|
+
Path to changelog file (default: changelog.txt).
|
|
202
|
+
.SS factorix mod changelog extract
|
|
203
|
+
Extract a changelog section for a specific version.
|
|
204
|
+
.TP
|
|
205
|
+
.BR \-\-version =\fIVALUE\fR
|
|
206
|
+
Version (X.Y.Z or Unreleased). Required.
|
|
207
|
+
.TP
|
|
208
|
+
.B \-\-json
|
|
209
|
+
Output in JSON format.
|
|
210
|
+
.TP
|
|
211
|
+
.BR \-\-changelog =\fIVALUE\fR
|
|
212
|
+
Path to changelog file (default: changelog.txt).
|
|
213
|
+
.SS factorix mod changelog check
|
|
214
|
+
Validate MOD changelog structure.
|
|
215
|
+
.TP
|
|
216
|
+
.B \-\-release
|
|
217
|
+
Disallow Unreleased section.
|
|
218
|
+
.TP
|
|
219
|
+
.BR \-\-changelog =\fIVALUE\fR
|
|
220
|
+
Path to changelog file (default: changelog.txt).
|
|
221
|
+
.TP
|
|
222
|
+
.BR \-\-info\-json =\fIVALUE\fR
|
|
223
|
+
Path to info.json file (default: info.json).
|
|
224
|
+
.SS factorix mod changelog release
|
|
225
|
+
Convert Unreleased changelog section to a versioned section.
|
|
226
|
+
.TP
|
|
227
|
+
.BR \-\-version =\fIVALUE\fR
|
|
228
|
+
Version (X.Y.Z, default: from info.json).
|
|
229
|
+
.TP
|
|
230
|
+
.BR \-\-date =\fIVALUE\fR
|
|
231
|
+
Release date (YYYY-MM-DD, default: today UTC).
|
|
232
|
+
.TP
|
|
233
|
+
.BR \-\-changelog =\fIVALUE\fR
|
|
234
|
+
Path to changelog file (default: changelog.txt).
|
|
235
|
+
.TP
|
|
236
|
+
.BR \-\-info\-json =\fIVALUE\fR
|
|
237
|
+
Path to info.json file (default: info.json).
|
|
187
238
|
.SH MOD PORTAL COMMANDS
|
|
188
239
|
These commands interact with the Factorio MOD Portal and require authentication.
|
|
189
240
|
.SS factorix mod upload FILE
|
|
@@ -53,6 +53,9 @@ module Factorix
|
|
|
53
53
|
}.freeze
|
|
54
54
|
private_constant :CATEGORIES
|
|
55
55
|
|
|
56
|
+
# @return [Array<String>] all category identifiers
|
|
57
|
+
def self.identifiers = CATEGORIES.keys
|
|
58
|
+
|
|
56
59
|
# Get Category instance for the given value
|
|
57
60
|
#
|
|
58
61
|
# Returns predefined instance for known categories (flyweight pattern).
|
data/lib/factorix/api/license.rb
CHANGED
|
@@ -9,10 +9,11 @@ module Factorix
|
|
|
9
9
|
# License object from MOD Portal API
|
|
10
10
|
#
|
|
11
11
|
# Represents a MOD license information.
|
|
12
|
+
# Uses flyweight pattern for standard licenses.
|
|
12
13
|
# Also provides valid license identifiers for edit_details API.
|
|
13
14
|
#
|
|
14
15
|
# @see https://wiki.factorio.com/Mod_portal_API
|
|
15
|
-
# @see https://wiki.factorio.com/Mod_details_API
|
|
16
|
+
# @see https://wiki.factorio.com/Mod_details_API#License
|
|
16
17
|
class License
|
|
17
18
|
# @!attribute [r] id
|
|
18
19
|
# @return [String] license ID
|
|
@@ -25,37 +26,92 @@ module Factorix
|
|
|
25
26
|
# @!attribute [r] url
|
|
26
27
|
# @return [URI::HTTPS] license URL
|
|
27
28
|
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
# Predefined standard license instances
|
|
30
|
+
DEFAULT_MIT = new(
|
|
31
|
+
id: "default_mit",
|
|
32
|
+
name: "MIT",
|
|
33
|
+
title: "MIT License",
|
|
34
|
+
description: "A permissive license that is short and to the point. It lets people do anything with your code with proper attribution and without warranty.",
|
|
35
|
+
url: "https://raw.githubusercontent.com/spdx/license-list-data/main/text/MIT.txt"
|
|
36
|
+
)
|
|
37
|
+
private_constant :DEFAULT_MIT
|
|
38
|
+
DEFAULT_GNUGPLV3 = new(
|
|
39
|
+
id: "default_gnugplv3",
|
|
40
|
+
name: "GNU GPLv3",
|
|
41
|
+
title: "GNU General Public License v3.0",
|
|
42
|
+
description: "The GNU GPL is the most widely used free software license and has a strong copyleft requirement.",
|
|
43
|
+
url: "https://raw.githubusercontent.com/spdx/license-list-data/main/text/GPL-3.0-or-later.txt"
|
|
44
|
+
)
|
|
45
|
+
private_constant :DEFAULT_GNUGPLV3
|
|
46
|
+
DEFAULT_GNULGPLV3 = new(
|
|
47
|
+
id: "default_gnulgplv3",
|
|
48
|
+
name: "GNU LGPLv3",
|
|
49
|
+
title: "GNU Lesser General Public License v3.0",
|
|
50
|
+
description: "Version 3 of the GNU LGPL is an additional set of permissions to the GNU GPLv3 license that requires derived works use the same license.",
|
|
51
|
+
url: "https://raw.githubusercontent.com/spdx/license-list-data/main/text/LGPL-3.0-or-later.txt"
|
|
52
|
+
)
|
|
53
|
+
private_constant :DEFAULT_GNULGPLV3
|
|
54
|
+
DEFAULT_MOZILLA2 = new(
|
|
55
|
+
id: "default_mozilla2",
|
|
56
|
+
name: "Mozilla Public License 2.0",
|
|
57
|
+
title: "Mozilla Public License Version 2.0",
|
|
58
|
+
description: "The Mozilla Public License (MPL 2.0) attempts to be a compromise between the permissive BSD license and the reciprocal GPL license.",
|
|
59
|
+
url: "https://raw.githubusercontent.com/spdx/license-list-data/main/text/MPL-2.0.txt"
|
|
60
|
+
)
|
|
61
|
+
private_constant :DEFAULT_MOZILLA2
|
|
62
|
+
DEFAULT_APACHE2 = new(
|
|
63
|
+
id: "default_apache2",
|
|
64
|
+
name: "Apache License 2.0",
|
|
65
|
+
title: "Apache License, Version 2.0",
|
|
66
|
+
description: "A permissive license that also provides an express grant of patent rights from contributors to users.",
|
|
67
|
+
url: "https://raw.githubusercontent.com/spdx/license-list-data/main/text/Apache-2.0.txt"
|
|
68
|
+
)
|
|
69
|
+
private_constant :DEFAULT_APACHE2
|
|
70
|
+
DEFAULT_UNLICENSE = new(
|
|
71
|
+
id: "default_unlicense",
|
|
72
|
+
name: "The Unlicense",
|
|
73
|
+
title: "The Unlicense",
|
|
74
|
+
description: "The Unlicense is a template to waive copyright interest in software and dedicate it to the public domain.",
|
|
75
|
+
url: "https://raw.githubusercontent.com/spdx/license-list-data/main/text/Unlicense.txt"
|
|
76
|
+
)
|
|
77
|
+
private_constant :DEFAULT_UNLICENSE
|
|
78
|
+
|
|
79
|
+
# Lookup table for flyweight pattern
|
|
80
|
+
LICENSES = {
|
|
81
|
+
"default_mit" => DEFAULT_MIT,
|
|
82
|
+
"default_gnugplv3" => DEFAULT_GNUGPLV3,
|
|
83
|
+
"default_gnulgplv3" => DEFAULT_GNULGPLV3,
|
|
84
|
+
"default_mozilla2" => DEFAULT_MOZILLA2,
|
|
85
|
+
"default_apache2" => DEFAULT_APACHE2,
|
|
86
|
+
"default_unlicense" => DEFAULT_UNLICENSE
|
|
37
87
|
}.freeze
|
|
38
|
-
private_constant :
|
|
88
|
+
private_constant :LICENSES
|
|
39
89
|
|
|
40
90
|
# Pattern for custom license identifiers (custom_ + 24 lowercase hex chars)
|
|
41
91
|
CUSTOM_LICENSE_PATTERN = /\Acustom_[0-9a-f]{24}\z/
|
|
42
92
|
private_constant :CUSTOM_LICENSE_PATTERN
|
|
43
93
|
|
|
94
|
+
# @return [Array<String>] all license identifiers
|
|
95
|
+
def self.identifiers = LICENSES.keys
|
|
96
|
+
|
|
97
|
+
# Get License instance for the given identifier
|
|
98
|
+
#
|
|
99
|
+
# Returns predefined instance for known licenses (flyweight pattern).
|
|
100
|
+
# Raises an error for unknown license identifiers.
|
|
101
|
+
#
|
|
102
|
+
# @param id [String] license identifier
|
|
103
|
+
# @return [License] License instance
|
|
104
|
+
# @raise [KeyError] if license identifier is unknown
|
|
105
|
+
def self.for(id) = LICENSES.fetch(id.to_s)
|
|
106
|
+
|
|
44
107
|
# Check if the given value is a valid license identifier
|
|
45
108
|
#
|
|
46
109
|
# @param value [String] license identifier
|
|
47
110
|
# @return [Boolean] true if valid (standard or custom license)
|
|
48
111
|
def self.valid_identifier?(value)
|
|
49
|
-
|
|
112
|
+
LICENSES.key?(value) || CUSTOM_LICENSE_PATTERN.match?(value)
|
|
50
113
|
end
|
|
51
114
|
|
|
52
|
-
# List all valid license identifier values
|
|
53
|
-
#
|
|
54
|
-
# @return [Array<String>] array of license identifiers
|
|
55
|
-
def self.identifier_values = IDENTIFIERS.keys
|
|
56
|
-
|
|
57
|
-
# Create License from API response hash
|
|
58
|
-
#
|
|
59
115
|
# @param id [String] license ID
|
|
60
116
|
# @param name [String] license name
|
|
61
117
|
# @param title [String] license title
|
data/lib/factorix/api/tag.rb
CHANGED
|
@@ -79,6 +79,9 @@ module Factorix
|
|
|
79
79
|
}.freeze
|
|
80
80
|
private_constant :TAGS
|
|
81
81
|
|
|
82
|
+
# @return [Array<String>] all tag identifiers
|
|
83
|
+
def self.identifiers = TAGS.keys
|
|
84
|
+
|
|
82
85
|
# Get Tag instance for the given value
|
|
83
86
|
#
|
|
84
87
|
# Returns predefined instance for known tags (flyweight pattern).
|
|
@@ -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
|
|
@@ -100,7 +100,7 @@ module Factorix
|
|
|
100
100
|
return if API::License.valid_identifier?(license)
|
|
101
101
|
|
|
102
102
|
say "Invalid license identifier: #{license}", prefix: :error
|
|
103
|
-
say "Valid identifiers: #{API::License.
|
|
103
|
+
say "Valid identifiers: #{API::License.identifiers.join(", ")}"
|
|
104
104
|
say "Custom licenses: custom_<24 hex chars> (e.g., custom_0123456789abcdef01234567)"
|
|
105
105
|
raise InvalidArgumentError, "Invalid license identifier"
|
|
106
106
|
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,36 +55,30 @@ 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)
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
# Show combined plan and ask once
|
|
66
|
+
show_sync_plan(install_targets, conflict_mods, changes, unlisted_mods)
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
return if needs_confirmation?(install_targets, conflict_mods, changes, unlisted_mods) &&
|
|
69
|
+
!confirm?("Do you want to apply these changes?")
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return unless confirm?("Do you want to install these MOD(s)?")
|
|
69
|
-
|
|
70
|
-
# Execute installation
|
|
71
|
+
# Execute phase
|
|
72
|
+
if install_targets.any?
|
|
71
73
|
execute_installation(install_targets, jobs)
|
|
72
74
|
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
75
|
end
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
resolve_conflicts(mod_list, save_data.mods, graph)
|
|
79
|
-
|
|
80
|
-
# Update mod-list.json
|
|
81
|
-
update_mod_list(mod_list, save_data.mods)
|
|
77
|
+
apply_mod_list_changes(mod_list, conflict_mods, changes, unlisted_mods)
|
|
82
78
|
backup_if_exists(runtime.mod_list_path)
|
|
83
79
|
mod_list.save
|
|
84
80
|
say "Updated mod-list.json", prefix: :success
|
|
85
81
|
|
|
86
|
-
# Update mod-settings.dat
|
|
87
82
|
update_mod_settings(save_data.startup_settings, save_data.version)
|
|
88
83
|
say "Updated mod-settings.dat", prefix: :success
|
|
89
84
|
|
|
@@ -92,10 +87,8 @@ module Factorix
|
|
|
92
87
|
|
|
93
88
|
private def find_mods_to_install(save_mods, installed_mods)
|
|
94
89
|
save_mods.reject do |mod_name, _mod_state|
|
|
95
|
-
# Skip base MOD (always installed)
|
|
96
90
|
next true if mod_name == "base"
|
|
97
91
|
|
|
98
|
-
# Check if MOD is installed
|
|
99
92
|
mod = Factorix::MOD[name: mod_name]
|
|
100
93
|
installed_mods.any? {|installed| installed.mod == mod }
|
|
101
94
|
end
|
|
@@ -108,18 +101,13 @@ module Factorix
|
|
|
108
101
|
# @param jobs [Integer] Number of parallel jobs
|
|
109
102
|
# @return [Array<Hash>] Installation targets with MOD info and releases
|
|
110
103
|
private def plan_installation(mods_to_install, graph, jobs)
|
|
111
|
-
# Create progress presenter for info fetching
|
|
112
104
|
presenter = Progress::Presenter.new(title: "\u{1F50D}\u{FE0E} Fetching MOD info", output: err)
|
|
113
|
-
|
|
114
|
-
# Fetch info for MODs to install
|
|
115
105
|
target_infos = fetch_target_mod_info(mods_to_install, jobs, presenter)
|
|
116
106
|
|
|
117
|
-
# Add to graph
|
|
118
107
|
target_infos.each do |info|
|
|
119
108
|
graph.add_uninstalled_mod(info[:mod_info], info[:release])
|
|
120
109
|
end
|
|
121
110
|
|
|
122
|
-
# Build install targets
|
|
123
111
|
build_install_targets(target_infos, runtime.mod_dir)
|
|
124
112
|
end
|
|
125
113
|
|
|
@@ -155,10 +143,7 @@ module Factorix
|
|
|
155
143
|
# @param version [MODVersion] Target version
|
|
156
144
|
# @return [Hash] {mod_name:, mod_info:, release:, version:}
|
|
157
145
|
private def fetch_single_mod_info(mod_name, version)
|
|
158
|
-
# Fetch full MOD info from portal
|
|
159
146
|
mod_info = portal.get_mod_full(mod_name)
|
|
160
|
-
|
|
161
|
-
# Find the specific version release
|
|
162
147
|
release = mod_info.releases.find {|r| r.version == version }
|
|
163
148
|
|
|
164
149
|
unless release
|
|
@@ -168,100 +153,240 @@ module Factorix
|
|
|
168
153
|
{mod_name:, mod_info:, release:, version:}
|
|
169
154
|
end
|
|
170
155
|
|
|
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
156
|
# Execute the installation
|
|
183
157
|
#
|
|
184
158
|
# @param targets [Array<Hash>] Installation targets
|
|
185
159
|
# @param jobs [Integer] Number of parallel jobs
|
|
186
160
|
# @return [void]
|
|
187
161
|
private def execute_installation(targets, jobs)
|
|
188
|
-
# Download all MODs
|
|
189
162
|
download_mods(targets, jobs)
|
|
190
163
|
end
|
|
191
164
|
|
|
192
|
-
#
|
|
165
|
+
# Find MODs that conflict with enabled MODs from the save file
|
|
193
166
|
#
|
|
194
167
|
# @param mod_list [MODList] Current MOD list
|
|
195
168
|
# @param save_mods [Hash<String, MODState>] MODs from save file
|
|
196
169
|
# @param graph [Dependency::Graph] Dependency graph
|
|
197
|
-
# @return [
|
|
198
|
-
private def
|
|
170
|
+
# @return [Array<Hash>] Conflict entries: {mod:, conflicts_with:}
|
|
171
|
+
private def find_conflict_mods(mod_list, save_mods, graph)
|
|
172
|
+
conflicts = []
|
|
173
|
+
seen = Set.new
|
|
174
|
+
|
|
199
175
|
save_mods.each do |mod_name, mod_state|
|
|
200
176
|
next unless mod_state.enabled?
|
|
201
177
|
|
|
202
|
-
|
|
178
|
+
save_mod = Factorix::MOD[name: mod_name]
|
|
203
179
|
|
|
204
|
-
graph.edges_from(
|
|
180
|
+
graph.edges_from(save_mod).each do |edge|
|
|
205
181
|
next unless edge.incompatible?
|
|
206
182
|
|
|
207
183
|
conflicting_mod = edge.to_mod
|
|
208
184
|
next unless mod_list.exist?(conflicting_mod) && mod_list.enabled?(conflicting_mod)
|
|
185
|
+
next unless seen.add?(conflicting_mod)
|
|
209
186
|
|
|
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)
|
|
187
|
+
conflicts << {mod: conflicting_mod, conflicts_with: save_mod}
|
|
213
188
|
end
|
|
214
189
|
|
|
215
|
-
graph.edges_to(
|
|
190
|
+
graph.edges_to(save_mod).each do |edge|
|
|
216
191
|
next unless edge.incompatible?
|
|
217
192
|
|
|
218
193
|
conflicting_mod = edge.from_mod
|
|
219
194
|
next unless mod_list.exist?(conflicting_mod) && mod_list.enabled?(conflicting_mod)
|
|
195
|
+
next unless seen.add?(conflicting_mod)
|
|
220
196
|
|
|
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)
|
|
197
|
+
conflicts << {mod: conflicting_mod, conflicts_with: save_mod}
|
|
224
198
|
end
|
|
225
199
|
end
|
|
200
|
+
|
|
201
|
+
conflicts
|
|
226
202
|
end
|
|
227
203
|
|
|
228
|
-
#
|
|
204
|
+
# Compute changes needed to bring mod-list.json in sync with save file
|
|
229
205
|
#
|
|
230
206
|
# @param mod_list [MODList] Current MOD list
|
|
231
207
|
# @param save_mods [Hash<String, MODState>] MODs from save file
|
|
232
|
-
# @return [
|
|
233
|
-
private def
|
|
208
|
+
# @return [Array<Hash>] Change entries: {mod:, action:, ...}
|
|
209
|
+
private def plan_mod_list_changes(mod_list, save_mods)
|
|
210
|
+
changes = []
|
|
211
|
+
|
|
234
212
|
save_mods.each do |mod_name, mod_state|
|
|
235
213
|
mod = Factorix::MOD[name: mod_name]
|
|
214
|
+
next if mod.base?
|
|
236
215
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
216
|
+
if mod_list.exist?(mod)
|
|
217
|
+
changes.concat(plan_existing_mod_changes(mod_list, mod, mod_state))
|
|
218
|
+
else
|
|
219
|
+
# Not in list: add it (silently if disabled)
|
|
220
|
+
changes << {mod:, action: :add, to_enabled: mod_state.enabled?, to_version: mod_state.version}
|
|
241
221
|
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
changes
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Plan changes for a single MOD that already exists in mod-list.json
|
|
228
|
+
#
|
|
229
|
+
# @param mod_list [MODList] Current MOD list
|
|
230
|
+
# @param mod [MOD] The MOD to plan changes for
|
|
231
|
+
# @param mod_state [MODState] MOD state from save file
|
|
232
|
+
# @return [Array<Hash>] Change entries for this MOD
|
|
233
|
+
private def plan_existing_mod_changes(mod_list, mod, mod_state)
|
|
234
|
+
current_enabled = mod_list.enabled?(mod)
|
|
235
|
+
return plan_expansion_mod_changes(mod, mod_state, current_enabled) if mod.expansion?
|
|
236
|
+
|
|
237
|
+
current_version = mod_list.version(mod)
|
|
238
|
+
to_enabled = mod_state.enabled?
|
|
239
|
+
to_version = mod_state.version
|
|
240
|
+
enabled_changed = current_enabled != to_enabled
|
|
241
|
+
version_changed = current_version && current_version != to_version
|
|
242
|
+
|
|
243
|
+
if enabled_changed
|
|
244
|
+
action = to_enabled ? :enable : :disable
|
|
245
|
+
[{mod:, action:, from_version: current_version, to_version:, from_enabled: current_enabled}]
|
|
246
|
+
elsif version_changed
|
|
247
|
+
[{mod:, action: :update, from_version: current_version, to_version:, from_enabled: current_enabled}]
|
|
248
|
+
else
|
|
249
|
+
[]
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Plan enable/disable changes for an expansion MOD
|
|
254
|
+
#
|
|
255
|
+
# @param mod [MOD] The expansion MOD
|
|
256
|
+
# @param mod_state [MODState] MOD state from save file
|
|
257
|
+
# @param current_enabled [Boolean] Current enabled state in mod-list.json
|
|
258
|
+
# @return [Array<Hash>] Change entries (0 or 1 element)
|
|
259
|
+
private def plan_expansion_mod_changes(mod, mod_state, current_enabled)
|
|
260
|
+
if mod_state.enabled? && !current_enabled
|
|
261
|
+
[{mod:, action: :enable}]
|
|
262
|
+
elsif !mod_state.enabled? && current_enabled
|
|
263
|
+
[{mod:, action: :disable}]
|
|
264
|
+
else
|
|
265
|
+
[]
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Find enabled MODs not listed in the save file (excluding conflict MODs)
|
|
270
|
+
#
|
|
271
|
+
# @param mod_list [MODList] Current MOD list
|
|
272
|
+
# @param save_mods [Hash<String, MODState>] MODs from save file
|
|
273
|
+
# @param conflict_mods [Array<Hash>] Already-planned conflict disables
|
|
274
|
+
# @return [Array<MOD>] MODs to disable
|
|
275
|
+
private def find_unlisted_mods(mod_list, save_mods, conflict_mods)
|
|
276
|
+
conflict_mod_set = Set.new(conflict_mods.map {|c| c[:mod] })
|
|
277
|
+
|
|
278
|
+
mod_list.each_mod.select do |mod|
|
|
279
|
+
!mod.base? &&
|
|
280
|
+
mod_list.enabled?(mod) &&
|
|
281
|
+
!save_mods.key?(mod.name) &&
|
|
282
|
+
!conflict_mod_set.include?(mod)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Show the combined sync plan
|
|
287
|
+
#
|
|
288
|
+
# @param install_targets [Array<Hash>] MODs to install
|
|
289
|
+
# @param conflict_mods [Array<Hash>] MODs to disable due to conflicts
|
|
290
|
+
# @param changes [Array<Hash>] MOD list changes from save file
|
|
291
|
+
# @param unlisted_mods [Array<MOD>] MODs to disable as unlisted
|
|
292
|
+
# @return [void]
|
|
293
|
+
private def show_sync_plan(install_targets, conflict_mods, changes, unlisted_mods)
|
|
294
|
+
unless needs_confirmation?(install_targets, conflict_mods, changes, unlisted_mods)
|
|
295
|
+
say "Nothing to change", prefix: :info
|
|
296
|
+
return
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
say "Planning to sync MOD(s):", prefix: :info
|
|
242
300
|
|
|
301
|
+
if install_targets.any?
|
|
302
|
+
say " Install:"
|
|
303
|
+
install_targets.each {|t| say " - #{t[:mod]}@#{t[:release].version}" }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
enable_changes = changes.select {|c| c[:action] == :enable }
|
|
307
|
+
if enable_changes.any?
|
|
308
|
+
say " Enable:"
|
|
309
|
+
enable_changes.each {|c| say " - #{c[:mod]}" }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
disable_changes = changes.select {|c| c[:action] == :disable }
|
|
313
|
+
all_disables = conflict_mods.map {|c| {mod: c[:mod], reason: "(conflicts with #{c[:conflicts_with]})"} } +
|
|
314
|
+
disable_changes.map {|c| {mod: c[:mod], reason: "(disabled in save file)"} } +
|
|
315
|
+
unlisted_mods.map {|m| {mod: m, reason: "(not listed in save file)"} }
|
|
316
|
+
if all_disables.any?
|
|
317
|
+
say " Disable:"
|
|
318
|
+
all_disables.each {|d| say " - #{d[:mod]} #{d[:reason]}" }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
update_changes = changes.select {|c| c[:action] == :update }
|
|
322
|
+
return if update_changes.none?
|
|
323
|
+
|
|
324
|
+
say " Update:"
|
|
325
|
+
update_changes.each {|c| say " - #{c[:mod]} (#{c[:from_version]} \u2192 #{c[:to_version]})" }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Apply all mod-list.json changes
|
|
329
|
+
#
|
|
330
|
+
# @param mod_list [MODList] MOD list to modify
|
|
331
|
+
# @param conflict_mods [Array<Hash>] Conflict entries to disable
|
|
332
|
+
# @param changes [Array<Hash>] MOD list changes
|
|
333
|
+
# @param unlisted_mods [Array<MOD>] Unlisted MODs to disable
|
|
334
|
+
# @return [void]
|
|
335
|
+
private def apply_mod_list_changes(mod_list, conflict_mods, changes, unlisted_mods)
|
|
336
|
+
conflict_mods.each do |conflict|
|
|
337
|
+
mod_list.disable(conflict[:mod])
|
|
338
|
+
logger.debug("Disabled conflicting MOD", mod_name: conflict[:mod].name, conflicts_with: conflict[:conflicts_with].name)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
changes.each {|change| apply_single_change(mod_list, change) }
|
|
342
|
+
|
|
343
|
+
unlisted_mods.each do |mod|
|
|
344
|
+
mod_list.disable(mod)
|
|
345
|
+
logger.debug("Disabled unlisted MOD", mod_name: mod.name)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Apply a single change entry to mod-list.json
|
|
350
|
+
#
|
|
351
|
+
# @param mod_list [MODList] MOD list to modify
|
|
352
|
+
# @param change [Hash] Change entry from plan_mod_list_changes
|
|
353
|
+
# @return [void]
|
|
354
|
+
private def apply_single_change(mod_list, change)
|
|
355
|
+
mod = change[:mod]
|
|
356
|
+
case change[:action]
|
|
357
|
+
when :enable
|
|
358
|
+
if mod_list.exist?(mod)
|
|
359
|
+
if mod.expansion?
|
|
360
|
+
mod_list.enable(mod)
|
|
361
|
+
else
|
|
362
|
+
mod_list.remove(mod)
|
|
363
|
+
mod_list.add(mod, enabled: true, version: change[:to_version])
|
|
364
|
+
end
|
|
365
|
+
else
|
|
366
|
+
mod_list.add(mod, enabled: true, version: change[:to_version])
|
|
367
|
+
end
|
|
368
|
+
logger.debug("Enabled MOD in mod-list.json", mod_name: mod.name)
|
|
369
|
+
when :disable
|
|
243
370
|
if mod_list.exist?(mod)
|
|
244
|
-
# expansion MOD: only update enabled state (not version)
|
|
245
371
|
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
|
|
372
|
+
mod_list.disable(mod)
|
|
253
373
|
else
|
|
254
|
-
# Regular MOD: update both version and enabled state
|
|
255
|
-
# Remove and re-add to update version
|
|
256
374
|
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?)
|
|
375
|
+
mod_list.add(mod, enabled: false, version: change[:to_version])
|
|
259
376
|
end
|
|
260
377
|
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)
|
|
378
|
+
mod_list.add(mod, enabled: false, version: change[:to_version])
|
|
264
379
|
end
|
|
380
|
+
logger.debug("Disabled MOD in mod-list.json", mod_name: mod.name)
|
|
381
|
+
when :update
|
|
382
|
+
mod_list.remove(mod)
|
|
383
|
+
mod_list.add(mod, enabled: change[:from_enabled], version: change[:to_version])
|
|
384
|
+
logger.debug("Updated MOD in mod-list.json", mod_name: mod.name, version: change[:to_version]&.to_s)
|
|
385
|
+
when :add
|
|
386
|
+
mod_list.add(mod, enabled: change[:to_enabled], version: change[:to_version])
|
|
387
|
+
logger.debug("Added MOD to mod-list.json", mod_name: mod.name, version: change[:to_version]&.to_s, enabled: change[:to_enabled])
|
|
388
|
+
else
|
|
389
|
+
raise ArgumentError, "Unexpected change action: #{change[:action]}"
|
|
265
390
|
end
|
|
266
391
|
end
|
|
267
392
|
|
|
@@ -271,27 +396,37 @@ module Factorix
|
|
|
271
396
|
# @param game_version [GameVersion] Game version from save file
|
|
272
397
|
# @return [void]
|
|
273
398
|
private def update_mod_settings(startup_settings, game_version)
|
|
274
|
-
# Load existing settings or create new
|
|
275
399
|
mod_settings = if runtime.mod_settings_path.exist?
|
|
276
400
|
MODSettings.load(runtime.mod_settings_path)
|
|
277
401
|
else
|
|
278
|
-
# Create new MODSettings with all sections
|
|
279
402
|
sections = MODSettings::VALID_SECTIONS.to_h {|section_name|
|
|
280
403
|
[section_name, MODSettings::Section.new(section_name)]
|
|
281
404
|
}
|
|
282
405
|
MODSettings.new(game_version, sections)
|
|
283
406
|
end
|
|
284
407
|
|
|
285
|
-
# Merge startup settings from save file
|
|
286
408
|
startup_section = mod_settings["startup"]
|
|
287
409
|
startup_settings.each do |key, value|
|
|
288
410
|
startup_section[key] = value
|
|
289
411
|
end
|
|
290
412
|
|
|
291
|
-
# Save updated settings
|
|
292
413
|
backup_if_exists(runtime.mod_settings_path)
|
|
293
414
|
mod_settings.save(runtime.mod_settings_path)
|
|
294
415
|
end
|
|
416
|
+
|
|
417
|
+
# Check whether user-visible changes exist that require confirmation
|
|
418
|
+
#
|
|
419
|
+
# @param install_targets [Array<Hash>] MODs to install
|
|
420
|
+
# @param conflict_mods [Array<Hash>] MODs to disable due to conflicts
|
|
421
|
+
# @param changes [Array<Hash>] MOD list changes
|
|
422
|
+
# @param unlisted_mods [Array<MOD>] MODs to disable as unlisted
|
|
423
|
+
# @return [Boolean]
|
|
424
|
+
private def needs_confirmation?(install_targets, conflict_mods, changes, unlisted_mods)
|
|
425
|
+
install_targets.any? ||
|
|
426
|
+
conflict_mods.any? ||
|
|
427
|
+
changes.any? {|c| c[:action] != :add || c[:to_enabled] } ||
|
|
428
|
+
unlisted_mods.any?
|
|
429
|
+
end
|
|
295
430
|
end
|
|
296
431
|
end
|
|
297
432
|
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
|
@@ -11,8 +11,9 @@ module Factorix
|
|
|
11
11
|
attr_reader description: String
|
|
12
12
|
attr_reader url: URI::HTTPS
|
|
13
13
|
|
|
14
|
+
def self.identifiers: () -> Array[String]
|
|
15
|
+
def self.for: (String id) -> License
|
|
14
16
|
def self.valid_identifier?: (String value) -> bool
|
|
15
|
-
def self.identifier_values: () -> Array[String]
|
|
16
17
|
|
|
17
18
|
def initialize: (id: String, name: String, title: String, description: String, url: String) -> void
|
|
18
19
|
end
|
data/sig/factorix/api/tag.rbs
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.0
|
|
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
|