Support enemy_expansion.build_base_unit_dispatch_cooldown in exchange strings (#977)

* Support enemy_expansion.build_base_unit_dispatch_cooldown in exchange strings

Factorio 2.1.13 added build_base_unit_dispatch_cooldown as the last
field of enemy_expansion in the map exchange string, and rejects map
settings without it. Track the full version in the reader state and
read the field for 2.1.13 and later. Adds a fixture string generated
with 2.1.14 and uses it in the load-scenario integration test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Replace version flags with versionAtLeast in exchange string reader

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cooldude2606andClaude Fable 5 authored and GitHub committed 2026-08-19 23:24:58 +01:00
commit 59cd193b65
5 files changed
+63 -16

No files matched your search

+20 -13
View File
@@ -3,14 +3,19 @@ import zlib from "zlib";
class MapReaderState {
pos = 0;
last_position = { x: 0, y: 0 };
/** True when a version greater than 2.0.0 is detected */
gt_v2_0 = false;
/** True when a version greater than 2.1.0 is detected */
gt_v2_1 = false;
// If more versions are required then we should expose version directly
/** Version of Factorio the string was created with */
version = [0, 0, 0, 0];
constructor(
public buf: Buffer
) { }
/** True when the version is at least the given major.minor.patch */
versionAtLeast(major: number, minor: number, patch = 0) {
const [vMajor, vMinor, vPatch] = this.version;
if (vMajor !== major) { return vMajor > major; }
if (vMinor !== minor) { return vMinor > minor; }
return vPatch >= patch;
}
}
function readUInt8(state: MapReaderState) {
@@ -164,7 +169,7 @@ function readBoundingBox(state: MapReaderState) {
}
function readCliffSettings(state: MapReaderState) {
return state.gt_v2_0 ? {
return state.versionAtLeast(2, 0) ? {
name: readString(state),
control: readString(state), // v2
cliff_elevation_0: readFloat(state),
@@ -189,7 +194,7 @@ function readTerritorySettings(state: MapReaderState) {
}
function readMapGenSettings(state: MapReaderState) {
return state.gt_v2_0 ? {
return state.versionAtLeast(2, 0) ? {
autoplace_controls: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)),
autoplace_settings: Object.fromEntries(readDict(state, readString, readAutoplaceSetting)),
default_enable_all_autoplace_controls: readBool(state),
@@ -265,7 +270,7 @@ function readEnemyEvolution(state: MapReaderState) {
}
function readEnemyExpansion(state: MapReaderState) {
return state.gt_v2_1 ? {
return state.versionAtLeast(2, 1) ? {
enabled: readOptional(state, readBool),
max_expansion_distance: readOptional(state, readUInt32),
min_expansion_distance: readOptional(state, readUInt32), // v2.1
@@ -281,6 +286,9 @@ function readEnemyExpansion(state: MapReaderState) {
evolution_group_size_factor: readOptional(state, readDouble), // v2.1
min_expansion_cooldown: readOptional(state, readUInt32),
max_expansion_cooldown: readOptional(state, readUInt32),
...state.versionAtLeast(2, 1, 13) ? {
build_base_unit_dispatch_cooldown: readOptional(state, readUInt32), // v2.1.13
} : {},
} : {
enabled: readOptional(state, readBool),
max_expansion_distance: readOptional(state, readUInt32),
@@ -355,7 +363,7 @@ function readPathFinder(state: MapReaderState) {
}
function readDifficultySettings(state: MapReaderState) {
return state.gt_v2_0 ? {
return state.versionAtLeast(2, 0) ? {
technology_price_multiplier: readDouble(state),
spoil_time_modifier: readDouble(state), // v2
} : {
@@ -374,7 +382,7 @@ function readAsteroids(state: MapReaderState) {
}
function readMapSettings(state: MapReaderState) {
if (state.gt_v2_1) {
if (state.versionAtLeast(2, 1)) {
return {
pollution: readPollution(state),
// steering removed v2.1
@@ -386,7 +394,7 @@ function readMapSettings(state: MapReaderState) {
difficulty_settings: readDifficultySettings(state),
asteroids: readAsteroids(state), // v2
};
} else if (state.gt_v2_0) {
} else if (state.versionAtLeast(2, 0)) {
return {
pollution: readPollution(state),
steering: readSteering(state),
@@ -460,8 +468,7 @@ export function readMapExchangeString(exchangeString: string) {
try {
const version = readVersion(state);
state.gt_v2_0 = version[0] >= 2;
state.gt_v2_1 = version[0] >= 2 && version[1] >= 1;
state.version = version;
data = {
version: version,
unknown: readUInt8(state),
+7 -3
View File
@@ -1211,10 +1211,14 @@ describe("Integration of Clusterio", function() {
await execCtl("instance start 44");
const instance = (await getInstances()).get(44);
const isV2 = lib.integerPartialVersion(instance.factorioVersion) > lib.integerPartialVersion("2.0");
const isV2_1 = lib.integerPartialVersion(instance.factorioVersion) > lib.integerPartialVersion("2.1");
const version = lib.integerPartialVersion(instance.factorioVersion);
const isV2 = version > lib.integerPartialVersion("2.0");
const isV2_1 = version > lib.integerPartialVersion("2.1");
const isV2_1_13 = version >= lib.integerPartialVersion("2.1.13");
let exchangeString = testStrings.modified;
if (isV2_1) {
if (isV2_1_13) {
exchangeString = testStrings.modified_v2_1_13;
} else if (isV2_1) {
exchangeString = testStrings.modified_v2_1;
} else if (isV2) {
exchangeString = testStrings.modified_v2;
+10
View File
@@ -69,6 +69,16 @@ describe("lib/factorio/exchange_string", function() {
});
});
it("should parse post v2.1.13 strings", function() {
assert.deepEqual(lib.readMapExchangeString(testStrings.modified_v2_1_13), {
version: [2, 1, 14, 1],
unknown: 0,
map_gen_settings: testSettings.modified_v2_1_map_gen_settings,
map_settings: testSettings.modified_v2_1_13_map_settings,
checksum: 272054359,
});
});
it("should handle malformed strings", function() {
assert.throws(
() => lib.readMapExchangeString("<<blah>>"),
+9
View File
@@ -615,6 +615,14 @@ const modified_v2_1_map_settings = {
},
};
const modified_v2_1_13_map_settings = {
...modified_v2_1_map_settings,
"enemy_expansion": {
...modified_v2_1_map_settings.enemy_expansion,
"build_base_unit_dispatch_cooldown": 1234,
},
};
const modified_space_age_map_gen_settings = {
autoplace_controls: {
"aquilo_crude_oil": { frequency: 1, size: 1, richness: 1 },
@@ -703,5 +711,6 @@ module.exports = {
modified_v2_map_settings,
modified_v2_1_map_gen_settings,
modified_v2_1_map_settings,
modified_v2_1_13_map_settings,
modified_space_age_map_gen_settings,
};
+17
View File
@@ -86,10 +86,27 @@ Gx0+5gy4RUpPimXd3JJg0hSVAZVaD42oXlGs+OdLeL8NIjXUO043NJp
38bydxUydeQi4j66kCmzxzUMyYTVaPneF1Uj+vlxQH+ZT82gl0Du2he
Y+lJ/JEpK/WZNmBM/mgXgnKlfCACLzljjH+s/hYo=<<<`;
const modified_v2_1_13 = `
>>>eNp1U89rE0EUftMmNYm15hAFRWoOPYiQEKKgFMmOXsSDB0HwJHGy
mejSzU47u1utHszBgwdBkF70olcrehHvAS/2JvUfqNSDvbVSpAehzsv
sTNYEH7zHt9/7/YadAAIzSmE64wrmA/QcowVXLC5yWRGSA4Cl866M27
wiPD/F0gIPeHel0mIhBlOA/sCV86QIdIW+rTAdsHjZC5uu73U66Y5ZK
dyFMM0cDyMmIy+422SSs2ZXeGEUY7VUUhiJIGmaMJHkPHy3tuYobSBz
OJYs8OKunqSHvI68zyIucQ8TCeTjr9vrvSezgHrwGMoHB6gKbao4VCA
9HU0UmUi26IogksKfZ/GD+ZbHwnylVq3XUI6lXR3Jl2IeuCu5erX2r9
ssN5J+csw/rFGrnhtInvt8mUWeCI5a1PTZAg+zyZGhfFnpFXs4Qsij0
oer3x+uOkSvUKUJ2E+Yfssw1xLw4j39nytjXHDegB1HNdm9t/X00/5e
g/x5u7txvXXHITd/FJfCs9sN5TyCx5xEM2WRzt00U+XoKKPAq5co2w7
JYkZpmFakZGd1AkjtxhSQjYziizPKvHmmTPmUDWuYoiVKOgP57RB9yS
0Dvo1NMEfJJWw1i+YLmuywM1C1jwbPKaGnjffEMETl1yE9Qxu/9C7rp
u3nVP+RQebGLpzeY4SxwbkUKGDDtjU/J9P3/nrIfNHXdPAWgFF7JHkZ
0L+YLmXPjcc3XoAz1D69BVjk1sULxb83nuUA<<<`;
module.exports = {
default: default_,
modified,
modified_v2,
modified_v2_1,
modified_v2_1_13,
modified_space_age,
};