diff --git a/doc/source/api_reference.rst b/doc/source/api_reference.rst index d6f7f65..a528514 100644 --- a/doc/source/api_reference.rst +++ b/doc/source/api_reference.rst @@ -1,17 +1,26 @@ API Reference ============= -FBG contain three primary modules +The following modules give access to the high level functions of FBG -* :mod:`solver` -* :mod:`layout` -* :mod:`flow` +* :mod:`solver` - generate blueprint +* :mod:`analyze` - analyze blueprint + +These modules provide basic functionality. + +* :mod:`layout` - elements located on a grid +* :mod:`flow` - graph representing flow between elements Solver ------ .. automodule:: solver :members: +Analyze +------- +.. automodule:: analyze + :members: + Layout ------ .. automodule:: layout diff --git a/server/analyze.py b/server/analyze.py index b94d40f..42b53a2 100644 --- a/server/analyze.py +++ b/server/analyze.py @@ -5,10 +5,13 @@ - [TODO] expand flow graph for desired production ''' +import logging + from vector import Vector import flow +log = logging.getLogger(__name__) # Categorize entity types # TODO: This should be removed once all types are supported @@ -89,14 +92,15 @@ def vec_from_xydict(xydict): - '''Convert a blueprint position to a Vector''' + # Convert a blueprint position to a Vector return Vector(xydict['x'], xydict['y']) def vec_from_dir(dir): return Vector(1, 0) -def extract_flow_from_site(site): +def extract_flow_from_site(site) -> flow.Graph: '''Extract flow graph from construction site + :param site: A construction site with machines and belts :return: A flow.Graph with constraints set from entity prototype values. You can use this as input to flow. @@ -119,9 +123,9 @@ def extract_flow_from_site(site): except KeyError as ex: raise ValueError(f'Entity is incomplete: {entity}') from ex except: - print(entity) + log.debug(entity) raise - print(G) + log.debug(G) # Group entities by type entity_kind_list = {} @@ -132,30 +136,43 @@ def extract_flow_from_site(site): entity_kind_list[kind] = ekl ekl.append(enr) + BELT_NORMAL = { + 'transport-belt', + 'fast-transport-belt', + 'express-transport-belt', + } + + def all_entities_of_kind(kind_set): + return [entity_id + for kind in kind_set + for entity_id in entity_kind_list.get(kind, []) + ] + # Link transport belts - for belt in entity_kind_list.get('transport-belt', []): + for belt in all_entities_of_kind(BELT_NORMAL): flow_dir = vec_from_dir(entity_dir[belt]) next_pos = entity_center[belt] + flow_dir next_belt = center_entity.get(next_pos) - print(f'belt {belt} at {entity_center[belt]} reach for {next_pos}, found belt {next_belt}') + log.debug(f'belt {belt} at {entity_center[belt]} reach for {next_pos}, found belt {next_belt}') if next_belt: G.add_edge(belt, next_belt) - print('---- after belt linked up ----') - print(G) + log.debug('---- after belt linked up ----') + log.debug(G) raise NotImplementedError() return G -def extract_flow_from_blueprint(bp_dict): +def extract_flow_from_blueprint(bp_dict) -> flow.Graph: '''Extract flow graph from blueprint + :param bp_dict: A blueprint dict as exported from :return: A flow.Graph with constraints set from entity prototype values. You can use this as input to flow. ''' assert isinstance(bp_dict, dict) if not 'blueprint' in bp_dict: raise ValueError('Dict does not contain a blueprint') - print(f'Blueprint content: {bp_dict["blueprint"].keys()}') + log.debug(f'Blueprint content: {bp_dict["blueprint"].keys()}') if not 'entities' in bp_dict['blueprint']: raise ValueError('Not a valid blueprint dict. No entities found') entity_list = bp_dict['blueprint']['entities'] diff --git a/server/cli.py b/server/cli.py index 03c16e1..fc13664 100644 --- a/server/cli.py +++ b/server/cli.py @@ -1,10 +1,20 @@ '''This is a command line interface to the main functions found in the server. It can be used for testing, or simply for scripting access to GERD featuers''' +import logging + import click import analyze import layout +import flow + +# Set up logging +logging.basicConfig( + filename='cli.log', filemode='w', + level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(message)s') +log = logging.getLogger() def load_blueprint(filename): '''Load and validate a string-encoded blueprint from a file.''' @@ -78,6 +88,15 @@ def show_blueprint_stats(bp_dict, show_entity_details): def gerd(): '''Command Line Interface access to some features of Gerd''' +@gerd.command +@click.argument('bp_file', type=click.types.Path(exists=True)) +def decode(bp_file): + '''Decode blueprint string as JSON''' + click.echo(f'Loading blueprint from "{bp_file}"') + bp_dict = load_blueprint(bp_file) + import json + print(json.dumps(bp_dict)) + @gerd.command @click.argument('bp_file', type=click.types.Path(exists=True)) @click.option('-v', '--entity-details/--no-entity-details', default=False, help='Show entity count per type') @@ -98,5 +117,8 @@ def maxflow(bp_file): # Convert blueprint to flow graph G = analyze.extract_flow_from_blueprint(bp_dict) + # Reduce flow to reflect max flow + flow.compute_max_flow(G) + if __name__ == '__main__': gerd() diff --git a/server/constants.py b/server/constants.py index 7f159a4..71a3017 100644 --- a/server/constants.py +++ b/server/constants.py @@ -9,10 +9,11 @@ class Direction(IntEnum): """ - Factorio direction enum. Encompasses all 8 cardinal directions and diagonals + Factorio direction enum. Encompasses all 16 cardinal directions and diagonals where north is 0 and increments clockwise. * ``NORTH`` (Default) + * ``NORTHNORTHEAST`` * ``NORTHEAST`` * ``EAST`` * ``SOUTHEAST`` @@ -22,11 +23,27 @@ class Direction(IntEnum): * ``NORTHWEST`` """ + # Factorio 2.0 has 16 directions, before 2.0 there were 8 NORTH = 0 - NORTHEAST = 1 - EAST = 2 - SOUTHEAST = 3 - SOUTH = 4 - SOUTHWEST = 5 - WEST = 6 - NORTHWEST = 7 + NORTHNORTHEAST = 1 + NORTHEAST = 2 + EASTNORTHEAST = 3 + EAST = 4 + EASTSOUTHEAST = 5 + SOUTHEAST = 6 + SOUTHSOUTHEAST = 7 + SOUTH = 8 + SOUTHSOUTHWEST = 9 + SOUTHWEST = 10 + WESTSOUTHWEST = 11 + WEST = 12 + WESTNORTHWEST = 13 + NORTHWEST = 14 + NORTHNORTHWEST = 15 + + +max_underground_length = { + 'underground-belt': 4, + 'fast-underground-belt': 6, + 'express-underground-belt': 8, +} diff --git a/server/layout.py b/server/layout.py index 99d4c11..de93b5e 100644 --- a/server/layout.py +++ b/server/layout.py @@ -2,6 +2,8 @@ Functions related to placing machines on a grid. ''' +import logging + from constants import Direction MACHINES_WITH_RECIPE = { @@ -9,6 +11,8 @@ 'assembling-machine-2', } +log = logging.getLogger(__name__) + class ConstructionSite: '''Representation of the area to layout a factory''' @@ -170,7 +174,7 @@ def entity_size(entity_name, direc: Direction = 0): size = factoriocalc_entity_size(entity_name) size = ENTITY_SIZE.get(entity_name) if size is None else size assert size is not None, f'Unknown entity {entity_name}' - if direc in [2, 6]: + if direc in [Direction.EAST, Direction.WEST]: # Switch x and y size y, x = size size = [x,y] @@ -215,16 +219,19 @@ def iter_entity_area(entity_name, direc: Direction): raise NotImplementedError(f'Unknown size of entity {entity_name}') return iter_area(size) -def factorio_version_string_as_int(): - '''return a 64 bit integer, corresponding to a version string''' - factorio_major_version = 0 - factorio_minor_version = 17 - factorio_patch_version = 13 - factorio_dev_version = 0xffef - return (factorio_major_version << 48 - | factorio_minor_version << 32 - | factorio_patch_version << 16 - | factorio_dev_version) +def factorio_version_string_as_int(version_string='0.17.13.65519'): + # 65519 = 0xffef = -17 in two's complement 16 bit + '''return a 64 bit integer, corresponding to a version string.''' + version_parts = [int(part) for part in version_string.split('.')] + if len(version_parts) > 4: + raise ValueError('Up to 4 parts accepted in version string') + version_int = 0 + for part in version_parts: + if part < 0 or part > 0xffff: + raise ValueError('Each version string part must fit in 16 bit') + version_int = version_int << 16 | part + version_int <<= 16 * (4 - len(version_parts)) + return version_int def factorio_version_int_as_string(version): '''return string, corresponding to a a 64 bit integer version integer from a blueprint''' @@ -289,9 +296,10 @@ def export_blueprint_dict(bp_dict): return encodedString -def import_blueprint_dict(exchangeString) -> dict: +def import_blueprint_dict(exchangeString, auto_upgrade=True) -> dict: '''Decodes a blueprint exchange string :param exchangeString: A blueprint exchange string + :param auto_upgrade: Determine if blueprints before factorio 2.0 should be upgraded :return: a dict representing the blueprint https://wiki.factorio.com/Blueprint_string_format ''' @@ -308,8 +316,23 @@ def import_blueprint_dict(exchangeString) -> dict: decompressedData = zlib.decompress(decodedString) jsonString = decompressedData.decode("utf-8") bp_dict = json.loads(jsonString) + if auto_upgrade: + upgrade_blueprint_version(bp_dict) return bp_dict +def upgrade_blueprint_version(bp_dict) -> dict: + '''Upgrade blueprint version to the one supported. + A blueprint < 2.0 has only 8 directions, where as >= 2.0 have 16 directions''' + orig_version = bp_dict['blueprint']['version'] + target_version = factorio_version_string_as_int('2.0.0.65535') + if orig_version < factorio_version_string_as_int('2.0'): + log.warning(f'Upgrading blueprint from version 0x{orig_version:016x} to 0x{target_version:016x}') + # 1.x has 8 directions, 2.x has 16 directions + for entity in bp_dict['blueprint']['entities']: + if 'direction' in entity: + entity['direction'] *= 2 + bp_dict['blueprint']['version'] = target_version + def place_blueprint_on_site(site: ConstructionSite, bp_dict, offset=(0,0)): '''Add objects from blueprint dict to construction site at the specified offset @@ -340,7 +363,7 @@ def place_blueprint_on_site(site: ConstructionSite, bp_dict, offset=(0,0)): del kwarg['entity_number'] del kwarg['name'] del kwarg['position'] - print(f'Add entity {kwarg}') + log.debug(f'Add entity {kwarg}') site.add_entity(**kwarg) # diff --git a/server/solver.py b/server/solver.py index 74dfb70..40d3b49 100644 --- a/server/solver.py +++ b/server/solver.py @@ -490,12 +490,7 @@ def connect_machines( else 'fast-underground-belt' if belt == 'fast-transport-belt' else 'express-underground-belt' if belt == 'express-transport-belt' else '') - #underground_belt = 'express-underground-belt' - max_underground_length = { - 'underground-belt': 4, - 'fast-underground-belt': 6, - 'express-underground-belt': 8, - } + from constants import max_underground_length def pos_distance(i,j): return ( abs(pos_list[j][0] - pos_list[i][0]) + abs(pos_list[j][1] - pos_list[i][1]) )