Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## [Unreleased]

### Changed

- `mod sync` now disables enabled MODs (including expansion MODs) not listed in the save file by default (#70)
- Add `--keep-unlisted` option to `mod sync` to preserve MODs not listed in the save file (#70)

## [0.10.0] - 2026-02-21

### Changed
Expand Down
2 changes: 1 addition & 1 deletion completion/_factorix.bash
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ _factorix() {
;;
sync)
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "$global_opts $confirmable_opts -j --jobs" -- "$cur"))
COMPREPLY=($(compgen -W "$global_opts $confirmable_opts -j --jobs --keep-unlisted" -- "$cur"))
else
COMPREPLY=($(compgen -f -X '!*.zip' -- "$cur"))
fi
Expand Down
1 change: 1 addition & 0 deletions completion/_factorix.fish
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ complete -c factorix -n "__factorix_using_subcommand mod search" -l json -d 'Out
# mod sync options
complete -c factorix -n "__factorix_using_subcommand mod sync" -s y -l yes -d 'Skip confirmation prompts'
complete -c factorix -n "__factorix_using_subcommand mod sync" -s j -l jobs -d 'Number of parallel downloads' -r
complete -c factorix -n "__factorix_using_subcommand mod sync" -l keep-unlisted -d 'Keep MODs not listed in save file enabled'
complete -c factorix -n "__factorix_using_subcommand mod sync" -ra '(__fish_complete_suffix .zip)'

# mod changelog subcommands
Expand Down
1 change: 1 addition & 0 deletions completion/_factorix.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ _factorix_mod() {
$global_opts \
$confirmable_opts \
'(-j --jobs)'{-j,--jobs}'[Number of parallel downloads]:jobs:' \
'--keep-unlisted[Keep MODs not listed in save file enabled]' \
'1:save file:_files -g "*.zip"'
;;
changelog)
Expand Down
7 changes: 6 additions & 1 deletion doc/components/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,15 @@ Show detailed MOD information from Factorio MOD Portal.

Synchronize MOD states from a save file.

**Options**:
- `-j`, `--jobs` - Number of parallel downloads (default: 4)
- `--keep-unlisted` - Keep MODs not listed in the save file enabled (default: disable them)

**Features**:
- Extracts MOD information from save file
- Downloads missing MODs concurrently
- Enables MODs to match save file state
- Enables/disables MODs to match save file state
- Disables enabled MODs (including expansion MODs) not listed in the save file (unless `--keep-unlisted` is specified)
- Preserves existing MOD files when possible

**Use case**: Set up MOD environment to match a specific save file
Expand Down
4 changes: 4 additions & 0 deletions doc/factorix.1
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,16 @@ Disable all MOD(s) except base.
Validate MOD dependencies. Reports missing, disabled, or incompatible dependencies.
.SS factorix mod sync SAVE_FILE
Sync MOD states and startup settings from a save file.
Enabled MODs not listed in the save file are disabled by default.
.TP
.BR \-y ", " \-\-yes
Skip confirmation prompts.
.TP
.BR \-j ", " \-\-jobs =\fIVALUE\fR
Number of parallel downloads (default: 4).
.TP
.B \-\-keep\-unlisted
Keep MODs not listed in the save file enabled.
.SS factorix mod changelog add ENTRY
Add an entry to MOD changelog.
.TP
Expand Down
26 changes: 23 additions & 3 deletions lib/factorix/cli/commands/mod/sync.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,21 @@ class Sync < Base
desc "Sync MOD states and startup settings from a save file"

example [
"save.zip # Sync MOD(s) from save file",
"-j 8 save.zip # Use 8 parallel downloads"
"save.zip # Sync MOD(s) from save file",
"-j 8 save.zip # Use 8 parallel downloads",
"--keep-unlisted save.zip # Keep MOD(s) not in save file enabled"
]

argument :save_file, required: true, desc: "Path to Factorio save file (.zip)"
option :jobs, aliases: ["-j"], default: "4", desc: "Number of parallel downloads"
option :keep_unlisted, type: :flag, default: false, desc: "Keep MOD(s) not listed in save file enabled"

# Execute the sync command
#
# @param save_file [String] Path to save file
# @param jobs [Integer] Number of parallel downloads
# @return [void]
def call(save_file:, jobs: "4", **)
def call(save_file:, jobs: "4", keep_unlisted: false, **)
jobs = Integer(jobs)
# Load save file
say "Loading save file: #{save_file}", prefix: :info
Expand Down Expand Up @@ -79,6 +81,7 @@ def call(save_file:, jobs: "4", **)

# Update mod-list.json
update_mod_list(mod_list, save_data.mods)
disable_unlisted_mods(mod_list, save_data.mods) unless keep_unlisted
backup_if_exists(runtime.mod_list_path)
mod_list.save
say "Updated mod-list.json", prefix: :success
Expand Down Expand Up @@ -265,6 +268,23 @@ def call(save_file:, jobs: "4", **)
end
end

