From 5e6b06f009ce34d759864911af948428d654989d Mon Sep 17 00:00:00 2001 From: bbassie <17990055+bbassie@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:30:32 +0000 Subject: [PATCH] Filter dependency resolution by the mod pack's Factorio version The dependency resolver picked the newest release satisfying the version range regardless of which Factorio version it targeted, so a 2.0 mod pack could end up with 2.1 dependencies. Only consider portal releases and local mods whose factorio_version matches the mod pack. Fixes #965 Co-Authored-By: Claude Fable 5 --- packages/controller/src/ControlConnection.ts | 15 +++- test/controller/ControlConnection.test.js | 73 ++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index 6baf2fe4..c9c9f8b0 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -713,9 +713,14 @@ export default class ControlConnection extends BaseConnection { return null; } - // Select the latest matching version + // Select the latest matching version for the mod pack's Factorio version const release = releases.releases - .filter(info => versionRange.testVersion(info.version)) + .filter(info => ( + versionRange.testVersion(info.version) + && lib.normaliseMajorMinorVersion( + lib.normaliseSourceVersion(info.info_json.factorio_version) + ) === factorioVersion + )) .reduce((max, cur) => ( max && lib.integerSourceVersion(max.version) > lib.integerSourceVersion(cur.version) ? max : cur ), undefined); @@ -797,7 +802,11 @@ export default class ControlConnection extends BaseConnection { // Check if a local version fits the requirements let candidate = localInfos - .filter(info => (info.name === mod.name && versionRange.testVersion(info.version))) + .filter(info => ( + info.name === mod.name + && info.factorioVersion === factorioVersion + && versionRange.testVersion(info.version) + )) .reduce((max, cur) => ( max && max.integerVersion > cur.integerVersion ? max : cur ), undefined); diff --git a/test/controller/ControlConnection.test.js b/test/controller/ControlConnection.test.js index 90483a08..f0cbccb7 100644 --- a/test/controller/ControlConnection.test.js +++ b/test/controller/ControlConnection.test.js @@ -1,5 +1,6 @@ "use strict"; const assert = require("assert").strict; +const lib = require("@clusterio/lib"); const { ControlConnection } = require("@clusterio/controller"); describe("controller/src/ControlConnection", function() { @@ -39,4 +40,76 @@ describe("controller/src/ControlConnection", function() { assert.equal(mockController.stopped, false); }); }); + + describe(".handleModDependencyResolveRequest()", function() { + const originalFetchModReleases = lib.ModStore.fetchModReleases; + const portal = new Map(); + let localMods; + + function release(version, factorioVersion, dependencies = []) { + return { + version, sha1: "0".repeat(40), + info_json: { factorio_version: factorioVersion, dependencies }, + }; + } + + before(function() { + lib.ModStore.fetchModReleases = async (name) => { + const releases = portal.get(name); + if (!releases) { + throw new Error("Fetch: returned 404 Not Found"); + } + return { name, owner: "owner", title: name, releases }; + }; + }); + after(function() { + lib.ModStore.fetchModReleases = originalFetchModReleases; + }); + beforeEach(function() { + portal.clear(); + localMods = []; + }); + + async function resolve(mods, factorioVersion, checkForUpdates = false) { + const response = await ControlConnection.prototype.handleModDependencyResolveRequest.call( + { _controller: { modStore: { mods: () => localMods } } }, + new lib.ModDependencyResolveRequest( + mods.map(mod => new lib.ModDependency(mod)), factorioVersion, checkForUpdates, + ), + ); + return new Map(response.dependencies.map(mod => [mod.name, mod])); + } + + it("only selects portal releases matching the mod pack's Factorio version", async function() { + portal.set("root", [release("1.0.0", "2.0", ["dep"]), release("1.1.0", "2.1", ["dep"])]); + portal.set("dep", [ + release("1.0.0", "1.1"), + release("2.0.0", "2.0"), + release("3.0.0", "2.1"), + ]); + assert.equal((await resolve(["root = 1.0.0"], "2.0")).get("dep").version, "2.0.0"); + assert.equal((await resolve(["root = 1.1.0"], "2.1")).get("dep").version, "3.0.0"); + }); + + it("only selects local mods matching the mod pack's Factorio version", async function() { + portal.set("root", [release("1.0.0", "2.0", ["dep"])]); + portal.set("dep", [release("2.0.0", "2.0")]); + localMods = [ + lib.ModInfo.fromJSON({ name: "dep", version: "3.0.0", factorio_version: "2.1" }), + lib.ModInfo.fromJSON({ name: "dep", version: "1.0.0", factorio_version: "2.0" }), + ]; + assert.equal((await resolve(["root = 1.0.0"], "2.0")).get("dep").version, "1.0.0"); + assert.equal((await resolve(["root = 1.0.0"], "2.0", true)).get("dep").version, "2.0.0"); + }); + + it("reports dependencies without a matching Factorio version as not found", async function() { + portal.set("root", [release("1.0.0", "2.0", ["dep"])]); + portal.set("dep", [release("3.0.0", "2.1")]); + const response = await ControlConnection.prototype.handleModDependencyResolveRequest.call( + { _controller: { modStore: { mods: () => localMods } } }, + new lib.ModDependencyResolveRequest([new lib.ModDependency("root = 1.0.0")], "2.0", false), + ); + assert.equal(response.errors.get("dep"), "notFound"); + }); + }); });