diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index e029119d..f2ec0aec 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -13,9 +13,9 @@ jobs: strategy: matrix: # Supported versions are active lts and maintenance see https://nodejs.org/en/about/releases/ - node-version: [20.x, 22.x] + node-version: [22.x, 24.x] # Supported versions are latest stable and one minor version before stable (even if from a previous major) - factorio-version: [1.1.110, 2.0.47] + factorio-version: [1.1.110, 2.0.77, 2.1.8] steps: - uses: actions/checkout@v4 @@ -54,7 +54,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 24.x - run: pnpm i --no-frozen-lockfile - run: pnpm run lint @@ -65,7 +65,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 24.x - run: pnpm i --no-frozen-lockfile - run: pnpm run build-mod # Upload build to artifacts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c6942d45..c2ebbb98 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,6 +6,10 @@ on: types: - released +permissions: + id-token: write + contents: read + jobs: npm: runs-on: ubuntu-latest @@ -15,12 +19,10 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 22.x + node-version: 24.x registry-url: https://registry.npmjs.org - run: pnpm install --no-frozen-lockfile - run: pnpm -r --filter='./{plugins,packages}/*' publish --no-git-checks - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - uses: softprops/action-gh-release@v2 with: files: pnpm-lock.yaml @@ -33,7 +35,7 @@ jobs: - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 22.x + node-version: 24.x - run: pnpm install --no-frozen-lockfile - run: pnpm -r --filter='./{plugins,packages}/*' run build-mod --output-dir ../../dist - run: node ./publish.js factorio ./dist 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/lua/clusterio_lib/info.json b/packages/host/lua/clusterio_lib/info.json index ed6dcfb8..aae5af7f 100644 --- a/packages/host/lua/clusterio_lib/info.json +++ b/packages/host/lua/clusterio_lib/info.json @@ -16,6 +16,10 @@ { "version": "2.0.20", "factorio_version": "2.0" + }, + { + "version": "2.0.21", + "factorio_version": "2.1" } ], "title": "Clusterio Library (Alpha)", diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 0985edfd..b99392dc 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -659,10 +659,8 @@ export default class Host extends lib.Link { return await this.downloadMod(mod); } - 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"); + async fetchMods(mods: Iterable, factorioVersion: lib.PartialVersion) { + const builtinModNames = lib.ModPack.getBuiltinModNames(factorioVersion); const modInfos: Promise[] = []; for (const mod of mods) { if (builtinModNames.includes(mod.name)) { diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index ac72da58..19f10032 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -797,9 +797,7 @@ end`.replace(/\r?\n/g, " "); } this.activeModPack = modPack; - // TODO validate factorioVersion - - const mods = await this._host.fetchMods(modPack.mods.values()); + const mods = await this._host.fetchMods(modPack.mods.values(), modPack.factorioVersion); await fs.mkdir(this.path("mods"), { recursive: true }); 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..4a529471 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -363,7 +363,11 @@ before(async function() { controllerProcess = await spawnNode("controller:", "../../packages/controller run", /Started controller/); - haveFactorioInstall = (await _listFactorioVersions(factorioDir)).versions.size > 0; + const factorioVersions = (await _listFactorioVersions(factorioDir)).versions; + const latestFactorioVersion = [...factorioVersions.values()] + .sort((a, b) => lib.integerFullVersion(b) - lib.integerFullVersion(a))[0]; + haveFactorioInstall = factorioVersions.size > 0; + const relativeFactorioDir = path.isAbsolute(factorioDir) ? factorioDir : path.join("..", "..", factorioDir); await execCtlProcess("host create-config --id 4 --name host --generate-token"); await execHost(`config set host.factorio_directory ${relativeFactorioDir}`); @@ -382,8 +386,11 @@ 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 = latestFactorioVersion; + + const [major, minor] = lib.normaliseMajorMinorVersion(latestFactorioVersion).split("."); + testPack.mods.set("clusterio_lib", { name: "clusterio_lib", enabled: true, version: `2.0.${major}${minor}` }); + 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, };