diff --git a/README.md b/README.md index d42bee9..7452440 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,9 @@ docker run -it fbg ``` ### Without docker -To install the server execute the following steps: +To install the server execute the following steps. + +Windows: ``` cd server python -m venv .fbg @@ -35,6 +37,14 @@ python -m venv .fbg pip install -r requirements.txt ``` +Linux or MacOS: +``` +cd server +python -m venv .fbg +source .fbg/bin/activate +pip install -r requirements.txt +``` + ## Run test ``` @@ -61,3 +71,209 @@ make clean html The server API is specified in OpenAPI file server/fbg-api.yaml If the server runs at port 5000 on localhost, the API documentation can be read at http://localhost:5000/ui + + +# Factorio Wiki + +https://wiki.factorio.com + + +# Overall Project Architecture + +## System Overview + +The Factorio Blueprint Generator is a multi-layered system that transforms factory specifications into Factorio blueprint strings. The architecture follows a modular design with clear separation of concerns: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Frontend UI │ +│ (Browser-based Interface) │ +└────────────────────┬────────────────────────────────────────┘ + │ HTTP/JSON + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ API Server Layer │ +│ (Connexion + Flask + OpenAPI) │ +│ │ +│ - GET / (Health check) │ +│ - POST /process (Blueprint generation) │ +│ - POST /compute-flow (Flow analysis) │ +└─────────────┬──────────────────────────┬────────────────────┘ + │ │ + ▼ ▼ + ┌──────────────────┐ ┌──────────────────┐ + │ Blueprint │ │ Flow Analysis │ + │ Generation │ │ Module │ + │ Pipeline │ │ │ + └──────────────────┘ └──────────────────┘ + │ + ┌──────┼──────┐ + ▼ ▼ ▼ + ┌─────────────────────────────────────────┐ + │ Core Processing Modules │ + │ │ + │ - solver.py (Layout & Routing) │ + │ - layout.py (Grid Management) │ + │ - flow.py (Flow Computation) │ + │ - analyze.py (Blueprint Analysis) │ + └─────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────┐ + │ External Dependencies │ + │ │ + │ - factoriocalc (Game mechanics) │ + │ - networkx (Graph algorithms) │ + └─────────────────────────────────────────┘ +``` + +## Core Components + +### 1. **Server Layer** (`server.py`) +- **Framework**: Connexion (OpenAPI framework built on Flask) +- **Port**: 5011 +- **Features**: + - CORS support for cross-origin requests + - OpenAPI specification-driven routing + - Comprehensive logging to `server.log` + +**Endpoints**: +- `GET /` - Health check endpoint +- `POST /process` - Generates a blueprint from input specifications +- `POST /compute-flow` - Analyzes blueprint flow and returns input/output requirements + +### 2. **API Specification** (`fbg-api.yaml`) +- OpenAPI 3.0.3 specification file +- Defines all API endpoints, request/response schemas, and documentation +- Automatically generates interactive API docs accessible at `http://localhost:5011/ui` + +### 3. **Blueprint Generation Pipeline** (`solver.py`) +The main orchestrator for converting factory specifications into blueprints. Key workflow: + +1. **Machine Specification**: Define desired output, input items, and throughput +2. **Factory Calculation**: Uses `factoriocalc` library to compute required machines and recipes +3. **Spatial Layout**: + - Places machines randomly on a construction site + - Applies spring physics simulation for optimal spacing + - Validates placement within grid boundaries +4. **Connection Management**: Routes belts and inserters between machines +5. **Pathfinding**: Uses A* algorithm for optimal routing +6. **Blueprint Conversion**: Converts the layout to Factorio blueprint string format + +**Key Classes**: +- `FactoryNode`: Represents machines and their ports (inputs/outputs) +- Various layout functions for positioning and optimization + +### 4. **Grid Management** (`layout.py`) +Manages the physical construction site where blueprints are placed. + +**Key Classes**: +- `ConstructionSite`: 2D grid representation of the blueprint area + - Tracks reserved cells to prevent overlaps + - Converts layout to Factorio blueprint string format + +**Features**: +- Sparse array implementation (only reserved cells are stored) +- Efficient for large blueprint dimensions +- Visual representation of grid state + +### 5. **Flow Analysis** (`flow.py`) +Computes material flow through factory networks using graph theory. + +**Key Classes**: +- `Node`: Represents a processing unit (machine, transport, etc.) + - Tracks input/output flows + - May contain recipes for item transformation +- `Graph`: Flow network representation + +**Algorithm**: +- Uses NetworkX for graph operations +- Computes maximum flow scenarios +- Identifies bottlenecks and flow requirements + +### 6. **Blueprint Analysis** (`analyze.py`) +Extracts and analyzes existing Factorio blueprints. + +**Capabilities**: +- Extracts flow graphs from blueprint strings +- Categorizes entity types (supported vs ignored) +- Future: Flow bottleneck detection +- Future: Automatic production expansion + +### 7. **Utility Modules** +- **`vector.py`**: 2D vector operations for positioning +- **`a_star_factorio.py`**: A* pathfinding implementation for routing +- **`force_layout_pandas.py`**: Force-directed graph layout algorithm +- **`node.py`**: Node abstraction for graph operations +- **`constants.py`**: Game constants and configuration + +## Data Flow + +### Blueprint Generation Workflow +``` +Client Request (POST /process) + ↓ +Server receives JSON input + ↓ +GenerateBlueprint() function + ├─ Load game configuration (via factoriocalc) + ├─ Calculate required machines and recipes + ├─ Create ConstructionSite (64×64 grid) + ├─ Generate FactoryNodes from machines + ├─ randomly_placed_machines() - initial placement + ├─ add_connections() - route belts/inserters + ├─ spring() - optimize spacing via physics + ├─ machines_to_int() - convert to integer coords + ├─ place_on_site() - validate placement + └─ site_as_blueprint_string() - generate output + ↓ +Return blueprint string to client +``` + +### Flow Analysis Workflow +``` +Client Request (POST /compute-flow) + ↓ +find_blueprint_flow() function + ├─ Parse blueprint export string + ├─ Extract entities and connections + ├─ Build flow graph (nodes and edges) + ├─ compute_max_flow() - analyze capacities + └─ Return inputs/outputs + ↓ +Client receives flow requirements +``` + +## Technology Stack + +| Layer | Technology | Purpose | +|-------|-----------|---------| +| API Framework | Connexion + Flask | REST API with OpenAPI spec | +| Game Logic | factoriocalc | Factorio game mechanics & recipes | +| Spatial Layout | Custom + Spring Physics | Blueprint grid management | +| Graph Processing | NetworkX | Flow computation & pathfinding | +| Pathfinding | A* Algorithm | Optimal routing between machines | +| Logging | Python logging | Debug and error tracking | +| Middleware | Starlette CORS | Cross-origin request handling | + +## Configuration & Logging + +- **Configuration**: Machine preferences and game settings are configured in `GenerateBlueprint()` +- **Logging**: All operations logged to `server.log` with DEBUG level +- **Error Handling**: Comprehensive try-catch blocks with informative error messages + +## Testing + +Test suite located in `server/test/`: +- `test/astar/` - Pathfinding tests +- `test/flow/` - Flow computation tests +- `test/layout/` - Grid layout tests +- `test/solver/` - Blueprint generation tests + +Run tests with: `python -m unittest` + +## External Dependencies + +- **factoriocalc**: Library providing Factorio game mechanics, recipes, and machine definitions +- **networkx**: Graph analysis and flow computation algorithms +- **pandas**: Data manipulation (used in force layout algorithm) diff --git a/server/a_star_factorio.py b/server/a_star_factorio.py index 3fffef7..61ac2b7 100644 --- a/server/a_star_factorio.py +++ b/server/a_star_factorio.py @@ -39,6 +39,8 @@ def __init__( isinstance(i, tuple) for i in end_positions ): raise TypeError("end_positions must be a list of tuples") + if start_node_illegal_neighbors is None: + start_node_illegal_neighbors = {} if not isinstance(start_node_illegal_neighbors, dict): raise TypeError("start_node_illegal_neighbors must be a dictionary") if not isinstance(end_node_illegal_neighbors, dict): diff --git a/server/fbg-api.yaml b/server/fbg-api.yaml index c5da4d3..ebad497 100644 --- a/server/fbg-api.yaml +++ b/server/fbg-api.yaml @@ -8,6 +8,21 @@ info: url: https://github.com/Buildasaurus/Factorio-Blueprint-Generator/blob/main/LICENSE version: 0.0.1 paths: + /: + get: + summary: Health check + description: Returns server status + operationId: server.get_status + responses: + '200': + description: Server is running + content: + application/json: + schema: + type: object + properties: + status: + type: string /process: post: summary: Process some input diff --git a/server/layout.py b/server/layout.py index 99d4c11..c6fd964 100644 --- a/server/layout.py +++ b/server/layout.py @@ -160,6 +160,7 @@ def get_entity_list(self): def factoriocalc_entity_size(machine_name): import factoriocalc + factoriocalc.setGameConfig("v2.0") machine_class = factoriocalc.mchByName.get(machine_name) if machine_class is None: return None diff --git a/server/server.py b/server/server.py index 09ca43f..29c2e87 100644 --- a/server/server.py +++ b/server/server.py @@ -10,6 +10,10 @@ import layout import solver +def get_status(): + '''Health check endpoint''' + return jsonify({'status': 'Server is running'}), 200 + # Set up logging logging.basicConfig(filename='server.log', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger() @@ -108,4 +112,4 @@ def find_blueprint_flow(): return jsonify(result) if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) + app.run(host='0.0.0.0', port=5011) diff --git a/server/solver.py b/server/solver.py index 74dfb70..10d9cca 100644 --- a/server/solver.py +++ b/server/solver.py @@ -188,8 +188,8 @@ class LocatedMachine(FactoryNode): "A data class to store a machine and its position" def __init__(self, machine: Machine, position=None): - # Items pr second - True to make it calculate actual value. - flow_by_item = machine.flows(True).byItem + # Items pr second - get the actual flow values for this single machine instance + flow_by_item = machine.flows().byItem super().__init__( position=position, @@ -437,7 +437,19 @@ def place_on_site(site: 'ConstructionSite', machines: List[LocatedMachine], path machine = lm.machine site.add_entity(machine.name, lm.position, 0, machine.recipe.name) else: - site.add_entity(lm.name, lm.position, 0) + # This is a Port - add request filters if it's a requester chest + request_filters = None + if lm.name == 'logistic-chest-requester' and len(lm.unused_output) > 0: + # For requester chests, set request filters for items it should request + request_filters = [] + for idx, (item_type, rate) in enumerate(lm.unused_output.items()): + item_name = item_type.name if hasattr(item_type, 'name') else str(item_type) + request_filters.append({ + 'index': idx + 1, + 'name': item_name, + 'count': max(1, int(rate * 10)) # Request enough to cover ~10 seconds + }) + site.add_entity(lm.name, lm.position, 0, request_filters=request_filters) for target in machines: for source in target.getConnections(): try: @@ -628,10 +640,13 @@ def add_entry_if_free(inserter_pos, step, entry_list, illegal_coordinate_diction # If no start or end squares exist, no route can be made. if len(fac_coordinates[i]) == 0: log.debug(f"Could not find any valid {'start' if i == 0 else 'end'} square") - return None + return [] fac_finder = A_star(site,fac_coordinates[0],fac_coordinates[1], illegal_coordinates_dicts[0], illegal_coordinates_dicts[1]) fac_path = fac_finder.find_path(True, path_visualizer) + if fac_path is None: + log.debug("A* pathfinding returned None") + return [] log.debug("nodecount: " + str(len(fac_path))) for node in fac_path: log.debug(node)