Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 217 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,24 @@ 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
.fbg\Scripts\activate.bat
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
```
Expand All @@ -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)
2 changes: 2 additions & 0 deletions server/a_star_factorio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
15 changes: 15 additions & 0 deletions server/fbg-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions server/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
23 changes: 19 additions & 4 deletions server/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down