UNPKG

@clusterio/lib

Version:
450 lines (405 loc) 14.9 kB
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 */ v2 = false; constructor( public buf: Buffer ) { } } function readUInt8(state: MapReaderState) { let value = state.buf.readUInt8(state.pos); state.pos += 1; return value; } function readBool(state: MapReaderState) { let value = readUInt8(state) !== 0; return value; } function readInt16(state: MapReaderState) { let value = state.buf.readInt16LE(state.pos); state.pos += 2; return value; } function readUInt16(state: MapReaderState) { let value = state.buf.readUInt16LE(state.pos); state.pos += 2; return value; } function readInt32(state: MapReaderState) { let value = state.buf.readInt32LE(state.pos); state.pos += 4; return value; } function readUInt32(state: MapReaderState) { let value = state.buf.readUInt32LE(state.pos); state.pos += 4; return value; } function readUInt32so(state: MapReaderState) { let value = readUInt8(state); if (value === 0xff) { return readUInt32(state); } return value; } function readFloat(state: MapReaderState) { let value = state.buf.readFloatLE(state.pos); state.pos += 4; return value; } function readDouble(state: MapReaderState) { let value = state.buf.readDoubleLE(state.pos); state.pos += 8; return value; } function readString(state: MapReaderState) { let size = readUInt32so(state); let data = state.buf.subarray(state.pos, state.pos + size).toString("utf-8"); state.pos += size; return data; } function readOptional<T>(state: MapReaderState, readValue: (s: MapReaderState) => T) { let load = readUInt8(state) !== 0; if (!load) { return null; } return readValue(state); } function readArray<T>(state: MapReaderState, readItem: (s: MapReaderState) => T) { let size = readUInt32so(state); let array: T[] = []; for (let i = 0; i < size; i++) { let item = readItem(state); array.push(item); } return array; } function readDict<K, V>( state: MapReaderState, readKey: (s: MapReaderState) => K, readValue: (s: MapReaderState) => V ) { let size = readUInt32so(state); let mapping = new Map<K, V>(); for (let i = 0; i < size; i++) { let key = readKey(state); let value = readValue(state); mapping.set(key, value); } return mapping; } function readVersion(state: MapReaderState) { let major = readUInt16(state); let minor = readUInt16(state); let patch = readUInt16(state); let developer = readUInt16(state); return [major, minor, patch, developer]; } function readFrequencySizeRichness(state: MapReaderState) { return { frequency: readFloat(state), size: readFloat(state), richness: readFloat(state), }; } function readAutoplaceSetting(state: MapReaderState) { return { treat_missing_as_default: readBool(state), settings: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)), }; } function readMapPosition(state: MapReaderState) { let x: number, y: number; let x_diff = readInt16(state) / 256; if (x_diff === 0x7fff / 256) { x = readInt32(state) / 256; y = readInt32(state) / 256; } else { let y_diff = readInt16(state) / 256; x = state.last_position.x + x_diff; y = state.last_position.y + y_diff; } state.last_position.x = x; state.last_position.x = y; return { x, y }; } function readBoundingBox(state: MapReaderState) { return { left_top: readMapPosition(state), right_bottom: readMapPosition(state), orientation: { x: readInt16(state), y: readInt16(state), }, }; } function readCliffSettings(state: MapReaderState) { return state.v2 ? { name: readString(state), control: readString(state), // v2 cliff_elevation_0: readFloat(state), cliff_elevation_interval: readFloat(state), richness: readFloat(state), cliff_smoothing: readFloat(state), // v2 } : { name: readString(state), cliff_elevation_0: readFloat(state), cliff_elevation_interval: readFloat(state), richness: readFloat(state), }; } function readTerritorySettings(state: MapReaderState) { return { units: readArray(state, readString), territory_index_expression: readString(state), territory_variation_expression: readString(state), minimum_territory_size: readUInt32(state), }; } function readMapGenSettings(state: MapReaderState) { return state.v2 ? { autoplace_controls: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)), autoplace_settings: Object.fromEntries(readDict(state, readString, readAutoplaceSetting)), default_enable_all_autoplace_controls: readBool(state), seed: readUInt32(state), width: readUInt32(state), height: readUInt32(state), area_to_generate_at_start: readBoundingBox(state), starting_area: readFloat(state), peaceful_mode: readBool(state), no_enemies_mode: readBool(state), // v2 starting_points: readArray(state, readMapPosition), property_expression_names: Object.fromEntries(readDict(state, readString, readString)), cliff_settings: readCliffSettings(state), territory_settings: readOptional(state, readTerritorySettings), // v2 } : { terrain_segmentation: readFloat(state), // v1 water: readFloat(state), // v1 autoplace_controls: Object.fromEntries(readDict(state, readString, readFrequencySizeRichness)), autoplace_settings: Object.fromEntries(readDict(state, readString, readAutoplaceSetting)), default_enable_all_autoplace_controls: readBool(state), seed: readUInt32(state), width: readUInt32(state), height: readUInt32(state), area_to_generate_at_start: readBoundingBox(state), starting_area: readFloat(state), peaceful_mode: readBool(state), starting_points: readArray(state, readMapPosition), property_expression_names: Object.fromEntries(readDict(state, readString, readString)), cliff_settings: readCliffSettings(state), }; } function readPollution(state: MapReaderState) { return { enabled: readOptional(state, readBool), diffusion_ratio: readOptional(state, readDouble), min_to_diffuse: readOptional(state, readDouble), ageing: readOptional(state, readDouble), expected_max_per_chunk: readOptional(state, readDouble), min_to_show_per_chunk: readOptional(state, readDouble), min_pollution_to_damage_trees: readOptional(state, readDouble), pollution_with_max_forest_damage: readOptional(state, readDouble), pollution_per_tree_damage: readOptional(state, readDouble), pollution_restored_per_tree_damage: readOptional(state, readDouble), max_pollution_to_restore_trees: readOptional(state, readDouble), enemy_attack_pollution_consumption_modifier: readOptional(state, readDouble), }; } function readSteeringValue(state: MapReaderState) { return { radius: readOptional(state, readDouble), separation_factor: readOptional(state, readDouble), separation_force: readOptional(state, readDouble), force_unit_fuzzy_goto_behavior: readOptional(state, readBool), }; } function readSteering(state: MapReaderState) { return { default: readSteeringValue(state), moving: readSteeringValue(state), }; } function readEnemyEvolution(state: MapReaderState) { return { enabled: readOptional(state, readBool), time_factor: readOptional(state, readDouble), destroy_factor: readOptional(state, readDouble), pollution_factor: readOptional(state, readDouble), }; } function readEnemyExpansion(state: MapReaderState) { return { enabled: readOptional(state, readBool), max_expansion_distance: readOptional(state, readUInt32), 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), min_expansion_cooldown: readOptional(state, readUInt32), max_expansion_cooldown: readOptional(state, readUInt32), }; } function readUnitGroup(state: MapReaderState) { return { min_group_gathering_time: readOptional(state, readUInt32), max_group_gathering_time: readOptional(state, readUInt32), max_wait_time_for_late_members: readOptional(state, readUInt32), max_group_radius: readOptional(state, readDouble), min_group_radius: readOptional(state, readDouble), max_member_speedup_when_behind: readOptional(state, readDouble), max_member_slowdown_when_ahead: readOptional(state, readDouble), max_group_slowdown_factor: readOptional(state, readDouble), max_group_member_fallback_factor: readOptional(state, readDouble), member_disown_distance: readOptional(state, readDouble), tick_tolerance_when_member_arrives: readOptional(state, readUInt32), max_gathering_unit_groups: readOptional(state, readUInt32), max_unit_group_size: readOptional(state, readUInt32), }; } function readPathFinder(state: MapReaderState) { return { fwd2bwd_ratio: readOptional(state, readInt32), goal_pressure_ratio: readOptional(state, readDouble), use_path_cache: readOptional(state, readBool), max_steps_worked_per_tick: readOptional(state, readDouble), max_work_done_per_tick: readOptional(state, readUInt32), short_cache_size: readOptional(state, readUInt32), long_cache_size: readOptional(state, readUInt32), short_cache_min_cacheable_distance: readOptional(state, readDouble), short_cache_min_algo_steps_to_cache: readOptional(state, readUInt32), long_cache_min_cacheable_distance: readOptional(state, readDouble), cache_max_connect_to_cache_steps_multiplier: readOptional(state, readUInt32), cache_accept_path_start_distance_ratio: readOptional(state, readDouble), cache_accept_path_end_distance_ratio: readOptional(state, readDouble), negative_cache_accept_path_start_distance_ratio: readOptional(state, readDouble), negative_cache_accept_path_end_distance_ratio: readOptional(state, readDouble), cache_path_start_distance_rating_multiplier: readOptional(state, readDouble), cache_path_end_distance_rating_multiplier: readOptional(state, readDouble), stale_enemy_with_same_destination_collision_penalty: readOptional(state, readDouble), ignore_moving_enemy_collision_distance: readOptional(state, readDouble), enemy_with_different_destination_collision_penalty: readOptional(state, readDouble), general_entity_collision_penalty: readOptional(state, readDouble), general_entity_subsequent_collision_penalty: readOptional(state, readDouble), extended_collision_penalty: readOptional(state, readDouble), max_clients_to_accept_any_new_request: readOptional(state, readUInt32), max_clients_to_accept_short_new_request: readOptional(state, readUInt32), direct_distance_to_consider_short_request: readOptional(state, readUInt32), short_request_max_steps: readOptional(state, readUInt32), short_request_ratio: readOptional(state, readDouble), min_steps_to_check_path_find_termination: readOptional(state, readUInt32), start_to_goal_cost_multiplier_to_terminate_path_find: readOptional(state, readDouble), overload_levels: readOptional(state, (p) => readArray(p, readUInt32)), overload_multipliers: readOptional(state, (p) => readArray(p, readDouble)), negative_path_cache_delay_interval: readOptional(state, readUInt32), }; } function readDifficultySettings(state: MapReaderState) { return state.v2 ? { technology_price_multiplier: readDouble(state), spoil_time_modifier: readDouble(state), // v2 } : { recipe_difficulty: readUInt8(state), // v1 technology_difficulty: readUInt8(state), // v1 technology_price_multiplier: readDouble(state), research_queue_setting: ["always", "after-victory", "never"][readUInt8(state)], // v1 }; } function readAsteroids(state: MapReaderState) { return { spawning_rate: readOptional(state, readDouble) ?? 1, max_ray_portals_expanded_per_tick: readOptional(state, readUInt32) ?? 100, }; } 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 } : { 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), }; } export interface MapExchangeData { /** Version of Factorio the string was created with. */ version: ReturnType<typeof readVersion>; unknown: number; /** * Decoded map generator settings in the format the --map-gen-settings * command line option to Factorio expect. */ map_gen_settings: ReturnType<typeof readMapGenSettings>; /** * Decoded map settings in the format the --map-settings command line * option to Factorio expects. */ map_settings: ReturnType<typeof readMapSettings>; /** CRC32 checksum for the exchange string. */ checksum: number; } /** * Parse a Map Exchange String * * Reads and decodes the data in the given map exchange string and returns * data structures that can be fed into the Factorio server when creating a * save to set the map gen settings and the map settings for the save. * * @param exchangeString - Max Exchange String to parse. * @returns Parsed result. */ export function readMapExchangeString(exchangeString: string) { exchangeString = exchangeString.replace(/[ \t\n\r]+/g, ""); if (!/>>>[0-9a-zA-Z\/+]+={0,3}<<</.test(exchangeString)) { throw new Error("Not a map exchange string"); } let buf = Buffer.from(exchangeString.slice(3, -3), "base64"); try { // eslint-disable-next-line node/no-sync buf = zlib.inflateSync(buf); } catch (err: any) { if (err.code.startsWith("Z_")) { throw new Error("Malformed map exchange string: zlib inflate failed"); } } let state = new MapReaderState(buf); let data: MapExchangeData; try { const version = readVersion(state); state.v2 = version[0] >= 2; data = { version: version, unknown: readUInt8(state), map_gen_settings: readMapGenSettings(state), map_settings: readMapSettings(state), checksum: readUInt32(state), }; } catch (err: any) { if (err.code === "ERR_OUT_OF_RANGE") { throw new Error("Malformed map exchange string: reached end before finishing parsing"); } throw err; } if (state.pos !== buf.length) { throw new Error("Malformed map exchange string: data after end"); } return data; }