From 8b4c8ad337de600c32c8c9ab207a28bb6bcc8b62 Mon Sep 17 00:00:00 2001 From: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> Date: Fri, 3 Jul 2026 22:33:43 +0100 Subject: [PATCH] New exchange string format for enemy expansion settings --- packages/ctl/src/commands_debug.ts | 20 +++ packages/host/src/Host.ts | 2 +- packages/lib/src/factorio/exchange_string.ts | 58 +++++-- test/integration/clusterio.js | 11 +- test/integration/index.js | 4 +- test/lib/factorio/exchange_string.js | 10 ++ test/lib/factorio/test_settings.js | 150 +++++++++++++++++++ test/lib/factorio/test_strings.js | 17 +++ 8 files changed, 255 insertions(+), 17 deletions(-) diff --git a/packages/ctl/src/commands_debug.ts b/packages/ctl/src/commands_debug.ts index 66559bd1..af991afc 100644 --- a/packages/ctl/src/commands_debug.ts +++ b/packages/ctl/src/commands_debug.ts @@ -9,3 +9,23 @@ debugCommands.add(new lib.Command({ control.keepOpen = true; }, })); + +debugCommands.add(new lib.Command({ + definition: [ + "parse-exchange-string ", + "Parse and print the contents of an exchange string", + (yargs) => { + yargs.positional("exchange-string", { describe: "String to parse", type: "string" }); + yargs.option("pretty", { alias: "p", type: "boolean", description: "Pretty output" }); + }], + handler: async function(args: { exchangeString: string, pretty: boolean }, control: Control) { + const result = lib.readMapExchangeString(args.exchangeString); + if (args.pretty) { + // eslint-disable-next-line no-console + console.log(JSON.stringify(result, null, 2)); + } else { + // eslint-disable-next-line no-console + console.log(JSON.stringify(result)); + } + }, +})); diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 0985edfd..6f2a193a 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -662,7 +662,7 @@ export default class Host extends lib.Link { async fetchMods(mods: Iterable) { // This is better than the previous hard coded names // But it really shouldn't be a hard coded version either - const builtinModNames = lib.ModPack.getBuiltinModNames("2.0"); + const builtinModNames = lib.ModPack.getBuiltinModNames("2.1"); const modInfos: Promise[] = []; for (const mod of mods) { if (builtinModNames.includes(mod.name)) { diff --git a/packages/lib/src/factorio/exchange_string.ts b/packages/lib/src/factorio/exchange_string.ts index 6f7e3380..04a7d333 100644 --- a/packages/lib/src/factorio/exchange_string.ts +++ b/packages/lib/src/factorio/exchange_string.ts @@ -5,6 +5,8 @@ class MapReaderState { last_position = { x: 0, y: 0 }; /** True when a version greater than 2.0.0 is detected */ v2 = false; + /** True when a version greater than 2.1.0 is detected */ + v2_1 = false; constructor( public buf: Buffer ) { } @@ -262,7 +264,23 @@ function readEnemyEvolution(state: MapReaderState) { } function readEnemyExpansion(state: MapReaderState) { - return { + return state.v2_1 ? { + enabled: readOptional(state, readBool), + max_expansion_distance: readOptional(state, readUInt32), + min_expansion_distance: readOptional(state, readUInt32), // v2.1 + friendly_base_influence_radius: readOptional(state, readUInt32), + enemy_building_influence_radius: readOptional(state, readUInt32), + building_coefficient: readOptional(state, readDouble), + other_base_coefficient: readOptional(state, readDouble), + neighbouring_chunk_coefficient: readOptional(state, readDouble), + neighbouring_base_chunk_coefficient: readOptional(state, readDouble), + max_colliding_tiles_coefficient: readOptional(state, readDouble), + settler_group_min_size: readOptional(state, readUInt32), + settler_group_max_size: readOptional(state, readUInt32), + evolution_group_size_factor: readOptional(state, readDouble), // v2.1 + min_expansion_cooldown: readOptional(state, readUInt32), + max_expansion_cooldown: readOptional(state, readUInt32), + } : { enabled: readOptional(state, readBool), max_expansion_distance: readOptional(state, readUInt32), friendly_base_influence_radius: readOptional(state, readUInt32), @@ -355,17 +373,32 @@ function readAsteroids(state: MapReaderState) { } function readMapSettings(state: MapReaderState) { - return state.v2 ? { - pollution: readPollution(state), - steering: readSteering(state), - enemy_evolution: readEnemyEvolution(state), - enemy_expansion: readEnemyExpansion(state), - unit_group: readUnitGroup(state), - path_finder: readPathFinder(state), - max_failed_behavior_count: readUInt32(state), - difficulty_settings: readDifficultySettings(state), - asteroids: readAsteroids(state), // v2 - } : { + if (state.v2_1) { + return { + pollution: readPollution(state), + // steering removed v2.1 + enemy_evolution: readEnemyEvolution(state), + enemy_expansion: readEnemyExpansion(state), + unit_group: readUnitGroup(state), + path_finder: readPathFinder(state), + max_failed_behavior_count: readUInt32(state), + difficulty_settings: readDifficultySettings(state), + asteroids: readAsteroids(state), // v2 + }; + } else if (state.v2) { + return { + pollution: readPollution(state), + steering: readSteering(state), + enemy_evolution: readEnemyEvolution(state), + enemy_expansion: readEnemyExpansion(state), + unit_group: readUnitGroup(state), + path_finder: readPathFinder(state), + max_failed_behavior_count: readUInt32(state), + difficulty_settings: readDifficultySettings(state), + asteroids: readAsteroids(state), // v2 + }; + } + return { pollution: readPollution(state), steering: readSteering(state), enemy_evolution: readEnemyEvolution(state), @@ -427,6 +460,7 @@ export function readMapExchangeString(exchangeString: string) { try { const version = readVersion(state); state.v2 = version[0] >= 2; + state.v2_1 = version[0] >= 2 && version[1] >= 1; data = { version: version, unknown: readUInt8(state), diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index f1aa80c5..2729eb8a 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -1150,8 +1150,15 @@ 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 exchangeString = (isV2 ? testStrings.modified_v2 : testStrings.modified).replace(/[\n\r]+/g, ""); - const args = `base/freeplay --seed 1234 --map-exchange-string "${exchangeString}"`; + const isV2_1 = lib.integerPartialVersion(instance.factorioVersion) > lib.integerPartialVersion("2.1"); + let exchangeString = testStrings.modified; + if (isV2_1) { + exchangeString = testStrings.modified_v2_1; + } else if (isV2) { + exchangeString = testStrings.modified_v2; + } + // eslint-disable-next-line max-len + const args = `base/freeplay --seed 1234 --map-exchange-string "${exchangeString.replace(/[\n\r]+/g, "")}"`; await execCtl("instance stop 44"); await execCtl(`instance load-scenario test ${args}`); diff --git a/test/integration/index.js b/test/integration/index.js index c9ea3d97..2f990f19 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -382,8 +382,8 @@ before(async function() { const testPack = lib.ModPack.fromJSON({}); testPack.id = 12; testPack.name = "subspace_storage-pack"; - testPack.factorioVersion = "2.0.0"; - testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: "2.0.20" }); + testPack.factorioVersion = "2.1"; + testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: "2.0.21" }); await control.sendTo("controller", new lib.ModPackCreateRequest(testPack)); await control.sendTo( "controller", diff --git a/test/lib/factorio/exchange_string.js b/test/lib/factorio/exchange_string.js index 08f92187..10d1421f 100644 --- a/test/lib/factorio/exchange_string.js +++ b/test/lib/factorio/exchange_string.js @@ -59,6 +59,16 @@ describe("lib/factorio/exchange_string", function() { }); }); + it("should parse post v2.1 strings", function() { + assert.deepEqual(lib.readMapExchangeString(testStrings.modified_v2_1), { + version: [2, 1, 9, 3], + unknown: 0, + map_gen_settings: testSettings.modified_v2_1_map_gen_settings, + map_settings: testSettings.modified_v2_1_map_settings, + checksum: 3180296799, + }); + }); + it("should handle malformed strings", function() { assert.throws( () => lib.readMapExchangeString("<>"), diff --git a/test/lib/factorio/test_settings.js b/test/lib/factorio/test_settings.js index 6795b99f..3c295c14 100644 --- a/test/lib/factorio/test_settings.js +++ b/test/lib/factorio/test_settings.js @@ -467,6 +467,154 @@ const modified_v2_map_settings = { }, }; +const modified_v2_1_map_gen_settings = { + "autoplace_controls": { + "coal": { "frequency": 1, "size": 1, "richness": 1 }, + "copper-ore": { "frequency": 0.5, "size": 1, "richness": 1 }, + "crude-oil": { "frequency": 0.5, "size": 1, "richness": 4 }, + "enemy-base": { "frequency": 2, "size": 1.5, "richness": 1 }, + "iron-ore": { "frequency": 1.5, "size": 1, "richness": 1 }, + "nauvis_cliff": { "frequency": 1, "size": 1, "richness": 1 }, + "rocks": { "frequency": 1, "size": 1, "richness": 1 }, + "starting_area_moisture": { "frequency": 1, "size": 1, "richness": 1 }, + "stone": { "frequency": 2, "size": 1, "richness": 1 }, + "trees": { "frequency": 1.3333333730697632, "size": 0.3333333432674408, "richness": 1 }, + "uranium-ore": { "frequency": 1, "size": 1.3333333730697632, "richness": 1 }, + "water": { "frequency": 0.5, "size": 0.3333333432674408, "richness": 1 }, + }, + "autoplace_settings": {}, + "default_enable_all_autoplace_controls": true, + "seed": 3378377395, + "width": 2000000, + "height": 2000000, + "area_to_generate_at_start": { + "left_top": { "x": -224, "y": -224 }, + "right_bottom": { "x": 224, "y": 224 }, + "orientation": { "x": 0, "y": -32767 }, + }, + "starting_area": 0.3333333432674408, + "peaceful_mode": false, + "no_enemies_mode": false, + "starting_points": [ + { "x": 0, "y": 0 }, + ], + "property_expression_names": { + "control:aux:bias": "-0.200000", + "control:aux:frequency": "2.000000", + "control:moisture:bias": "-0.200000", + "control:moisture:frequency": "0.333333", + "elevation": "elevation_lakes", + }, + "cliff_settings": { + "name": "cliff", + "control": "", + "cliff_elevation_0": 10, + "cliff_elevation_interval": 40, + "richness": 1, + "cliff_smoothing": 1, + }, + "territory_settings": null, +}; + +const modified_v2_1_map_settings = { + "pollution": { + "enabled": true, + "diffusion_ratio": 0.02, + "min_to_diffuse": 15, + "ageing": 1.5, + "expected_max_per_chunk": 150, + "min_to_show_per_chunk": 50, + "min_pollution_to_damage_trees": 3785, + "pollution_with_max_forest_damage": 150, + "pollution_per_tree_damage": 50, + "pollution_restored_per_tree_damage": 3714, + "max_pollution_to_restore_trees": 20, + "enemy_attack_pollution_consumption_modifier": 1, + }, + "enemy_evolution": { + "enabled": true, + "time_factor": 0.00002, + "destroy_factor": 0.002, + "pollution_factor": 0.000012, + }, + "enemy_expansion": { + "enabled": true, + "max_expansion_distance": 13, + "min_expansion_distance": 3, + "friendly_base_influence_radius": 6, + "enemy_building_influence_radius": 3, + "building_coefficient": 0.5, + "other_base_coefficient": 3, + "neighbouring_chunk_coefficient": 0.5, + "neighbouring_base_chunk_coefficient": 0.5, + "max_colliding_tiles_coefficient": 0.8, + "settler_group_min_size": 5, + "settler_group_max_size": 20, + "evolution_group_size_factor": 4, + "min_expansion_cooldown": 169200, + "max_expansion_cooldown": 414000, + }, + "unit_group": { + "min_group_gathering_time": 3600, + "max_group_gathering_time": 36000, + "max_wait_time_for_late_members": 7200, + "max_group_radius": 30, + "min_group_radius": 5, + "max_member_speedup_when_behind": 1.4, + "max_member_slowdown_when_ahead": 0.6, + "max_group_slowdown_factor": 0.3, + "max_group_member_fallback_factor": 3, + "member_disown_distance": 10, + "tick_tolerance_when_member_arrives": 60, + "max_gathering_unit_groups": 30, + "max_unit_group_size": 200, + }, + "path_finder": { + "fwd2bwd_ratio": 5, + "goal_pressure_ratio": 2, + "use_path_cache": true, + "max_steps_worked_per_tick": 1000, + "max_work_done_per_tick": 8000, + "short_cache_size": 5, + "long_cache_size": 25, + "short_cache_min_cacheable_distance": 10, + "short_cache_min_algo_steps_to_cache": 50, + "long_cache_min_cacheable_distance": 30, + "cache_max_connect_to_cache_steps_multiplier": 100, + "cache_accept_path_start_distance_ratio": 0.2, + "cache_accept_path_end_distance_ratio": 0.15, + "negative_cache_accept_path_start_distance_ratio": 0.3, + "negative_cache_accept_path_end_distance_ratio": 0.3, + "cache_path_start_distance_rating_multiplier": 10, + "cache_path_end_distance_rating_multiplier": 20, + "stale_enemy_with_same_destination_collision_penalty": 30, + "ignore_moving_enemy_collision_distance": 5, + "enemy_with_different_destination_collision_penalty": 30, + "general_entity_collision_penalty": 10, + "general_entity_subsequent_collision_penalty": 3, + "extended_collision_penalty": 3, + "max_clients_to_accept_any_new_request": 10, + "max_clients_to_accept_short_new_request": 100, + "direct_distance_to_consider_short_request": 100, + "short_request_max_steps": 1000, + "short_request_ratio": 0.5, + "min_steps_to_check_path_find_termination": 2000, + "start_to_goal_cost_multiplier_to_terminate_path_find": 2000, + "overload_levels": [0, 100, 500], + "overload_multipliers": [2, 3, 4], + "negative_path_cache_delay_interval": 20, + }, + "max_failed_behavior_count": 3, + "difficulty_settings": { + "technology_price_multiplier": 12, + "spoil_time_modifier": 1, + }, + "asteroids": { + "spawning_rate": 1, + "max_ray_portals_expanded_per_tick": 100, + }, +}; + const modified_space_age_map_gen_settings = { autoplace_controls: { "aquilo_crude_oil": { frequency: 1, size: 1, richness: 1 }, @@ -553,5 +701,7 @@ module.exports = { modified_map_settings, modified_v2_map_gen_settings, modified_v2_map_settings, + modified_v2_1_map_gen_settings, + modified_v2_1_map_settings, modified_space_age_map_gen_settings, }; diff --git a/test/lib/factorio/test_strings.js b/test/lib/factorio/test_strings.js index 6c724629..8b81be75 100644 --- a/test/lib/factorio/test_strings.js +++ b/test/lib/factorio/test_strings.js @@ -49,6 +49,22 @@ Ln6IET6h2alhiMgvQ3SGxnDDDd32daT/yCD5fQ8R3WMEyeN/PENaNmy E5utYOI24z/cT+oQf48FdgozaFZg6qX+1KqW+GYyy4qNZgFNY/0IAHj RI5Q/27QvL<<<`; +const modified_v2_1 = ` +>>>eNp1U89rE0EUftM0NYlVc4iCIjWHHkRICNFTkezoRTx4ELxKnGwm +unSz087uVlsP5uDBg1CQXvSiVyt6EY9CwIu9if4DlXqwt1aK9CDEeZm +dyZrgg/f49nu/37BTQCAPGYDZaVcwH6DnGC24YmmJy4qQHAAsnXdl3O +YV4fkplhZ4wLurlRYLMZgC9IeunCdFoCv0bYXZgMUrXth0fa/TSXfMS +uEuhmnmVBgxGXnB3SaTnDW7wgujGKulksJIBEnThIkk5+GbzU1HaQOZ +o7FkgRd39SQ95HXkfRZxiXuYSCDvf93e6j2eA9TBIygPBqgKbas4VCA +9HU0UmUi26IogksJfYPGDhZbHwnylVq3XUE6mXR3Jl2MeuKu5erX2r9 +ssN5Z+ZsI/qlGrXhxKnvt8hUWeCE5Y1PTZIg+zyZGhfEXpVXs4QsjD0 +rtr39c2HKJXqNIEHCZMv2WY6wl49pb+zzVtXHDJgD1HNdm/t/Pkw+FB +g/x5vf/1RuuOQ279KC6HF3YbynkMj5lBM2ORzt02U+XoOKPAi+couw7 +JYkZplFakZG9jCkjt5gyQ4nHFv3qqTPmsjWiYeiVKOkP57RB9xB0Dvk +00n6fkMnaZQ/MZTXbUFKhaRYN1Sug54z09ClH5dUjP0MYvvcaWafsp1 +X9skPmJ46b3GGNscC4FCtiwbc3PTPrUX46YL/qSDp8BMOqAJI8C+u/S +peyl8e7GC3Ce2le3AIs019Y//gU3lOV1<<<`; + const modified_space_age = ` >>>eNp1VEFoE0EUnUkb27TWBomFSqkBexAhMVZPRbJbL+LBg+BV1slm kgzd7LYzs9HYgz304EEQpJf2Yq9W9CIevBW8WFAQPXmr1IOCh1aL9CD @@ -74,5 +90,6 @@ module.exports = { default: default_, modified, modified_v2, + modified_v2_1, modified_space_age, };