# Disable enabled MODs that are not listed in the save file
#
# @param mod_list [MODList] Current MOD list
# @param save_mods [Hash<String, MODState>] MODs from save file
# @return [void]
private def disable_unlisted_mods(mod_list, save_mods)
mod_list.each_mod do |mod|
next if mod.base?
next unless mod_list.enabled?(mod)
next if save_mods.key?(mod.name)

mod_list.disable(mod)
say "Disabled #{mod} (not listed in save file)", prefix: :info
logger.debug("Disabled unlisted MOD", mod_name: mod.name)
end
end

# Update mod-settings.dat with startup settings from save file
#
# @param startup_settings [MODSettings::Section] Startup settings from save file
Expand Down
104 changes: 63 additions & 41 deletions spec/factorix/cli/commands/mod/sync_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,51 @@
end

let(:mod_list) { Factorix::MODList.new }
let(:installed_mods) { [] }
let(:graph) { instance_double(Factorix::Dependency::Graph) }

let(:base_info) do
Factorix::InfoJSON[
name: "base",
version: base_mod_version,
title: "Base MOD",
author: "Wube",
description: "Base game",
factorio_version: "2.0",
dependencies: []
]
end

let(:test_mod_info) do
Factorix::InfoJSON[
name: "test-mod",
version: Factorix::MODVersion.from_string("1.0.0"),
title: "Test MOD",
author: "Test Author",
description: "Test description",
factorio_version: "2.0",
dependencies: []
]
end

let(:installed_mods) do
[
Factorix::InstalledMOD[
mod: Factorix::MOD[name: "base"],
version: base_mod_version,
form: Factorix::InstalledMOD::DIRECTORY_FORM,
path: Pathname("/path/to/base"),
info: base_info
],
Factorix::InstalledMOD[
mod: Factorix::MOD[name: "test-mod"],
version: Factorix::MODVersion.from_string("1.0.0"),
form: Factorix::InstalledMOD::ZIP_FORM,
path: Pathname("/path/to/test-mod_1.0.0.zip"),
info: test_mod_info
]
]
end

before do
allow(runtime).to receive_messages(running?: false, mod_dir:, mod_list_path:, mod_settings_path:)
allow(Factorix::SaveFile).to receive(:load).and_return(save_data)
Expand Down Expand Up @@ -63,58 +105,38 @@

describe "#call" do
context "when all MODs from save file are already installed" do
let(:base_info) do
Factorix::InfoJSON[
name: "base",
version: base_mod_version,
title: "Base MOD",
author: "Wube",
description: "Base game",
factorio_version: "2.0",
dependencies: []
]
before do
mod_list.add(Factorix::MOD[name: "base"], enabled: true, version: base_mod_version)
mod_list.add(Factorix::MOD[name: "test-mod"], enabled: true, version: Factorix::MODVersion.from_string("1.0.0"))
end

let(:test_mod_info) do
Factorix::InfoJSON[
name: "test-mod",
version: Factorix::MODVersion.from_string("1.0.0"),
title: "Test MOD",
author: "Test Author",
description: "Test description",
factorio_version: "2.0",
dependencies: []
]
end
it "updates mod-list.json" do
run_command(command, %W[#{save_file_path}])

let(:installed_mods) do
[
Factorix::InstalledMOD[
mod: Factorix::MOD[name: "base"],
version: base_mod_version,
form: Factorix::InstalledMOD::DIRECTORY_FORM,
path: Pathname("/path/to/base"),
info: base_info
],
Factorix::InstalledMOD[
mod: Factorix::MOD[name: "test-mod"],
version: Factorix::MODVersion.from_string("1.0.0"),
form: Factorix::InstalledMOD::ZIP_FORM,
path: Pathname("/path/to/test-mod_1.0.0.zip"),
info: test_mod_info
]
]
expect(mod_list).to have_received(:save).with(no_args)
end
end

context "when there are enabled MODs not listed in the save file" do
before do
mod_list.add(Factorix::MOD[name: "base"], enabled: true, version: base_mod_version)
mod_list.add(Factorix::MOD[name: "test-mod"], enabled: true, version: Factorix::MODVersion.from_string("1.0.0"))
mod_list.add(Factorix::MOD[name: "extra-mod"], enabled: true)
mod_list.add(Factorix::MOD[name: "space-age"], enabled: true)
end

it "updates mod-list.json" do
it "disables unlisted regular and expansion MODs by default" do
run_command(command, %W[#{save_file_path}])

expect(mod_list).to have_received(:save).with(no_args)
expect(mod_list.enabled?(Factorix::MOD[name: "extra-mod"])).to be false
expect(mod_list.enabled?(Factorix::MOD[name: "space-age"])).to be false
end

it "keeps unlisted MODs enabled when --keep-unlisted is given" do
run_command(command, %W[--keep-unlisted #{save_file_path}])

expect(mod_list.enabled?(Factorix::MOD[name: "extra-mod"])).to be true
expect(mod_list.enabled?(Factorix::MOD[name: "space-age"])).to be true
end
end
end
Expand Down