mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 23:14:53 +09:00
Remove extra whitespace, use more consistent spacing, add missing semicolons, using dangling commas, break up long lines, consistent multiline operator placement, add missing JSDoc strings and so on.
395 lines
12 KiB
TypeScript
395 lines
12 KiB
TypeScript
import zlib from "zlib";
|
|
|
|
class MapReaderState {
|
|
pos = 0;
|
|
last_position = { x: 0, y: 0 };
|
|
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 {
|
|
name: readString(state),
|
|
elevation_0: readFloat(state),
|
|
elevation_interval: readFloat(state),
|
|
richness: readFloat(state),
|
|
};
|
|
}
|
|
|
|
function readMapGenSettings(state: MapReaderState) {
|
|
return {
|
|
terrain_segmentation: readFloat(state),
|
|
water: readFloat(state),
|
|
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 {
|
|
recipe_difficulty: readUInt8(state),
|
|
technology_difficulty: readUInt8(state),
|
|
technology_price_multiplier: readDouble(state),
|
|
research_queue_setting: ["always", "after-victory", "never"][readUInt8(state)],
|
|
};
|
|
}
|
|
|
|
function readMapSettings(state: MapReaderState) {
|
|
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),
|
|
};
|
|
}
|
|
|
|
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 {
|
|
data = {
|
|
version: readVersion(state),
|
|
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;
|
|
}
|