├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE.txt ├── README.md ├── docs ├── .nojekyll ├── README.md ├── _sidebar.md ├── classes │ ├── Blueprint.md │ ├── Entity.md │ └── Tile.md ├── examples.md └── index.html ├── package-lock.json ├── package.json ├── src ├── book.ts ├── defaultentities.ts ├── electrical-connections.ts ├── entity.ts ├── index.ts ├── tile.ts └── util.ts ├── test ├── blueprint_generate_tests.js ├── blueprint_parse_tests.js ├── electric_connection_tests.js └── util.js ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | /nbproject 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "12" 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false 3 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lucas Simon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # factorio-blueprint 2 | 3 | [![npm version](https://badge.fury.io/js/factorio-blueprint.svg)](https://badge.fury.io/js/factorio-blueprint) 4 | [![Build Status](https://travis-ci.org/demipixel/factorio-blueprint.svg?branch=master)](https://travis-ci.org/demipixel/factorio-blueprint) 5 | 6 | A node.js library created to help you create, modify, and export Factorio blueprints and blueprint strings! 7 | 8 | This library supports simple tasks such as adding or removing entities to more complex tasks such as connecting 9 | entities via wires and modifying combinators. 10 | 11 | See docs [here](https://demipixel.github.io/factorio-blueprint). 12 | 13 | ## Getting Started 14 | 15 | ### Website Usage 16 | 17 | If you want to use this on a site, you can access the latest build in /dist 18 | 19 | ### Install via NPM 20 | 21 | ``` 22 | $ npm install factorio-blueprint 23 | ``` 24 | 25 | ### Basic Usage 26 | 27 | ```js 28 | const Blueprint = require('factorio-blueprint'); 29 | 30 | // Create a blueprint with nothing in it 31 | const myBlueprint = new Blueprint(); 32 | // Import a blueprint using a blueprint string 33 | const importedBlueprint = new Blueprint(blueprintString); 34 | 35 | // Modify the blueprint! 36 | myBlueprint.createEntity('transport-belt', { x: 0, y: 0 }, Blueprint.UP); 37 | importedBlueprint.entities[0].remove(); 38 | 39 | // Export the string to use in-game 40 | console.log(myBlueprint.encode()); 41 | ``` 42 | 43 | ## [Click here for full documentation!](https://demipixel.github.io/factorio-blueprint) 44 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demipixel/factorio-blueprint/4443b91f38440217a812b1a64c69791d068232a4/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # factorio-blueprint 2 | 3 | A node.js library created to help you create, modify, and export Factorio blueprints and blueprint strings! 4 | 5 | This library supports simple tasks such as adding or removing entities to more complex tasks such as connecting 6 | entities via wires and modifying combinators. 7 | 8 | ## Getting Started 9 | 10 | ### Website Usage 11 | 12 | If you want to use this on a site, you can access the latest build in /dist 13 | 14 | ### Install via NPM 15 | 16 | ``` 17 | $ npm install factorio-blueprint 18 | ``` 19 | 20 | ### Basic Usage 21 | 22 | ```js 23 | const Blueprint = require('factorio-blueprint'); 24 | 25 | // Create a blueprint with nothing in it 26 | const myBlueprint = new Blueprint(); 27 | // Import a blueprint using a blueprint string 28 | const importedBlueprint = new Blueprint(blueprintString); 29 | 30 | // Modify the blueprint! 31 | myBlueprint.createEntity('transport-belt', { x: 0, y: 0 }, Blueprint.UP); 32 | importedBlueprint.entities[0].remove(); 33 | 34 | // Export the string to use in-game 35 | console.log(myBlueprint.encode()); 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - Getting Started 2 | - [Introduction](/) 3 | - [Examples](/examples.md) 4 | - Classes 5 | - [Blueprint](/classes/Blueprint.md) 6 | - [Entity](/classes/Entity.md) 7 | - [Tile](/classes/Tile.md) 8 | -------------------------------------------------------------------------------- /docs/classes/Blueprint.md: -------------------------------------------------------------------------------- 1 | # Blueprint 2 | 3 | Blueprint is the class that is returned by `require('factorio-blueprint')`. It is the main class used for loading 4 | or creating new blueprints. 5 | 6 | ## Properties 7 | 8 | ### icons 9 | 10 | Array of names of entities. No more than four icons are allowed. If this array is empty when calling `.luaString()`, two icons will be automatically generated. 11 | 12 | ### entities 13 | 14 | Array of all entities. 15 | 16 | ### name 17 | 18 | Name of the blueprint, can be set. Be wary of setting this to anything other than letters or numbers. 19 | 20 | ### description 21 | 22 | Description of the blueprint, can be set. 23 | 24 | ### snapping 25 | 26 | Dictionary with information on grid snapping, if set. Not set by default. 27 | ``` 28 | { 29 | grid: {x: 32, y: 32}, //size of the grid, defaults to one chunk 30 | absolute: true //whether it is absolutely positioned on the world grid or relative to the first placement 31 | position: {x: 1, y: 1}, //world offset of the upper left corner of the blueprint, used only with absolute positioning 32 | } 33 | ``` 34 | 35 | ### static UP 36 | 37 | Returns the integer for the direction up, helpful in Blueprint.createEntity() 38 | 39 | ### static DOWN 40 | 41 | ### static RIGHT 42 | 43 | ### static LEFT 44 | 45 | ### static ROTATION_NONE, ROTATION_90_CW, ROTATION_180_CW, ROTATION_270_CW, ROTATION_270_CCW, ROTATION_180_CCW, ROTATION_90_CCW 46 | 47 | Rotation parameters for the `placeBlueprint` function. 48 | 49 | ## Methods 50 | 51 | ### Blueprint([data], [opt]) 52 | 53 | Create an empty blueprint. Optional `data` can be a blueprint string or a Factorio blueprint object. See `load()` for info on the `opt`. 54 | 55 | ### static Blueprint.getBook(str) 56 | 57 | Returns an array of blueprints that were in the book. 58 | 59 | ### load(data, [opt]) 60 | 61 | Loads blueprint from a blueprint string or a Factorio blueprint object `data`. Returns self. Options are (all default false): 62 | 63 | ```js 64 | opt = { 65 | allowOverlap: false, // Throws an error if two entities are colliding unless allowOverlap is true 66 | fixEntityData: false // Automatically adds unknown entities to entityData, useful for mods 67 | } 68 | ``` 69 | 70 | ### toString() 71 | 72 | Outputs blueprint in fancy format with all the entities. 73 | 74 | ### createEntity(name, position, direction=0, allowOverlap=false, noPlace=false, center=false) 75 | 76 | Creates an entity of type `name` (use underscores) at `position` (top-left corner, any object with x and y attributes) facing `direction`. 77 | - Use `allowOverlap` to ignore two entities overlapping (which Factorio does not like...) 78 | - Use `noPlace` if you want the entity to be created but not placed (mainly used in .load()) 79 | - Use `center` if you want `position` to refer to the center of the entity (again, mainly used by .load()) 80 | 81 | Returns the newly created [Entity](./classes/Entity.md). 82 | 83 | ### createEntityWithData(data, allowOverlap=false, noPlace=false, center=false) 84 | 85 | Creates an entity with loaded data (containing keys such as name, position, direction, recipe, filters, and other options). 86 | 87 | Typically used when generating entities from a blueprint string. Can be used to clone an entity using the data of `entityToClone.getData()` (though make sure you change the position or whatever else you want to before creating it). 88 | 89 | ### createTile(name, position) 90 | 91 | Creates a tile (such as concrete or stone bricks) of type `name` at `position`. 92 | 93 | Returns the newly created [Tile](./classes/Tile.md). 94 | 95 | ### placeBlueprint(otherBlueprint, position, rotations=0, allowOverlap=false) 96 | 97 | Places `otherBlueprint` at `position` (being the center of `otherBlueprint`) with `rotations`; Supply one of the `Blueprint.ROTATION_*` constants. `allowOverlap` works the same as in createEntity(). Clones both entities and tiles. 98 | 99 | Returns self. 100 | 101 | ### findEntity(position) 102 | 103 | Return entity that overlaps `position` (or `null` if there is no such entity). 104 | 105 | ### findTile(position) 106 | 107 | Return tile at `position` (or `null` if there is no such tile). 108 | 109 | ### removeEntity(entity) 110 | 111 | Removes `entity`, returns it (or false if it was not removed) 112 | 113 | ### removeTile(tile) 114 | 115 | Removes `tile`, returns it (or false if it was not removed) 116 | 117 | ### removeEntityAtPosition(position) 118 | 119 | Removes an entity that overlaps `position`, returns the entity (or false if it was not removed). 120 | 121 | This is helpful as `blueprint.findEntity(position).remove()` could throw an error if no entity is found at that position. 122 | 123 | ### removeTileAtPosition(position) 124 | 125 | Removes a tile at `position`, returns the tile (or false if it was not removed) 126 | 127 | ### setSnapping(size, absolute?, absolutePosition?) 128 | 129 | Sets the grid snapping for the blueprint. 130 | `size` Position. Size of the grid 131 | `absolute` Boolean (optional). Absolute positioning will align the blueprint with the world grid 132 | `absolutePosition` Position (optional). Offsets an absolutely positioned blueprint from the world grid 133 | 134 | Note: The value that's shown as "grid position" in the GUI is controlled by moving the center with `fixCenter()` 135 | 136 | ### fixCenter([point]) 137 | 138 | Centers all entities on the blueprint. I recommend calling this before `Blueprint.encode()`. An optional `point` may be provided to center about. 139 | 140 | Returns self. 141 | 142 | ### center() 143 | 144 | Get position that is the center of all entities 145 | 146 | ### topLeft() 147 | 148 | Get top-left-most entity's corner position 149 | 150 | ### topRight() 151 | 152 | Get top-right-most entity's corner position 153 | 154 | ### bottomLeft() 155 | 156 | Get bottom-left-most entity's corner position 157 | 158 | ### bottomRight() 159 | 160 | Get bottom-right-most entity's corner position 161 | 162 | ### generateIcon(num) 163 | 164 | Generate icons based off the entities, `num` is the number of icons from 1 to 4. This is called automatically if no icons are provided. Returns self. 165 | 166 | ### toObject({ autoConnectPoles = true }) 167 | 168 | Object containing all the data (just before being converted to JSON). This is the data used by Factorio to load the blueprint (after it has been decoded). `autoConnectPoles` will destroy all (if any) electrical pole connections and reconnect them all as if they were placed manually in Factorio. 169 | 170 | ### toJSON() 171 | 172 | Get the JSON data of the blueprint just before it's encoded. 173 | 174 | ### encode({version="latest", autoConnectPoles=true}) 175 | 176 | Get the encoded blueprint string wihh options. 177 | - `version` encoding method (only option is `0` or `latest` at the moment). 178 | - `autoConnectPoles` described above in `toObject()` 179 | 180 | ### static Blueprint.isBook(str) 181 | 182 | Returns a boolean on whether or not the string is a blueprint book (otherwise it's just a blueprint). 183 | 184 | ### static Blueprint.toBook(blueprints, activeIndex=0, {version="latest", autoConnectPoles=true}) 185 | 186 | Get an encoded string using an array of blueprint objects stored in `blueprints`. `activeIndex` is the currently selected blueprint. Options described above in `encode()` 187 | 188 | ### static Blueprint.setEntityData(data) 189 | 190 | `data` should be an object, with keys as entity names and values as objects with options. A basic example is: 191 | 192 | ```js 193 | { 194 | my_modded_assembly_machine: { 195 | type: 'item', // 'item', 'fluid', 'virtual', 'tile', or 'recipe' 196 | width: 2, 197 | height: 2, 198 | 199 | recipe: true, 200 | modules: 4, 201 | 202 | inventorySize: 48, // How many slots this container has (such as a chest) 203 | filterAmount: false, // Set to false for filter inserters which have filters but no "amounts" on the filters 204 | directionType: false // true for underground belts 205 | } 206 | } 207 | ``` 208 | 209 | If you do not provide data for modded objects, certain functions such as `entity.size` as `blueprint.findEntity()` will not work correctly. 210 | 211 | Use [defaultentities.js](https://github.com/demipixel/factorio-blueprint/blob/master/defaultentities.js) as a reference for vanilla items. 212 | 213 | ### static Blueprint.getEntityData() 214 | 215 | Get the current object containing all entity data (mapping (entity name) -> (entity data)). See above for format. 216 | -------------------------------------------------------------------------------- /docs/classes/Entity.md: -------------------------------------------------------------------------------- 1 | # Entity 2 | 3 | Entities are any object that can be stored in a blueprint at a specific location. 4 | Examples of entities include belts, power poles, and assembly machines. 5 | 6 | ## Properties 7 | 8 | Do not modify properties directly! Functions to set things such as direction or position 9 | are included because they do more than just set the variable. 10 | 11 | ### bp 12 | 13 | Get the entity's blueprint parent. 14 | 15 | ### name 16 | 17 | The name of the entity using underscores (such as `"transport_belt"`). 18 | Only change to entities with the same size (e.g. You could swap a medium electric pole for 19 | a transport belt, but not for an electric furnace) 20 | 21 | ### position 22 | 23 | A [VictorJS](http://victorjs.org/) object of the top left corner of the entity. 24 | 25 | ### size 26 | 27 | [VictorJS](http://victorjs.org/) object for the width/height of the entity (using `size.x` and `size.y`) 28 | 29 | ### direction 30 | 31 | Number from 0 to 7. For most things it's 0, 2, 4, or 6. You can compare using the static properties 32 | for Blueprint such as `Blueprint.UP`. 33 | 34 | ### connections 35 | 36 | List of wire connections with other entities. 37 | 38 | ### neighbours 39 | 40 | List of entities that this electrical pole is connected to. 41 | 42 | ### condition 43 | 44 | Condition (if the entity is a combinator). 45 | 46 | ### constantEnabled 47 | 48 | Boolean which specifies whether or not a constant combinator is on or off. 49 | 50 | ### filters 51 | 52 | Object of filters. Keys are the positions (0 to X), values are item names. Used for storage containers. 53 | 54 | ### requestFilters 55 | 56 | Same format as `filters` but instead used for a chest's request filters. 57 | 58 | ### constants 59 | 60 | Same format as `filters` but instead use for constant combinator values. 61 | 62 | ### recipe 63 | 64 | Entity name of the recipe (again, using underscores). 65 | 66 | ### directionType 67 | 68 | "input" or "output" to distinguish between ins and outs of underground belts 69 | 70 | ### bar 71 | 72 | How many slots have not been blocked off (0 for all slots blocked off, 1 for all but one, -1 for none). Can be up to entity.INVENTORY_SIZE 73 | 74 | ### modules 75 | 76 | Dictionary of (module name) -> (# of that moudle). You *can* edit this property! Example: 77 | 78 | ``` 79 | { 80 | speed_module_3: 2 81 | } 82 | ``` 83 | 84 | ### INVENTORY_SIZE 85 | 86 | Number of slots this entity has 87 | 88 | ## Methods 89 | 90 | ### toString() 91 | 92 | Fancy display for data about the entity. 93 | 94 | ### remove() 95 | 96 | Removes self from blueprint. 97 | 98 | ### topLeft(), topRight(), bottomLeft(), bottomRight(), center() 99 | 100 | Gets a Victor position of respective relative location. `entity.topLeft()` is the same as `entity.position.clone()`. 101 | 102 | ### connect(toEntity, { fromSide, toSide, color }) 103 | 104 | Connect a wire (used for circuits) from one entity to another. 105 | 106 | `toEntity` The entity we are connecting the wire to. 107 | 108 | `fromSide` The side on the current entity that we should connect the wire to. This can be `"in"` or `"out"`. `"in"` is needed for most entities (other than decider/arithmetic combinators) and `undefined` will default to `"in"`. 109 | 110 | `toSide` The side on the `toEntity` that we should connect the wire to. 111 | 112 | `color` The color used, either `"red"` or `"green"` (default is `"red"`). 113 | 114 | Returns self. 115 | 116 | ### removeConnection(toEntity, { fromSide, toSide, color }) 117 | 118 | Remove wire connection (if it exists). Returns self. 119 | 120 | ### removeConnectionsWithEntity(toEntity, color) 121 | 122 | Removes all wire connections with `toEntity`. `color` is optional (defaults to all color wires) or either `"red"` or `"green"`. Returns self. 123 | 124 | ### removeAllConnections() 125 | 126 | Remove all wire connections with this entity. Returns self. 127 | 128 | ### setFilter(position, item, [amount]) 129 | 130 | Sets filter at `position` (this is 0-indexed) with `amount` of `item` (an entity name). 131 | 132 | Returns self. 133 | 134 | ### removeAllFilters() 135 | 136 | Removes all filters on the given entity. Returns self. 137 | 138 | ### setRequestFilter(position, item, amount) 139 | 140 | Sets logistics request filter at `position` (this is 1-indexed) with `amount` of `item` (an entity name). Returns self. 141 | 142 | ### removeAllRequestFilters() 143 | 144 | Remove all request filters on this entity. Returns self. 145 | 146 | ### setCondition(opt) 147 | 148 | Sets the condition on an arithmetic or decider combinator. 149 | 150 | ```js 151 | opt = { 152 | left: 'transport_belt', // String (item/entity name) 153 | right: 4, // Number (constant) or String (item/entity name) 154 | operator: '>', // If arithmetic, +-*/, if decider, <>= 155 | countFromInput: true, // For decider combinator, should output count from input (or be one). Default is true 156 | out: 'medium_electric_pole' // String (item/entity name) 157 | } 158 | ``` 159 | 160 | ### setDirection(dir) 161 | 162 | See `entity.direction`, recommend using static directions on Blueprint (such as `Blueprint.UP`). Returns self 163 | 164 | ### setDirectionType(type) 165 | 166 | Either `input` or `output`, only used for underground belts. Returns self. 167 | 168 | ### setRecipe(recipe) 169 | 170 | Sets the recipe for this entity. 171 | 172 | ### setBar(count) 173 | 174 | Sets the bar for this entity (see `entity.bar`). Returns self. 175 | 176 | ### setConstant(position, name, count) 177 | 178 | Set a constant combinator's value (`count`) for a signal (`name`) at `position` (0-indexed). Returns self 179 | 180 | ### setParameters(opt) 181 | 182 | Parameters for programmable speakers. 183 | 184 | ```js 185 | opt = { 186 | volume: 0, // 0.0 to 1.0 volume of a programmble speaker 187 | playGlobally: true, // Whether a programmable speaker should play globally 188 | allowPolyphony: true // Whether a programmable speaker should be able to play multiple overlapping sounds 189 | } 190 | ``` 191 | 192 | Returns self. 193 | 194 | ### setCircuityParameters(opt) 195 | 196 | More programmable speaker options, (in a different function because of how blueprints are constructed, to be changed soon). 197 | 198 | ```js 199 | opt = { 200 | signalIsPitch: false, // Whether a signal should be used as the pitch in a programmable speaker 201 | instrument: 0, // The ID of the instrument (0-indexed) 202 | note: 5, // The ID of the note to be played (0-indexed), different for different instruments 203 | } 204 | ``` 205 | 206 | Returns self. 207 | 208 | ### setAlertParameters(opt) 209 | 210 | Sets options for alerts on programmable speakers. 211 | 212 | ```js 213 | opt = { 214 | showAlert: true, // Whether or not to show an alert in the GUI when a sound is played 215 | showOnMap: false, // Whether or not the location should appear on the map 216 | message: 'I was created automagically!', // String containing the alert message 217 | icon: 'transport_belt' // The name of the icon to be displayed with the message 218 | } 219 | ``` 220 | 221 | ### getData() 222 | 223 | Return object with factorio-style data about entity. Entity names will contain dashes, not underscores. 224 | This is what is used decoded by Factorio when parsing a blueprint. For the whole blueprint, use `blueprint.toObject()`. 225 | -------------------------------------------------------------------------------- /docs/classes/Tile.md: -------------------------------------------------------------------------------- 1 | # Tile 2 | 3 | ## Properties 4 | 5 | ### bp 6 | 7 | Get the tile's blueprint parent 8 | 9 | ### name 10 | 11 | One of the following (or a modded tile name): 12 | 13 | `"concrete", "hazard_concrete_left", "hazard_concrete_right", "stone_path"`. 14 | 15 | ### position 16 | 17 | A [VictorJS](http://victorjs.org/) object of the top left corner of the tile. 18 | 19 | ### remove() 20 | 21 | Removes self from blueprint. 22 | 23 | ### getData() 24 | 25 | Return object with factorio-style data about entity. Entity names will contain dashes, not underscores 26 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## factorio-generators and Autotorio 4 | 5 | A fantastic example is [factorio-generators](https://github.com/demipixel/factorio-generators), a tool which is used to create ore outposts, oil outposts, and modify blueprints (such as replacing entities or flipping the blueprint entirely). [Autotorio](http://autotorio.com/outpost) uses factorio-generators to provide these tools to users. 6 | 7 | ## Basic Train Station 8 | 9 | ```js 10 | const Blueprint = require('factorio-blueprint'); 11 | 12 | const bp = new Blueprint(); 13 | 14 | // Rails are size 2, therefore we only need them every other tile 15 | for (let i = 0; i < 20; i += 2) { 16 | bp.createEntity('straight_rail', { x: i, y: 1 }, Blueprint.RIGHT); 17 | } 18 | // Train stop faces in the direction that the train should be facing when it arrives 19 | bp.createEntity('train_stop', { x: 0, y: 3}, Blueprint.RIGHT); 20 | 21 | // Center the blueprint around the entities instead of position (0,0) 22 | bp.fixCenter(); 23 | 24 | // Output the blueprint string! 25 | console.log(bp.encode()); 26 | ``` 27 | ## Belt Upgrader 28 | 29 | ```js 30 | const Blueprint = require('factorio-blueprint'); 31 | 32 | const bp = new Blueprint(str); 33 | 34 | bp.entities.forEach(entity => { 35 | if (entity.name.includes('transport_belt')) entity.name = 'express_transport_belt'; 36 | else if (entity.name.includes('splitter')) entity.name = 'express_splitter'; 37 | else if (entity.name.includes('underground_belt')) entity.name = 'express_underground_belt'; 38 | }); 39 | 40 | console.log(bp.encode()); 41 | ``` 42 | 43 | ## Speedy Miners 44 | 45 | ```js 46 | const Blueprint = require('factorio-blueprint'); 47 | 48 | const bp = new Blueprint(str); 49 | 50 | bp.entities.forEach(entity => { 51 | if (entity.name == 'electric_mining_drill') entity.modules['speed_module_3'] = 3; 52 | }); 53 | 54 | console.log(bp.encode()); 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | factorio-blueprint - Factorio Blueprints API 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Generating Blueprints...
13 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "factorio-blueprint", 3 | "version": "2.6.5", 4 | "description": "Factorio Blueprints API", 5 | "keywords": [ 6 | "factorio", 7 | "blueprint", 8 | "api", 9 | "generator", 10 | "library", 11 | "autotorio", 12 | "mod", 13 | "encode", 14 | "decode" 15 | ], 16 | "main": "dist/factorio-blueprint.min.js", 17 | "types": "dist/src/index.d.ts", 18 | "scripts": { 19 | "test": "mocha", 20 | "build": "webpack --mode production", 21 | "prepublishOnly": "npm run build && npm run test" 22 | }, 23 | "directories": { 24 | "test": "test" 25 | }, 26 | "author": "DemiPixel ", 27 | "contributors": [ 28 | { 29 | "name": "Ryan Davis", 30 | "email": "ryepup@gmail.com" 31 | } 32 | ], 33 | "license": "ISC", 34 | "devDependencies": { 35 | "@types/assert": "^1.5.6", 36 | "@types/prettyjson": "0.0.30", 37 | "@types/victor": "^1.1.0", 38 | "babel-cli": "^6.26.0", 39 | "babel-core": "^6.26.3", 40 | "babel-loader": "^9.1.2", 41 | "babel-preset-env": "^1.7.0", 42 | "chai": "^4.3.7", 43 | "core-js": "^3.29.1", 44 | "mocha": "^10.2.0", 45 | "node-polyfill-webpack-plugin": "^2.0.1", 46 | "prettyjson": "^1.2.5", 47 | "ts-loader": "^9.4.2", 48 | "typescript": "^5.0.2", 49 | "victor": "^1.1.0", 50 | "webpack": "^5.76.2", 51 | "webpack-cli": "^5.0.1" 52 | }, 53 | "repository": { 54 | "type": "git", 55 | "url": "git://github.com/demipixel/factorio-blueprint.git" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/book.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by anth on 21.05.2017. 3 | */ 4 | 5 | import Blueprint, { BlueprintOptions } from './index'; 6 | import util from './util'; 7 | 8 | export default (str: string, opt?: BlueprintOptions) => { 9 | const version = str.slice(0, 1); 10 | if (version !== '0') { 11 | throw new Error( 12 | 'Cannot find decoder for blueprint book version ' + version, 13 | ); 14 | } 15 | let obj = util.decode[version](str); 16 | 17 | const blueprints = obj.blueprint_book.blueprints; 18 | const blueprintList: Blueprint[] = []; 19 | 20 | blueprints.forEach((data: any) => { 21 | const blueprintData = data.blueprint; 22 | 23 | if (data.index === undefined) { 24 | blueprintList.push(new Blueprint(blueprintData, opt)); 25 | } else { 26 | blueprintList[data.index] = new Blueprint(blueprintData, opt); 27 | } 28 | }); 29 | 30 | return blueprintList; 31 | }; 32 | -------------------------------------------------------------------------------- /src/defaultentities.ts: -------------------------------------------------------------------------------- 1 | enum Type { 2 | Fluid = 'fluid', 3 | Item = 'item', 4 | Virtual = 'virtual', 5 | Tile = 'tile', 6 | Recipe = 'recipe', 7 | } 8 | 9 | export interface EntityDescription { 10 | type?: string; 11 | width?: number; 12 | height?: number; 13 | 14 | parameters?: boolean; 15 | alertParameters?: boolean; 16 | 17 | inventorySize?: number; 18 | directionType?: boolean; 19 | filterAmount?: boolean; 20 | combinator?: boolean; 21 | modules?: number; 22 | recipe?: boolean; 23 | maxElectricReach?: number; 24 | } 25 | 26 | const DEFAULT_ENTITIES: { [entity_name: string]: EntityDescription } = { 27 | // ADD MORE (vanilla) AS YOU PLEASE (or modded if it's just for you)! 28 | // Somebody will probably automate the gathering of this data soon... 29 | 30 | programmable_speaker: { 31 | type: Type.Item, 32 | width: 1, 33 | height: 1, 34 | 35 | parameters: true, 36 | alertParameters: true, 37 | }, 38 | 39 | heat_exchanger: { 40 | type: Type.Item, 41 | width: 3, 42 | height: 2, 43 | }, 44 | 45 | heat_pipe: { 46 | type: Type.Item, 47 | width: 1, 48 | height: 1, 49 | }, 50 | 51 | nuclear_reactor: { 52 | type: Type.Item, 53 | width: 5, 54 | height: 5, 55 | }, 56 | 57 | centrifuge: { 58 | type: Type.Item, 59 | width: 3, 60 | height: 3, 61 | }, 62 | 63 | steam_turbine: { 64 | type: Type.Item, 65 | width: 3, 66 | height: 5, 67 | }, 68 | 69 | tank: { 70 | type: Type.Item, 71 | }, 72 | car: { 73 | type: Type.Item, 74 | }, 75 | cargo_wagon: { 76 | type: Type.Item, 77 | inventorySize: 40, 78 | }, 79 | fluid_wagon: { 80 | type: Type.Item, 81 | }, 82 | locomotive: { 83 | type: Type.Item, 84 | }, 85 | 86 | light_armor: { 87 | type: Type.Item, 88 | }, 89 | heavy_armor: { 90 | type: Type.Item, 91 | }, 92 | modular_armor: { 93 | type: Type.Item, 94 | }, 95 | grenade: { 96 | type: Type.Item, 97 | }, 98 | cluster_grenade: { 99 | type: Type.Item, 100 | }, 101 | flamethrower: { 102 | type: Type.Item, 103 | }, 104 | flamethrower_ammo: { 105 | type: Type.Item, 106 | }, 107 | rocket_launcher: { 108 | type: Type.Item, 109 | }, 110 | rocket: { 111 | type: Type.Item, 112 | }, 113 | explosive_rocket: { 114 | type: Type.Item, 115 | }, 116 | atomic_bomb: { 117 | type: Type.Item, 118 | }, 119 | combat_shotgun: { 120 | type: Type.Item, 121 | }, 122 | shotgun: { 123 | type: Type.Item, 124 | }, 125 | shotgun_shell: { 126 | type: Type.Item, 127 | }, 128 | piercing_shotgun_shell: { 129 | type: Type.Item, 130 | }, 131 | submachine_gun: { 132 | type: Type.Item, 133 | }, 134 | pistol: { 135 | type: Type.Item, 136 | }, 137 | firearm_magazine: { 138 | type: Type.Item, 139 | }, 140 | piercing_rounds_magazine: { 141 | type: Type.Item, 142 | }, 143 | uranium_rounds_magazine: { 144 | type: Type.Item, 145 | }, 146 | cannon_shell: { 147 | type: Type.Item, 148 | }, 149 | explosive_cannon_shell: { 150 | type: Type.Item, 151 | }, 152 | uranium_cannon_shell: { 153 | type: Type.Item, 154 | }, 155 | explosive_uranium_cannon_shell: { 156 | type: Type.Item, 157 | }, 158 | 159 | power_armor: { 160 | type: Type.Item, 161 | }, 162 | power_armor_mk2: { 163 | type: Type.Item, 164 | }, 165 | energy_shield_equipment: { 166 | type: Type.Item, 167 | }, 168 | energy_shield_mk2_equipment: { 169 | type: Type.Item, 170 | }, 171 | solar_panel_equipment: { 172 | type: Type.Item, 173 | }, 174 | fusion_reactor_equipment: { 175 | type: Type.Item, 176 | }, 177 | battery_equipment: { 178 | type: Type.Item, 179 | }, 180 | battery_mk2_equipment: { 181 | type: Type.Item, 182 | }, 183 | personal_laser_defense_equipment: { 184 | type: Type.Item, 185 | }, 186 | discharge_defense_equipment: { 187 | type: Type.Item, 188 | }, 189 | exoskeleton_equipment: { 190 | type: Type.Item, 191 | }, 192 | personal_roboport_equipment: { 193 | type: Type.Item, 194 | }, 195 | personal_roboport_mk2_equipment: { 196 | type: Type.Item, 197 | }, 198 | night_vision_equipment: { 199 | type: Type.Item, 200 | }, 201 | 202 | discharge_defense_remote: { 203 | type: Type.Item, 204 | }, 205 | destroyer_capsule: { 206 | type: Type.Item, 207 | }, 208 | distractor_capsule: { 209 | type: Type.Item, 210 | }, 211 | defender_capsule: { 212 | type: Type.Item, 213 | }, 214 | slowdown_capsule: { 215 | type: Type.Item, 216 | }, 217 | poison_capsule: { 218 | type: Type.Item, 219 | }, 220 | 221 | stone: { 222 | type: Type.Item, 223 | }, 224 | 225 | solid_fuel: { 226 | type: Type.Item, 227 | }, 228 | 229 | stone_brick: { 230 | type: Type.Item, 231 | }, 232 | 233 | stone_path: { 234 | type: Type.Tile, 235 | }, 236 | landfill: { 237 | type: Type.Tile, 238 | }, 239 | concrete: { 240 | type: Type.Tile, 241 | }, 242 | hazard_concrete: { 243 | type: Type.Item, 244 | }, 245 | hazard_concrete_left: { 246 | type: Type.Tile, 247 | }, 248 | hazard_concrete_right: { 249 | type: Type.Tile, 250 | }, 251 | refined_concrete: { 252 | type: Type.Tile, 253 | }, 254 | refined_hazard_concrete: { 255 | type: Type.Item, 256 | }, 257 | refined_hazard_concrete_left: { 258 | type: Type.Tile, 259 | }, 260 | refined_hazard_concrete_right: { 261 | type: Type.Tile, 262 | }, 263 | 264 | iron_axe: { 265 | type: Type.Item, 266 | }, 267 | steel_axe: { 268 | type: Type.Item, 269 | }, 270 | repair_pack: { 271 | type: Type.Item, 272 | }, 273 | blueprint: { 274 | type: Type.Item, 275 | }, 276 | deconstruction_planner: { 277 | type: Type.Item, 278 | }, 279 | blueprint_book: { 280 | type: Type.Item, 281 | }, 282 | 283 | copper_cable: { 284 | type: Type.Item, 285 | }, 286 | red_wire: { 287 | type: Type.Item, 288 | }, 289 | green_wire: { 290 | type: Type.Item, 291 | }, 292 | 293 | beacon: { 294 | type: Type.Item, 295 | width: 3, 296 | height: 3, 297 | 298 | modules: 2, 299 | }, 300 | small_electric_pole: { 301 | type: Type.Item, 302 | width: 1, 303 | height: 1, 304 | maxElectricReach: 7.5, 305 | }, 306 | medium_electric_pole: { 307 | type: Type.Item, 308 | width: 1, 309 | height: 1, 310 | maxElectricReach: 9, 311 | }, 312 | substation: { 313 | type: Type.Item, 314 | width: 2, 315 | height: 2, 316 | maxElectricReach: 18, 317 | }, 318 | big_electric_pole: { 319 | type: Type.Item, 320 | width: 2, 321 | height: 2, 322 | maxElectricReach: 30, 323 | }, 324 | offshore_pump: { 325 | type: Type.Item, 326 | width: 2, 327 | height: 2, 328 | }, 329 | small_lamp: { 330 | type: Type.Item, 331 | width: 1, 332 | height: 1, 333 | }, 334 | solar_panel: { 335 | type: Type.Item, 336 | width: 3, 337 | height: 3, 338 | }, 339 | arithmetic_combinator: { 340 | type: Type.Item, 341 | width: 1, 342 | height: 2, 343 | }, 344 | decider_combinator: { 345 | type: Type.Item, 346 | width: 1, 347 | height: 2, 348 | }, 349 | constant_combinator: { 350 | type: Type.Item, 351 | width: 1, 352 | height: 1, 353 | }, 354 | 355 | splitter: { 356 | // Default position is facing north, 2 wide and 1 high for all splitters. 357 | type: Type.Item, 358 | width: 2, 359 | height: 1, 360 | }, 361 | fast_splitter: { 362 | type: Type.Item, 363 | width: 2, 364 | height: 1, 365 | }, 366 | express_splitter: { 367 | type: Type.Item, 368 | width: 2, 369 | height: 1, 370 | }, 371 | transport_belt: { 372 | type: Type.Item, 373 | width: 1, 374 | height: 1, 375 | }, 376 | fast_transport_belt: { 377 | type: Type.Item, 378 | width: 1, 379 | height: 1, 380 | }, 381 | express_transport_belt: { 382 | type: Type.Item, 383 | width: 1, 384 | height: 1, 385 | }, 386 | underground_belt: { 387 | type: Type.Item, 388 | width: 1, 389 | height: 1, 390 | directionType: true, 391 | }, 392 | fast_underground_belt: { 393 | type: Type.Item, 394 | width: 1, 395 | height: 1, 396 | directionType: true, 397 | }, 398 | express_underground_belt: { 399 | type: Type.Item, 400 | width: 1, 401 | height: 1, 402 | directionType: true, 403 | }, 404 | assembling_machine_1: { 405 | type: Type.Item, 406 | width: 3, 407 | height: 3, 408 | recipe: true, 409 | }, 410 | assembling_machine_2: { 411 | type: Type.Item, 412 | width: 3, 413 | height: 3, 414 | 415 | recipe: true, 416 | modules: 2, 417 | }, 418 | assembling_machine_3: { 419 | type: Type.Item, 420 | width: 3, 421 | height: 3, 422 | 423 | recipe: true, 424 | modules: 4, 425 | }, 426 | wooden_chest: { 427 | type: Type.Item, 428 | width: 1, 429 | height: 1, 430 | 431 | inventorySize: 16, 432 | }, 433 | iron_chest: { 434 | type: Type.Item, 435 | width: 1, 436 | height: 1, 437 | 438 | inventorySize: 32, 439 | }, 440 | steel_chest: { 441 | type: Type.Item, 442 | width: 1, 443 | height: 1, 444 | inventorySize: 48, 445 | }, 446 | logistic_chest_passive_provider: { 447 | type: Type.Item, 448 | width: 1, 449 | height: 1, 450 | inventorySize: 48, 451 | }, 452 | logistic_chest_active_provider: { 453 | type: Type.Item, 454 | width: 1, 455 | height: 1, 456 | inventorySize: 48, 457 | }, 458 | logistic_chest_storage: { 459 | type: Type.Item, 460 | width: 1, 461 | height: 1, 462 | inventorySize: 48, 463 | }, 464 | logistic_chest_requester: { 465 | type: Type.Item, 466 | width: 1, 467 | height: 1, 468 | inventorySize: 48, 469 | }, 470 | logistic_chest_buffer: { 471 | type: Type.Item, 472 | width: 1, 473 | height: 1, 474 | inventorySize: 48, 475 | }, 476 | storage_tank: { 477 | type: Type.Item, 478 | width: 3, 479 | height: 3, 480 | }, 481 | burner_inserter: { 482 | type: Type.Item, 483 | width: 1, 484 | height: 1, 485 | }, 486 | inserter: { 487 | type: Type.Item, 488 | width: 1, 489 | height: 1, 490 | }, 491 | long_handed_inserter: { 492 | type: Type.Item, 493 | width: 1, 494 | height: 1, 495 | }, 496 | fast_inserter: { 497 | type: Type.Item, 498 | width: 1, 499 | height: 1, 500 | }, 501 | filter_inserter: { 502 | type: Type.Item, 503 | width: 1, 504 | height: 1, 505 | filterAmount: false, 506 | }, 507 | stack_inserter: { 508 | type: Type.Item, 509 | width: 1, 510 | height: 1, 511 | }, 512 | stack_filter_inserter: { 513 | type: Type.Item, 514 | width: 1, 515 | height: 1, 516 | filterAmount: false, 517 | }, 518 | gate: { 519 | type: Type.Item, 520 | width: 1, 521 | height: 1, 522 | }, 523 | stone_wall: { 524 | type: Type.Item, 525 | width: 1, 526 | height: 1, 527 | }, 528 | radar: { 529 | type: Type.Item, 530 | width: 3, 531 | height: 3, 532 | }, 533 | rail: { 534 | type: Type.Item, 535 | }, 536 | straight_rail: { 537 | type: Type.Item, 538 | width: 2, 539 | height: 2, 540 | }, 541 | curved_rail: { 542 | type: Type.Item, 543 | width: 1, 544 | height: 1, 545 | }, 546 | // Lets figure out curved rails later. (1 curved rail deconstructs to 4 straight rails) 547 | land_mine: { 548 | type: Type.Item, 549 | width: 1, 550 | height: 1, 551 | }, 552 | train_stop: { 553 | // pretty sure this is a 1.2x1.2 centered in a 2x2 square. 554 | type: Type.Item, 555 | width: 2, 556 | height: 2, 557 | }, 558 | rail_signal: { 559 | type: Type.Item, 560 | width: 1, 561 | height: 1, 562 | }, 563 | rail_chain_signal: { 564 | type: Type.Item, 565 | width: 1, 566 | height: 1, 567 | }, 568 | lab: { 569 | type: Type.Item, 570 | width: 3, 571 | height: 3, 572 | 573 | modules: 2, 574 | }, 575 | rocket_silo: { 576 | type: Type.Item, 577 | width: 9, 578 | height: 10, //unsure about these values, got them from code only (never counted it in game, but 10 sounds right.) 579 | 580 | modules: 4, 581 | }, 582 | chemical_plant: { 583 | type: Type.Item, 584 | width: 3, 585 | height: 3, 586 | 587 | modules: 3, 588 | }, 589 | oil_refinery: { 590 | type: Type.Item, 591 | width: 5, 592 | height: 5, 593 | 594 | modules: 3, 595 | }, 596 | stone_furnace: { 597 | type: Type.Item, 598 | width: 2, 599 | height: 2, 600 | }, 601 | steel_furnace: { 602 | type: Type.Item, 603 | width: 2, 604 | height: 2, 605 | }, 606 | electric_furnace: { 607 | type: Type.Item, 608 | width: 3, 609 | height: 3, 610 | 611 | modules: 2, 612 | }, 613 | pumpjack: { 614 | type: Type.Item, 615 | width: 3, 616 | height: 3, 617 | }, 618 | burner_mining_drill: { 619 | type: Type.Item, 620 | width: 2, 621 | height: 2, 622 | }, 623 | 624 | electric_mining_drill: { 625 | type: Type.Item, 626 | width: 3, 627 | height: 3, 628 | 629 | modules: 3, 630 | }, 631 | pump: { 632 | type: Type.Item, 633 | width: 1, 634 | height: 2, 635 | }, 636 | pipe: { 637 | type: Type.Item, 638 | width: 1, 639 | height: 1, 640 | }, 641 | pipe_to_ground: { 642 | type: Type.Item, 643 | width: 1, 644 | height: 1, 645 | }, 646 | 647 | electronic_circuit: { 648 | type: Type.Item, 649 | }, 650 | advanced_circuit: { 651 | type: Type.Item, 652 | }, 653 | 654 | boiler: { 655 | type: Type.Item, 656 | width: 3, 657 | height: 2, 658 | }, 659 | steam_engine: { 660 | type: Type.Item, 661 | width: 5, 662 | height: 3, 663 | }, 664 | accumulator: { 665 | type: Type.Item, 666 | width: 2, 667 | height: 2, 668 | }, 669 | 670 | roboport: { 671 | type: Type.Item, 672 | width: 4, 673 | height: 4, 674 | }, 675 | construction_robot: { 676 | type: Type.Item, 677 | }, 678 | logistic_robot: { 679 | type: Type.Item, 680 | }, 681 | power_switch: { 682 | type: Type.Item, 683 | width: 3, 684 | height: 3, 685 | }, 686 | 687 | gun_turret: { 688 | type: Type.Item, 689 | width: 2, 690 | height: 2, 691 | }, 692 | artillery_turret: { 693 | type: Type.Item, 694 | width: 3, 695 | height: 3, 696 | }, 697 | laser_turret: { 698 | type: Type.Item, 699 | width: 2, 700 | height: 2, 701 | }, 702 | flamethrower_turret: { 703 | type: Type.Item, 704 | width: 2, 705 | height: 3, 706 | }, 707 | 708 | productivity_module: { 709 | type: Type.Item, 710 | }, 711 | productivity_module_2: { 712 | type: Type.Item, 713 | }, 714 | productivity_module_3: { 715 | type: Type.Item, 716 | }, 717 | effectivity_module: { 718 | type: Type.Item, 719 | }, 720 | effectivity_module_2: { 721 | type: Type.Item, 722 | }, 723 | effectivity_module_3: { 724 | type: Type.Item, 725 | }, 726 | speed_module: { 727 | type: Type.Item, 728 | }, 729 | speed_module_2: { 730 | type: Type.Item, 731 | }, 732 | speed_module_3: { 733 | type: Type.Item, 734 | }, 735 | 736 | water: { 737 | type: Type.Fluid, 738 | }, 739 | crude_oil: { 740 | type: Type.Fluid, 741 | }, 742 | petroleum_gas: { 743 | type: Type.Fluid, 744 | }, 745 | heavy_oil: { 746 | type: Type.Fluid, 747 | }, 748 | light_oil: { 749 | type: Type.Fluid, 750 | }, 751 | sulfuric_acid: { 752 | type: Type.Fluid, 753 | }, 754 | lubricant: { 755 | type: Type.Fluid, 756 | }, 757 | steam: { 758 | type: Type.Fluid, 759 | }, 760 | 761 | basic_oil_processing: { 762 | type: Type.Recipe, 763 | }, 764 | advanced_oil_processing: { 765 | type: Type.Recipe, 766 | }, 767 | heavy_oil_cracking: { 768 | type: Type.Recipe, 769 | }, 770 | light_oil_cracking: { 771 | type: Type.Recipe, 772 | }, 773 | coal_liquefaction: { 774 | type: Type.Recipe, 775 | }, 776 | 777 | raw_fish: { 778 | type: Type.Item, 779 | }, 780 | wood: { 781 | type: Type.Item, 782 | }, 783 | raw_wood: { 784 | type: Type.Item, 785 | }, 786 | iron_ore: { 787 | type: Type.Item, 788 | }, 789 | iron_plate: { 790 | type: Type.Item, 791 | }, 792 | copper_ore: { 793 | type: Type.Item, 794 | }, 795 | copper_plate: { 796 | type: Type.Item, 797 | }, 798 | steel_plate: { 799 | type: Type.Item, 800 | }, 801 | coal: { 802 | type: Type.Item, 803 | }, 804 | uranium_ore: { 805 | type: Type.Item, 806 | }, 807 | plastic_bar: { 808 | type: Type.Item, 809 | }, 810 | sulfur: { 811 | type: Type.Item, 812 | }, 813 | 814 | crude_oil_barrel: { 815 | type: Type.Item, 816 | }, 817 | heavy_oil_barrel: { 818 | type: Type.Item, 819 | }, 820 | light_oil_barrel: { 821 | type: Type.Item, 822 | }, 823 | lubricant_barrel: { 824 | type: Type.Item, 825 | }, 826 | petroleum_gas_barrel: { 827 | type: Type.Item, 828 | }, 829 | sulfuric_acid_barrel: { 830 | type: Type.Item, 831 | }, 832 | water_barrel: { 833 | type: Type.Item, 834 | }, 835 | empty_barrel: { 836 | type: Type.Item, 837 | }, 838 | 839 | processing_unit: { 840 | type: Type.Item, 841 | }, 842 | 843 | engine_unit: { 844 | type: Type.Item, 845 | }, 846 | 847 | electric_engine_unit: { 848 | type: Type.Item, 849 | }, 850 | 851 | battery: { 852 | type: Type.Item, 853 | }, 854 | 855 | explosives: { 856 | type: Type.Item, 857 | }, 858 | flying_robot_frame: { 859 | type: Type.Item, 860 | }, 861 | low_density_structure: { 862 | type: Type.Item, 863 | }, 864 | rocket_fuel: { 865 | type: Type.Item, 866 | }, 867 | rocket_control_unit: { 868 | type: Type.Item, 869 | }, 870 | satellite: { 871 | type: Type.Item, 872 | }, 873 | uranium_235: { 874 | type: Type.Item, 875 | }, 876 | uranium_238: { 877 | type: Type.Item, 878 | }, 879 | 880 | uranium_fuel_cell: { 881 | type: Type.Item, 882 | }, 883 | used_up_uranium_fuel_cell: { 884 | type: Type.Item, 885 | }, 886 | science_pack_1: { 887 | type: Type.Item, 888 | }, 889 | science_pack_2: { 890 | type: Type.Item, 891 | }, 892 | science_pack_3: { 893 | type: Type.Item, 894 | }, 895 | military_science_pack: { 896 | type: Type.Item, 897 | }, 898 | production_science_pack: { 899 | type: Type.Item, 900 | }, 901 | high_tech_science_pack: { 902 | type: Type.Item, 903 | }, 904 | space_science_pack: { 905 | type: Type.Item, 906 | }, 907 | 908 | iron_stick: { 909 | type: Type.Item, 910 | }, 911 | iron_gear_wheel: { 912 | type: Type.Item, 913 | }, 914 | 915 | signal_anything: { 916 | type: Type.Virtual, 917 | combinator: true, 918 | }, 919 | signal_each: { 920 | type: Type.Virtual, 921 | combinator: true, 922 | }, 923 | signal_everything: { 924 | type: Type.Virtual, 925 | combinator: true, 926 | }, 927 | signal_0: { 928 | type: Type.Virtual, 929 | }, 930 | signal_1: { 931 | type: Type.Virtual, 932 | }, 933 | signal_2: { 934 | type: Type.Virtual, 935 | }, 936 | signal_3: { 937 | type: Type.Virtual, 938 | }, 939 | signal_4: { 940 | type: Type.Virtual, 941 | }, 942 | signal_5: { 943 | type: Type.Virtual, 944 | }, 945 | signal_6: { 946 | type: Type.Virtual, 947 | }, 948 | signal_7: { 949 | type: Type.Virtual, 950 | }, 951 | signal_8: { 952 | type: Type.Virtual, 953 | }, 954 | signal_9: { 955 | type: Type.Virtual, 956 | }, 957 | signal_A: { 958 | type: Type.Virtual, 959 | }, 960 | signal_B: { 961 | type: Type.Virtual, 962 | }, 963 | signal_C: { 964 | type: Type.Virtual, 965 | }, 966 | signal_D: { 967 | type: Type.Virtual, 968 | }, 969 | signal_E: { 970 | type: Type.Virtual, 971 | }, 972 | signal_F: { 973 | type: Type.Virtual, 974 | }, 975 | signal_G: { 976 | type: Type.Virtual, 977 | }, 978 | signal_H: { 979 | type: Type.Virtual, 980 | }, 981 | signal_I: { 982 | type: Type.Virtual, 983 | }, 984 | signal_J: { 985 | type: Type.Virtual, 986 | }, 987 | signal_K: { 988 | type: Type.Virtual, 989 | }, 990 | signal_L: { 991 | type: Type.Virtual, 992 | }, 993 | signal_M: { 994 | type: Type.Virtual, 995 | }, 996 | signal_N: { 997 | type: Type.Virtual, 998 | }, 999 | signal_O: { 1000 | type: Type.Virtual, 1001 | }, 1002 | signal_P: { 1003 | type: Type.Virtual, 1004 | }, 1005 | signal_Q: { 1006 | type: Type.Virtual, 1007 | }, 1008 | signal_R: { 1009 | type: Type.Virtual, 1010 | }, 1011 | signal_S: { 1012 | type: Type.Virtual, 1013 | }, 1014 | signal_T: { 1015 | type: Type.Virtual, 1016 | }, 1017 | signal_U: { 1018 | type: Type.Virtual, 1019 | }, 1020 | signal_V: { 1021 | type: Type.Virtual, 1022 | }, 1023 | signal_W: { 1024 | type: Type.Virtual, 1025 | }, 1026 | signal_X: { 1027 | type: Type.Virtual, 1028 | }, 1029 | signal_Y: { 1030 | type: Type.Virtual, 1031 | }, 1032 | signal_Z: { 1033 | type: Type.Virtual, 1034 | }, 1035 | 1036 | signal_blue: { 1037 | type: Type.Virtual, 1038 | }, 1039 | signal_red: { 1040 | type: Type.Virtual, 1041 | }, 1042 | signal_green: { 1043 | type: Type.Virtual, 1044 | }, 1045 | signal_yellow: { 1046 | type: Type.Virtual, 1047 | }, 1048 | signal_cyan: { 1049 | type: Type.Virtual, 1050 | }, 1051 | signal_pink: { 1052 | type: Type.Virtual, 1053 | }, 1054 | signal_white: { 1055 | type: Type.Virtual, 1056 | }, 1057 | signal_grey: { 1058 | type: Type.Virtual, 1059 | }, 1060 | signal_black: { 1061 | type: Type.Virtual, 1062 | }, 1063 | }; 1064 | 1065 | export default DEFAULT_ENTITIES; 1066 | -------------------------------------------------------------------------------- /src/electrical-connections.ts: -------------------------------------------------------------------------------- 1 | import Blueprint from './'; 2 | import Entity from './entity'; 3 | 4 | export function generateElectricalConnections(bp: Blueprint) { 5 | const entityData = Blueprint.getEntityData(); 6 | 7 | const poles = bp.entities.filter( 8 | (ent) => 'maxElectricReach' in entityData[ent.name], 9 | ); 10 | 11 | for (const pole of poles) { 12 | pole.neighbours = []; 13 | } 14 | 15 | for (const pole of poles) { 16 | const sortedPotentialNeighbors = poles 17 | .filter( 18 | (otherPole) => 19 | otherPole.id < pole.id && 20 | pole.position.distance(otherPole.position) <= 21 | Math.min( 22 | entityData[pole.name].maxElectricReach || 0, 23 | entityData[otherPole.name].maxElectricReach || 0, 24 | ), 25 | ) 26 | .sort((a, b) => { 27 | const aSqDist = pole.position.distanceSq(a.position); 28 | const bSqDist = pole.position.distanceSq(b.position); 29 | 30 | if (aSqDist === bSqDist) { 31 | return a.position.y === b.position.y 32 | ? a.position.x < b.position.x 33 | ? -1 34 | : 1 35 | : a.position.y < b.position.y 36 | ? -1 37 | : 1; 38 | } else { 39 | return aSqDist < bSqDist ? -1 : 1; 40 | } 41 | }); 42 | 43 | const doNotConnectPoles: Entity[] = []; 44 | for (const neighbor of sortedPotentialNeighbors) { 45 | if (doNotConnectPoles.includes(neighbor)) { 46 | continue; 47 | } else if (pole.neighbours.includes(neighbor)) { 48 | doNotConnectPoles.push(...neighbor.neighbours); 49 | continue; 50 | } 51 | 52 | doNotConnectPoles.push(...neighbor.neighbours); 53 | pole.neighbours.push(neighbor); 54 | neighbor.neighbours.push(pole); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/entity.ts: -------------------------------------------------------------------------------- 1 | import Victor from 'victor'; 2 | 3 | import entityData from './defaultentities'; 4 | import Blueprint from './index'; 5 | 6 | type PositionGrid = { [location: string]: Entity }; 7 | type Side = 1 | 2 | 'in' | 'out'; 8 | type Color = 'red' | 'green'; 9 | type Priority = 'left' | 'right'; 10 | type DirectionType = 'input' | 'output'; 11 | 12 | interface Connection { 13 | entity: Entity; 14 | color: Color; 15 | side: Side; 16 | id?: string; 17 | } 18 | 19 | interface CombinatorData { 20 | left?: string; 21 | right?: string | number; 22 | operator?: string; 23 | out?: string; 24 | 25 | controlEnable?: boolean; 26 | readContents?: boolean; 27 | readMode?: string; 28 | countFromInput?: boolean; 29 | } 30 | 31 | interface Constant { 32 | name: string; 33 | count: number; 34 | } 35 | 36 | interface AlertParameters { 37 | showAlert?: boolean; 38 | showOnMap?: boolean; 39 | icon?: string | { type: string; name: string }; 40 | message?: string; 41 | } 42 | 43 | export default class Entity { 44 | id: number; 45 | bp: Blueprint; 46 | name: string; 47 | position: Victor; 48 | direction: number; 49 | 50 | rawConnections: any; 51 | connections: Connection[]; 52 | rawNeighbours?: number[]; 53 | neighbours: Entity[]; 54 | circuitParameters: any; 55 | condition: CombinatorData; 56 | constants?: { [position: number]: Constant }; 57 | constantEnabled: boolean; 58 | trainControlBehavior: Record; 59 | 60 | parameters: any; 61 | alertParameters: AlertParameters; 62 | 63 | filters: { [position: number]: string }; 64 | requestFilters: { [position: number]: Constant }; 65 | directionType: DirectionType; 66 | recipe?: string; 67 | bar: number; 68 | 69 | modules: any; 70 | 71 | stationName?: string; 72 | manualTrainsLimit?: number; 73 | 74 | splitterFilter?: string; 75 | inputPriority: Priority | undefined; 76 | outputPriority: Priority | undefined; 77 | 78 | size: Victor; 79 | HAS_DIRECTION_TYPE: boolean; 80 | CAN_HAVE_RECIPE: boolean; 81 | CAN_HAVE_MODULES: number; 82 | INVENTORY_SIZE: number; 83 | 84 | constructor(data: any, bp: Blueprint, center?: boolean) { 85 | if (!entityData[bp.checkName(data.name)]) 86 | entityData[bp.checkName(data.name)] = {}; 87 | let myData = entityData[bp.checkName(data.name)]; // entityData contains info like width, height, filterAmount, etc 88 | 89 | this.id = -1; // Id used when generating blueprint 90 | this.bp = bp; // Blueprint 91 | this.name = this.bp.checkName(data.name); // Name or "type" 92 | this.position = Victor.fromObject(data.position); // Position of top left corner 93 | this.direction = 0; // Direction (usually 0, 2, 4, or 6) 94 | 95 | this.rawConnections = data.connections; // Used in parsing connections from existing entity 96 | this.connections = []; // Wire connections 97 | this.rawNeighbours = data.neighbors; 98 | this.neighbours = []; 99 | this.circuitParameters = data.circuit_parameters || null; 100 | this.condition = this.parseCondition(data); // Condition in combinator 101 | this.constants = this.parseConstants(data); 102 | this.trainControlBehavior = this.parseTrainControlBehavior(data); 103 | this.constantEnabled = 104 | data.control_behavior && data.control_behavior.is_on !== undefined 105 | ? data.control_behavior.is_on 106 | : true; // Is constant combinator on/off 107 | 108 | this.parameters = data.paramaters || (myData.parameters ? {} : null); 109 | this.alertParameters = 110 | data.alert_parameters || (myData.alertParameters ? {} : null); 111 | 112 | this.filters = {}; // Filters for container 113 | this.requestFilters = {}; // Request filters for requester chest 114 | this.directionType = data.type || 'input'; // Underground belts input/output 115 | this.recipe = data.recipe ? this.bp.checkName(data.recipe) : undefined; 116 | this.bar = data.bar || -1; 117 | 118 | this.modules = data.items 119 | ? Object.keys(data.items).reduce( 120 | (obj: { [module: string]: number }, key) => { 121 | obj[this.bp.checkName(key)] = data.items[key]; 122 | return obj; 123 | }, 124 | {}, 125 | ) 126 | : {}; 127 | 128 | this.stationName = data.station ? data.station : undefined; 129 | this.manualTrainsLimit = data.manual_trains_limit || undefined; 130 | 131 | this.splitterFilter = data.filter 132 | ? this.bp.checkName(data.filter) 133 | : undefined; 134 | this.inputPriority = data.input_priority || undefined; 135 | this.outputPriority = data.output_priority || undefined; 136 | 137 | this.size = myData 138 | ? new Victor(myData.width || 0, myData.height || 0) // Size in Victor form 139 | : entityData[this.name] 140 | ? new Victor( 141 | entityData[this.name].width || 0, 142 | entityData[this.name].height || 0, 143 | ) 144 | : new Victor(1, 1); 145 | this.HAS_DIRECTION_TYPE = myData.directionType || false; 146 | this.CAN_HAVE_RECIPE = myData.recipe || false; 147 | this.CAN_HAVE_MODULES = myData.modules || 0; 148 | this.INVENTORY_SIZE = myData.inventorySize || 0; 149 | 150 | this.setDirection(data.direction || 0); 151 | 152 | this.parseFilters(data.filters); 153 | this.parseRequestFilters(data.request_filters); 154 | 155 | if (center) { 156 | this.position 157 | .add(new Victor(0.5, 0.5)) 158 | .subtract(this.size.clone().divide(new Victor(2, 2))); 159 | } 160 | this.position = new Victor( 161 | Math.round(this.position.x * 100) / 100, 162 | Math.round(this.position.y * 100) / 100, 163 | ); 164 | } 165 | 166 | // Beautiful string format 167 | /*toString() { 168 | let str = this.id+') '+this.name + ' =>\n'; 169 | str += ' position: '+this.position+'\n'; 170 | str += ' direction: '+this.direction+'\n'; 171 | str += ' connections: '; 172 | if (!this.connections.length) str += 'none\n'; 173 | else { 174 | str += '\n'; 175 | const two = this.name == 'arithmetic_combinator' || this.name == 'decider_combinator'; 176 | for (let i = 1; i <= (two ? 2 : 1); i++) { 177 | const side = two && i == 2 ? 'out' : 'in' 178 | const conns = this.connections.filter(c => c.side == i); 179 | if (conns.length) { 180 | if (two) str += ' '+side+':\n'; 181 | for (let j = 0; j < 2; j++) { 182 | const color = j == 0 ? 'red' : 'green'; 183 | const exactConns = conns.filter(c => c.color == color); 184 | if (exactConns.length) str += ' '+color+': '+exactConns.map(c => c.entity.id).join(',')+'\n'; 185 | } 186 | } 187 | } 188 | } 189 | if (this.condition) { 190 | str += ' condition:\n'; 191 | str += ' expr: '+this.condition.left+' '+this.condition.operator+' '+this.condition.right+'\n'; 192 | str += this.condition.countFromInput != undefined ? ' countFromInput: '+(this.condition.countFromInput || false)+'\n' : ''; 193 | str += this.condition.out != undefined ? ' out: '+this.condition.out+'\n' : ''; 194 | } 195 | return str; 196 | }*/ 197 | 198 | ///////////////////////////////////// 199 | ///// Parsing from existing blueprint 200 | ///////////////////////////////////// 201 | 202 | // Parse connections into standard Entity format 203 | parseConnections(entityList: any) { 204 | const conns = this.rawConnections; 205 | if (conns) { 206 | for (let side in conns) { 207 | if (side != '1' && side != '2') return; // Not a number! 208 | for (let color in conns[side]) { 209 | for (let i = 0; i < conns[side][color].length; i++) { 210 | const id = conns[side][color][i]['entity_id']; 211 | const connection: Connection = { 212 | entity: entityList[id - 1], 213 | color: color == 'red' ? 'red' : 'green', // Garbage to make typescript shut up 214 | side: parseInt(side) == 1 ? 1 : 2, 215 | id: conns[side][color][i]['circuit_id'], 216 | }; 217 | this.connections.push(connection); 218 | } 219 | } 220 | } 221 | } 222 | 223 | if (this.rawNeighbours) { 224 | this.neighbours = this.rawNeighbours.map((id) => entityList[id - 1]); 225 | } 226 | } 227 | 228 | // Parse filters into standard Entity format 229 | parseFilters(filters: any) { 230 | // Parse filters from json (for constructor) 231 | if (!filters) return []; 232 | for (let i = 0; i < filters.length; i++) { 233 | const name = this.bp.checkName(filters[i].name); 234 | 235 | const final_position = filters[i].index - 1; 236 | const final_name = name; 237 | 238 | this.setFilter(final_position, final_name); 239 | } 240 | } 241 | 242 | // Parse request filters into standard Entity format 243 | parseRequestFilters(request_filters: any) { 244 | // Parse request_filters from json (for constructor) 245 | if (!request_filters) return []; 246 | for (let i = 0; i < request_filters.length; i++) { 247 | request_filters[i].name = this.bp.checkName(request_filters[i].name); 248 | this.setRequestFilter( 249 | request_filters[i].index - 1, 250 | request_filters[i].name, 251 | request_filters[i].count, 252 | ); 253 | } 254 | } 255 | 256 | // Parse condition into standard Entity format 257 | parseCondition(data: any) { 258 | const controlBehavior = data.control_behavior; 259 | const condition = 260 | (controlBehavior && 261 | (controlBehavior.decider_conditions || 262 | controlBehavior.arithmetic_conditions || 263 | controlBehavior.circuit_condition)) || 264 | {}; 265 | if (!controlBehavior) return {}; 266 | if (condition.first_signal) 267 | condition.first_signal.name = this.bp.checkName( 268 | condition.first_signal.name, 269 | ); 270 | if (condition.second_signal) 271 | condition.second_signal.name = this.bp.checkName( 272 | condition.second_signal.name, 273 | ); 274 | if (condition.output_signal) 275 | condition.output_signal.name = this.bp.checkName( 276 | condition.output_signal.name, 277 | ); 278 | const out: CombinatorData = { 279 | left: condition.first_signal ? condition.first_signal.name : undefined, 280 | right: condition.second_signal 281 | ? condition.second_signal.name 282 | : condition.constant 283 | ? parseInt(condition.constant) 284 | : undefined, 285 | out: condition.output_signal ? condition.output_signal.name : undefined, 286 | operator: undefined, 287 | 288 | controlEnable: controlBehavior.circuit_enable_disable, // circuit_enable_disable, true/false 289 | readContents: controlBehavior.circuit_read_hand_contents, // circuit_read_hand_contents, true/false 290 | readMode: 291 | controlBehavior.circuit_contents_read_mode != undefined 292 | ? condition.circuit_contents_read_mode == 0 293 | ? 'pulse' 294 | : 'hold' 295 | : undefined, 296 | countFromInput: undefined, 297 | }; 298 | [ 299 | condition.first_signal, 300 | condition.second_signal, 301 | condition.output_signal, 302 | ].forEach((signal) => { 303 | if (signal && !entityData[signal.name]) 304 | entityData[signal.name] = { type: signal.type }; 305 | }); 306 | if (this.name == 'decider_combinator') { 307 | out.countFromInput = condition.copy_count_from_input == 'true'; 308 | } 309 | 310 | if (condition.comparator) 311 | // Set operator 312 | out.operator = condition.comparator == ':' ? '=' : condition.comparator; 313 | else out.operator = condition.operation; 314 | 315 | return out; 316 | } 317 | 318 | // Parse constants if this is a constant combinator 319 | parseConstants(data: any) { 320 | if (this.name != 'constant_combinator') return undefined; 321 | else if (!data.control_behavior || !data.control_behavior.filters) 322 | return {}; 323 | const constants: { 324 | [position: number]: { name: string; count: number }; 325 | } = {}; 326 | 327 | data.control_behavior.filters.forEach((filter: any) => { 328 | if (!entityData[this.bp.checkName(filter.signal.name)]) { 329 | entityData[this.bp.checkName(filter.signal.name)] = { 330 | type: filter.signal.type, 331 | }; 332 | } 333 | 334 | constants[parseInt(filter.index) - 1] = { 335 | name: this.bp.checkName(filter.signal.name), 336 | count: filter.count || 0, 337 | }; 338 | }); 339 | 340 | return constants; 341 | } 342 | 343 | parseTrainControlBehavior(data: any) { 344 | if (!data.control_behavior) return {}; 345 | 346 | const controlBehavior = data.control_behavior; 347 | const keys = [ 348 | 'circuit_enable_disable', 349 | 'read_from_train', 350 | 'read_stopped_train', 351 | 'train_stopped_signal', 352 | 'set_trains_limit', 353 | 'trains_limit_signal', 354 | 'read_trains_count', 355 | 'trains_count_signal', 356 | ]; 357 | 358 | const out: Record = {}; 359 | for (let i = 0; i < keys.length; i++) { 360 | const key = keys[i]; 361 | if (controlBehavior[key] != undefined) { 362 | out[key] = controlBehavior[key]; 363 | } 364 | } 365 | 366 | return out; 367 | } 368 | 369 | //////////////// 370 | //////////////// 371 | //////////////// 372 | 373 | // Sets values in BP (tile data, parses connections). 374 | // Typically, when loading from an existing blueprint, all entities are creating at the same time, 375 | // and then all are placed at the same time (so each entity can parse the connections of the others) 376 | place(positionGrid: PositionGrid, entityList: any[]) { 377 | this.setTileData(positionGrid); 378 | this.parseConnections(entityList); 379 | return this; 380 | } 381 | 382 | // Remove entity from blueprint 383 | remove() { 384 | return this.bp.removeEntity(this); 385 | } 386 | 387 | // Cleans up tile data after removing 388 | removeCleanup(positionGrid: PositionGrid) { 389 | this.removeTileData(positionGrid); 390 | return this; 391 | } 392 | 393 | // Quick corner/center positions 394 | topLeft() { 395 | return this.position.clone(); 396 | } 397 | topRight() { 398 | return this.position 399 | .clone() 400 | .add(this.size.clone().multiply(new Victor(1, 0))); 401 | } 402 | bottomRight() { 403 | return this.position.clone().add(this.size); 404 | } 405 | bottomLeft() { 406 | return this.position 407 | .clone() 408 | .add(this.size.clone().multiply(new Victor(0, 1))); 409 | } 410 | center() { 411 | return this.position 412 | .clone() 413 | .add(this.size.clone().divide(new Victor(2, 2))); 414 | } 415 | 416 | // Adds self to grid array 417 | setTileData(positionGrid: PositionGrid) { 418 | this.tileDataAction( 419 | positionGrid, 420 | (x, y) => (positionGrid[x + ',' + y] = this), 421 | ); 422 | return this; 423 | } 424 | 425 | // Removes self from grid array 426 | removeTileData(positionGrid: PositionGrid) { 427 | this.tileDataAction( 428 | positionGrid, 429 | (x, y) => delete positionGrid[x + ',' + y], 430 | ); 431 | return this; 432 | } 433 | 434 | // Return true if this entity overlaps with no other 435 | checkNoOverlap(positionGrid: PositionGrid) { 436 | const ent = this.getOverlap(positionGrid); 437 | 438 | if (!ent) return true; 439 | 440 | if ( 441 | (this.name == 'gate' && ent.name == 'straight_rail') || 442 | (ent.name == 'gate' && this.name == 'straight_rail') 443 | ) 444 | return true; 445 | 446 | return false; 447 | } 448 | 449 | // Returns an item this entity overlaps with (or null) 450 | getOverlap(positionGrid: PositionGrid): Entity | null { 451 | let item: Entity | null = null; 452 | this.tileDataAction(positionGrid, (x, y) => { 453 | item = positionGrid[x + ',' + y] || item; 454 | }); 455 | return item; 456 | } 457 | 458 | // Do an action on every tile that this entity overlaps in a given positionGrid 459 | tileDataAction( 460 | positionGrid: PositionGrid, 461 | fn: (x: number, y: number) => void, 462 | ) { 463 | if (!positionGrid) return; 464 | const topLeft = this.topLeft(); 465 | const bottomRight = this.bottomRight().subtract(new Victor(0.9, 0.9)); 466 | for (let x = Math.floor(topLeft.x); x < bottomRight.x; x++) { 467 | for (let y = Math.floor(topLeft.y); y < bottomRight.y; y++) { 468 | fn(x, y); 469 | } 470 | } 471 | } 472 | 473 | // Connect current entity to another entity via wire 474 | connect( 475 | ent: Entity, 476 | { 477 | fromSide, 478 | toSide, 479 | color 480 | }: { 481 | fromSide?: Side 482 | toSide?: Side 483 | color: Color 484 | } 485 | ) { 486 | fromSide = convertSide(this, fromSide); 487 | toSide = convertSide(ent, toSide); 488 | 489 | const checkCombinator = (name: string) => { 490 | return name == 'decider_combinator' || name == 'arithmetic_combinator'; 491 | }; 492 | 493 | color = color == 'green' ? color : 'red'; 494 | 495 | this.connections.push({ 496 | entity: ent, 497 | color: color, 498 | side: fromSide, 499 | id: checkCombinator(ent.name) ? toSide.toString() : undefined, 500 | }); 501 | ent.connections.push({ 502 | entity: this, 503 | color: color, 504 | side: toSide, 505 | id: checkCombinator(this.name) ? fromSide.toString() : undefined, 506 | }); 507 | return this; 508 | } 509 | 510 | // Remove a specific wire connection given all details 511 | removeConnection( 512 | ent: Entity, 513 | { fromSide, 514 | toSide, 515 | color 516 | }: { 517 | fromSide?: Side 518 | toSide?: Side 519 | color: Color 520 | } 521 | ) { 522 | fromSide = convertSide(this, fromSide); 523 | toSide = convertSide(ent, toSide); 524 | color = color || 'red'; 525 | 526 | for (let i = 0; i < this.connections.length; i++) { 527 | if ( 528 | this.connections[i].entity == ent && 529 | this.connections[i].side == fromSide && 530 | this.connections[i].color == color 531 | ) { 532 | this.connections.splice(i, 1); 533 | break; 534 | } 535 | } 536 | for (let i = 0; i < ent.connections.length; i++) { 537 | if ( 538 | ent.connections[i].entity == this && 539 | ent.connections[i].side == toSide && 540 | ent.connections[i].color == color 541 | ) { 542 | ent.connections.splice(i, 1); 543 | break; 544 | } 545 | } 546 | return this; 547 | } 548 | 549 | // Remove all wire connections with entity (optionally of a specific color) 550 | removeConnectionsWithEntity(ent: Entity, color: Color) { 551 | for (let i = this.connections.length - 1; i >= 0; i--) { 552 | if ( 553 | this.connections[i].entity == ent && 554 | (!color || this.connections[i].color == color) 555 | ) 556 | this.connections.splice(i, 1); 557 | } 558 | 559 | for (let i = ent.connections.length - 1; i >= 0; i--) { 560 | if ( 561 | ent.connections[i].entity == this && 562 | (!color || ent.connections[i].color == color) 563 | ) 564 | ent.connections.splice(i, 1); 565 | } 566 | return this; 567 | } 568 | 569 | // Remove all wire connections 570 | removeAllConnections() { 571 | for (let i = 0; i < this.connections.length; i++) { 572 | let ent = this.connections[i].entity; 573 | for (let j = 0; j < ent.connections.length; j++) { 574 | if (ent.connections[j].entity == this) { 575 | ent.connections.splice(j, 1); 576 | break; 577 | } 578 | } 579 | } 580 | this.connections = []; 581 | return this; 582 | } 583 | 584 | setFilter(pos: number, name: string) { 585 | if (pos < 0) throw new Error('Filter index cannot be less than 0!'); 586 | name = this.bp.checkName(name); 587 | if (name == null) delete this.filters[pos]; 588 | else this.filters[pos] = name; 589 | return this; 590 | } 591 | 592 | setRequestFilter(pos: number, name: string, count: number) { 593 | if (pos < 0) throw new Error('Filter index cannot be less than 0!'); 594 | name = this.bp.checkName(name); 595 | if (name == null) delete this.requestFilters[pos]; 596 | else 597 | this.requestFilters[pos] = { 598 | name, 599 | count, 600 | }; 601 | return this; 602 | } 603 | 604 | removeAllFilters() { 605 | this.filters = {}; 606 | return this; 607 | } 608 | 609 | removeAllRequestFilters() { 610 | this.requestFilters = {}; 611 | return this; 612 | } 613 | 614 | // Sets condition of entity (for combinators) 615 | setCondition(opt: CombinatorData) { 616 | if (opt.countFromInput != undefined && this.name != 'decider_combinator') 617 | throw new Error('Cannot set countFromInput for ' + this.name); 618 | else if (opt.readMode && opt.readMode != 'pulse' && opt.readMode != 'hold') 619 | throw new Error('readMode in a condition must be "pulse" or "hold"!'); 620 | else if ( 621 | this.name == 'arithmetic_combinator' && 622 | (opt.left == 'signal_everything' || 623 | opt.out == 'signal_everything' || 624 | opt.left == 'signal_anything' || 625 | opt.out == 'signal_anything') 626 | ) 627 | throw new Error( 628 | 'Only comparitive conditions can contain signal_everything or signal_anything. Instead use signal_each', 629 | ); 630 | else if (opt.out == 'signal_each' && opt.left != 'signal_each') 631 | throw new Error( 632 | 'Left condition must be signal_each for output to be signal_each.' + 633 | (this.name != 'arithmetic_combinator' 634 | ? ' Use signal_everything for the output instead' 635 | : ''), 636 | ); 637 | 638 | if (opt.left) opt.left = this.bp.checkName(opt.left); 639 | if (typeof opt.right == 'string') opt.right = this.bp.checkName(opt.right); 640 | if (opt.out) opt.out = this.bp.checkName(opt.out); 641 | 642 | if (!this.condition) this.condition = {}; 643 | this.condition = { 644 | left: this.condition.left || opt.left, 645 | right: this.condition.right || opt.right, 646 | operator: this.condition.operator || opt.operator, 647 | countFromInput: this.condition.countFromInput || opt.countFromInput, 648 | out: this.condition.out || opt.out, 649 | 650 | controlEnable: this.condition.controlEnable || opt.controlEnable, // circuit_enable_disable, true/false 651 | readContents: this.condition.readContents || opt.readContents, // circuit_read_hand_contents, true/false 652 | readMode: this.condition.readMode || opt.readMode, // circuit_contents_read_mode, 0 or 1 653 | }; 654 | return this; 655 | } 656 | 657 | // Sets direction of entity 658 | setDirection(dir: number) { 659 | // if (this.direction == null) return this; // Prevent rotation when we know what things can rotate in defaultentities.js 660 | this.size = new Victor( 661 | dir % 4 == this.direction % 4 ? this.size.x : this.size.y, 662 | dir % 4 == this.direction % 4 ? this.size.y : this.size.x, 663 | ); 664 | this.direction = dir; 665 | return this; 666 | } 667 | 668 | setDirectionType(type: DirectionType) { 669 | if (!this.HAS_DIRECTION_TYPE) 670 | throw new Error( 671 | 'This type of item does not have a directionType! Usually only underground belts have these.', 672 | ); 673 | this.directionType = type; 674 | 675 | return this; 676 | } 677 | 678 | setRecipe(recipe: string) { 679 | if (!this.CAN_HAVE_RECIPE) 680 | throw new Error('This entity cannot have a recipe.'); 681 | this.recipe = this.bp.checkName(recipe); 682 | 683 | return this; 684 | } 685 | 686 | setBar(num: number) { 687 | if (!this.INVENTORY_SIZE) 688 | throw new Error('Only entities with inventories can have bars!'); 689 | else if (typeof num == 'number' && num < 0) 690 | throw new Error('You must provide a positive value to setBar()'); 691 | this.bar = typeof num != 'number' || num >= this.INVENTORY_SIZE ? -1 : num; 692 | 693 | return this; 694 | } 695 | 696 | setCircuitParameters(obj: any) { 697 | if (!this.circuitParameters) this.circuitParameters = {}; 698 | Object.keys(obj).forEach((key) => (this.circuitParameters[key] = obj[key])); 699 | 700 | return this; 701 | } 702 | 703 | setParameters(obj: any) { 704 | if (!this.parameters) this.parameters = {}; 705 | Object.keys(obj).forEach((key) => (this.parameters[key] = obj[key])); 706 | 707 | return this; 708 | } 709 | 710 | setAlertParameters(opt: AlertParameters) { 711 | if (!this.alertParameters) this.alertParameters = {}; 712 | 713 | Object.keys(opt).forEach( 714 | // @ts-ignore 715 | (key: keyof AlertParameters) => (this.alertParameters[key] = opt[key]), 716 | ); 717 | 718 | return this; 719 | } 720 | 721 | setConstant(pos: number, name: string, count: number) { 722 | if (this.name != 'constant_combinator') 723 | throw new Error('Can only set constants for constant combinators!'); 724 | else if (pos < 0 || pos >= 18) 725 | throw new Error( 726 | pos + ' is an invalid position (must be between 0 and 17 inclusive)', 727 | ); 728 | 729 | if (!this.constants) { 730 | this.constants = {}; 731 | } 732 | 733 | if (!name) delete this.constants[pos]; 734 | else 735 | this.constants[pos] = { 736 | name: this.bp.checkName(name), 737 | count: count == undefined ? 0 : count, 738 | }; 739 | 740 | return this; 741 | } 742 | 743 | setStationName(name: string) { 744 | this.stationName = name; 745 | return this; 746 | } 747 | 748 | setManualTrainsLimit(limit: number) { 749 | this.manualTrainsLimit = limit; 750 | return this; 751 | } 752 | 753 | setSplitterFilter(name: string) { 754 | this.splitterFilter = name; 755 | return this; 756 | } 757 | 758 | setInputPriority(priority?: Priority) { 759 | this.inputPriority = priority; 760 | return this; 761 | } 762 | 763 | setOutputPriority(priority?: Priority) { 764 | this.outputPriority = priority; 765 | return this; 766 | } 767 | 768 | getData() { 769 | const useValueOrDefault = (val: any, def: any) => 770 | val != undefined ? val : def; 771 | 772 | const getOptionData = (append: any = {}) => { 773 | if (!this.condition) return append; 774 | 775 | append.circuit_enable_disable = this.condition.controlEnable; 776 | append.circuit_read_hand_contents = this.condition.readContents; 777 | append.circuit_contents_read_mode = 778 | this.condition.readMode != undefined 779 | ? this.condition.readMode == 'pulse' 780 | ? 0 781 | : 1 782 | : undefined; 783 | 784 | return append; 785 | }; 786 | 787 | const getCondition = () => { 788 | // let key = this.name == 'arithmetic_combinator' ? 'arithmetic' : (this.name == 'decider_combinator' ? 'decider' : 'circuit'); 789 | const out: any = {}; 790 | 791 | out.first_signal = this.condition.left 792 | ? { 793 | type: entityData[this.condition.left].type, 794 | name: this.condition.left.replace(/_/g, '-'), 795 | } 796 | : undefined; 797 | out.second_signal = 798 | typeof this.condition.right == 'string' 799 | ? { 800 | type: entityData[this.condition.right].type, 801 | name: this.condition.right.replace(/_/g, '-'), 802 | } 803 | : undefined; 804 | out.constant = 805 | typeof this.condition.right == 'number' 806 | ? this.condition.right 807 | : undefined; 808 | out.operation = undefined; 809 | out.comparator = undefined; 810 | out.output_signal = this.condition.out 811 | ? { 812 | type: entityData[this.condition.out].type, 813 | name: this.condition.out.replace(/_/g, '-'), 814 | } 815 | : undefined; 816 | 817 | if (this.name != 'arithmetic_combinator') { 818 | out.comparator = this.condition.operator; 819 | out.copy_count_from_input = 820 | this.condition.countFromInput != undefined 821 | ? (!!this.condition.countFromInput).toString() 822 | : undefined; 823 | } else { 824 | out.operation = this.condition.operator; 825 | } 826 | return out; 827 | }; 828 | 829 | const getAlertParameters = ({ 830 | showAlert = false, 831 | showOnMap = true, 832 | message = '', 833 | icon = undefined, 834 | }: AlertParameters) => { 835 | if (icon) { 836 | // Allow shorthand (icon name only) by 837 | // looking up type 838 | if (typeof icon === 'string') { 839 | icon = { 840 | type: entityData[icon].type || '', 841 | name: icon.replace(/_/g, '-'), 842 | }; 843 | } else { 844 | icon = { 845 | type: icon.type.replace(/_/g, '-'), 846 | name: icon.name.replace(/_/g, '-'), 847 | }; 848 | } 849 | } 850 | return { 851 | show_alert: showAlert, 852 | show_on_map: showOnMap, 853 | alert_message: message, 854 | icon_signal_id: icon, 855 | }; 856 | }; 857 | 858 | return { 859 | name: this.bp.fixName(this.name), 860 | position: this.center().subtract(new Victor(0.5, 0.5)) as { 861 | x: number; 862 | y: number; 863 | }, 864 | direction: this.direction || 0, 865 | entity_number: -1, 866 | 867 | type: /*this.HAS_DIRECTION_TYPE*/ this.directionType 868 | ? this.directionType 869 | : undefined, 870 | recipe: /*this.CAN_HAVE_RECIPE &&*/ this.recipe 871 | ? this.bp.fixName(this.recipe) 872 | : undefined, 873 | bar: /*this.INVENTORY_SIZE &&*/ this.bar != -1 ? this.bar : undefined, 874 | 875 | station: this.stationName, 876 | manual_trains_limit: this.manualTrainsLimit, 877 | 878 | filter: this.splitterFilter 879 | ? this.bp.fixName(this.splitterFilter) 880 | : undefined, 881 | input_priority: this.inputPriority || undefined, 882 | output_priority: this.outputPriority || undefined, 883 | 884 | items: 885 | /*this.CAN_HAVE_MODULES &&*/ this.modules && 886 | Object.keys(this.modules).length 887 | ? Object.keys(this.modules).reduce( 888 | (obj: { [name: string]: number }, key) => { 889 | obj[this.bp.fixName(key)] = this.modules[key]; 890 | return obj; 891 | }, 892 | {}, 893 | ) 894 | : undefined, 895 | 896 | filters: makeEmptyArrayUndefined( 897 | Object.keys(this.filters).map((filterPosition: string) => { 898 | return { 899 | index: parseInt(filterPosition) + 1, 900 | name: this.bp.fixName(this.filters[parseInt(filterPosition)]), 901 | }; 902 | }), 903 | ), 904 | 905 | request_filters: makeEmptyArrayUndefined( 906 | Object.keys(this.requestFilters).map((index: string) => { 907 | const rFilter = this.requestFilters[parseInt(index)]; 908 | return { 909 | name: this.bp.fixName(rFilter.name), 910 | count: rFilter.count, 911 | index: parseInt(index) + 1, 912 | }; 913 | }), 914 | ), 915 | 916 | connections: 917 | this.connections.length || 918 | this.condition || 919 | Object.keys(this.condition).length || 920 | Object.keys(this.circuitParameters).length 921 | ? this.connections.reduce( 922 | ( 923 | obj: { 924 | [side: string]: { 925 | [color: string]: { 926 | entity_id: number; 927 | circuit_id?: string; 928 | }[]; 929 | }; 930 | }, 931 | connection, 932 | ) => { 933 | let side = connection.side; 934 | let color = connection.color; 935 | if (!obj[side]) obj[side] = {}; 936 | if (!obj[side][color]) obj[side][color] = []; 937 | obj[side][color].push({ 938 | entity_id: connection.entity.id, 939 | circuit_id: connection.id, 940 | }); 941 | return obj; 942 | }, 943 | {}, 944 | ) 945 | : undefined, 946 | 947 | neighbours: this.neighbours.map((ent) => ent.id), 948 | parameters: this.parameters 949 | ? { 950 | playback_volume: useValueOrDefault(this.parameters.volume, 1.0), 951 | playback_globally: useValueOrDefault( 952 | this.parameters.playGlobally, 953 | false, 954 | ), 955 | allow_polyphony: useValueOrDefault( 956 | this.parameters.allowPolyphony, 957 | true, 958 | ), 959 | } 960 | : undefined, 961 | 962 | alert_parameters: this.alertParameters 963 | ? getAlertParameters(this.alertParameters) 964 | : undefined, 965 | 966 | control_behavior: 967 | this.constants || 968 | this.condition || 969 | this.trainControlBehavior || 970 | this.name == 'decider_combinator' || 971 | this.name == 'arithmetic_combinator' 972 | ? getOptionData({ 973 | ...this.trainControlBehavior, 974 | 975 | filters: 976 | this.constants && Object.keys(this.constants).length 977 | ? Object.keys(this.constants).map((key, i) => { 978 | // @ts-ignore 979 | const data = this.constants[key]; 980 | return { 981 | signal: { 982 | name: this.bp.fixName(data.name), 983 | type: entityData[data.name].type, 984 | }, 985 | count: data.count != undefined ? data.count : 0, 986 | index: parseInt(key) + 1, 987 | }; 988 | }) 989 | : undefined, 990 | 991 | decider_conditions: 992 | this.name == 'decider_combinator' ? getCondition() : undefined, 993 | arithmetic_conditions: 994 | this.name == 'arithmetic_combinator' 995 | ? getCondition() 996 | : undefined, 997 | circuit_condition: 998 | !this.name.includes('combinator') && this.condition.left 999 | ? getCondition() 1000 | : undefined, 1001 | 1002 | is_on: 1003 | this.name == 'constant_combinator' && !this.constantEnabled 1004 | ? this.constantEnabled 1005 | : undefined, 1006 | 1007 | circuit_parameters: this.circuitParameters 1008 | ? { 1009 | signal_value_is_pitch: useValueOrDefault( 1010 | this.circuitParameters.signalIsPitch, 1011 | false, 1012 | ), 1013 | instrument_id: useValueOrDefault( 1014 | this.circuitParameters.instrument, 1015 | 0, 1016 | ), 1017 | note_id: useValueOrDefault(this.circuitParameters.note, 0), 1018 | } 1019 | : undefined, 1020 | }) 1021 | : undefined, 1022 | }; 1023 | } 1024 | } 1025 | 1026 | // Lib Functions 1027 | 1028 | // Convert 'in' or 'out' of wires (only combinators have both of these) to a 1 or 2. 1029 | function convertSide(ent: Entity, side?: Side) { 1030 | if (!side) return 1; 1031 | if (side == 1 || side == 2) return side; 1032 | else if (side == 'in' || side == 'out') { 1033 | if ( 1034 | ent && 1035 | ent.name != 'arithmetic_combinator' && 1036 | ent.name != 'decider_combinator' 1037 | ) 1038 | return 1; 1039 | else return side == 'in' ? 1 : 2; 1040 | } else throw new Error('Invalid side'); 1041 | } 1042 | 1043 | function makeEmptyArrayUndefined(arr: any[]) { 1044 | return arr.length ? arr : undefined; 1045 | } 1046 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import prettyJSON from 'prettyjson'; 4 | import Victor from 'victor'; 5 | 6 | import book from './book'; 7 | import entityData from './defaultentities'; 8 | import { generateElectricalConnections } from './electrical-connections'; 9 | import Entity from './entity'; 10 | import Tile from './tile'; 11 | import util from './util'; 12 | 13 | export default class Blueprint { 14 | name: string; 15 | description: string; 16 | icons: string[]; 17 | entities: Entity[]; 18 | tiles: Tile[]; 19 | entityPositionGrid: { [location: string]: Entity }; 20 | tilePositionGrid: { [location: string]: Tile }; 21 | version: number; 22 | checkWithEntityData: boolean; 23 | snapping: { grid: Position, position?: Position, absolute?: boolean }; 24 | 25 | constructor(data?: any, opt: BlueprintOptions = {}) { 26 | this.name = 'Blueprint'; 27 | this.icons = []; // Icons for Blueprint (up to 4) 28 | this.entities = []; // List of all entities in Blueprint 29 | this.tiles = []; // List of all tiles in Blueprint (such as stone path or concrete) 30 | this.entityPositionGrid = {}; // Object with tile keys in format "x,y" => entity 31 | this.tilePositionGrid = {}; 32 | this.version = 281479273971713; // Factorio version 1.1.35 33 | this.checkWithEntityData = 34 | opt.checkWithEntityData != undefined ? opt.checkWithEntityData : true; // make sure checkName() validates with entityData 35 | if (data) this.load(data, opt); 36 | } 37 | 38 | // All entities in beautiful string format 39 | toString(opt: ToObjectOpt) { 40 | this.setIds(); 41 | return prettyJSON.render(this.toObject(opt).blueprint, { 42 | noAlign: true, 43 | numberColor: 'magenta', 44 | }); 45 | } 46 | 47 | // Load blueprint from an existing one 48 | load(data: any, opt: BlueprintOptions = { fixEntityData: false }) { 49 | if (typeof data === 'string') { 50 | const version = data.slice(0, 1); 51 | if (version !== '0') { 52 | throw new Error('Cannot find decoder for blueprint version ' + version); 53 | } 54 | data = util.decode[version](data); 55 | } 56 | return this.fillFromObject(data, opt); 57 | } 58 | 59 | static test(str) { 60 | const version = str.slice(0, 1); 61 | return util.decode[version](str); 62 | } 63 | 64 | fillFromObject(data: any, opt: BlueprintLoadOptions) { 65 | if (data.hasOwnProperty('blueprint')) data = data.blueprint; 66 | 67 | if (!data.tiles) data.tiles = []; 68 | if (!data.entities) data.entities = []; 69 | if (!data.icons) data.icons = []; 70 | 71 | this.name = data.label; 72 | this.description = data.description; 73 | this.version = data.version; 74 | 75 | data.entities.forEach((entity: any) => { 76 | if (opt.fixEntityData) { 77 | const data: any = {}; 78 | data[this.jsName(entity.name)] = { type: 'item', width: 1, height: 1 }; 79 | Blueprint.setEntityData(data); 80 | } 81 | this.createEntityWithData(entity, opt.allowOverlap || false, true, true); // no overlap (unless option allows it), place altogether later, positions are their center 82 | }); 83 | this.entities.forEach((entity) => { 84 | entity.place(this.entityPositionGrid, this.entities); 85 | }); 86 | 87 | data.tiles.forEach((tile: any) => { 88 | this.createTile(tile.name, tile.position); 89 | }); 90 | 91 | this.icons = []; 92 | data.icons.forEach((icon: any) => { 93 | this.icons[icon.index - 1] = this.checkName(icon.signal.name); 94 | }); 95 | 96 | if (data['snap-to-grid']) { 97 | this.setSnapping(data['snap-to-grid'], data['absolute-snapping'], data['position-relative-to-grid']); 98 | } 99 | 100 | this.setIds(); 101 | 102 | return this; 103 | } 104 | 105 | placeBlueprint( 106 | bp: Blueprint, 107 | position: Position, 108 | rotations: number, 109 | allowOverlap: boolean, 110 | ) { 111 | // rotations is 0, 1, 2, 3 or any of the Blueprint.ROTATION_* constants. 112 | const entitiesCreated: Entity[] = []; 113 | bp.entities.forEach((ent) => { 114 | const data = ent.getData(); 115 | 116 | data.direction += (rotations || 0) * 2; 117 | // data.direction += 8; 118 | data.direction %= 8; 119 | 120 | if (rotations == 3) 121 | data.position = { x: data.position.y, y: -data.position.x }; 122 | else if (rotations == 2) 123 | data.position = { x: -data.position.x, y: -data.position.y }; 124 | else if (rotations == 1) 125 | data.position = { x: -data.position.y, y: data.position.x }; 126 | 127 | data.position.x += position.x; 128 | data.position.y += position.y; 129 | 130 | entitiesCreated.push( 131 | this.createEntityWithData(data, allowOverlap, true, true), 132 | ); 133 | }); 134 | 135 | entitiesCreated.forEach((e) => { 136 | e.place(this.entityPositionGrid, entitiesCreated); 137 | }); 138 | 139 | bp.tiles.forEach((tile) => { 140 | const data = tile.getData(); 141 | 142 | if (rotations == 3) 143 | data.position = { x: data.position.y, y: -data.position.x }; 144 | else if (rotations == 2) 145 | data.position = { x: -data.position.x, y: -data.position.y }; 146 | else if (rotations == 1) 147 | data.position = { x: -data.position.y, y: data.position.x }; 148 | 149 | data.position.x += position.x; 150 | data.position.y += position.y; 151 | 152 | this.createTileWithData(data); 153 | }); 154 | 155 | return this; 156 | } 157 | 158 | // Create an entity! 159 | createEntity( 160 | name: string, 161 | position: Position, 162 | direction?: number, 163 | allowOverlap?: boolean, 164 | noPlace?: boolean, 165 | center?: boolean, 166 | ) { 167 | return this.createEntityWithData( 168 | { 169 | name: name, 170 | position: position, 171 | direction: direction || 0, 172 | }, 173 | allowOverlap, 174 | noPlace, 175 | center, 176 | ); 177 | // Need to add to defaultentities.js whether something is rotatable. If not, set direction to null. 178 | } 179 | 180 | // Creates an entity with a data object instead of paramaters 181 | createEntityWithData( 182 | data: any, 183 | allowOverlap?: boolean, 184 | noPlace?: boolean, 185 | center?: boolean, 186 | ) { 187 | const ent = new Entity(data, this, center); 188 | if (allowOverlap || ent.checkNoOverlap(this.entityPositionGrid)) { 189 | if (!noPlace) ent.place(this.entityPositionGrid, this.entities); 190 | this.entities.push(ent); 191 | return ent; 192 | } else { 193 | const otherEnt = ent.getOverlap(this.entityPositionGrid); 194 | throw new Error( 195 | 'Entity ' + 196 | data.name + 197 | ' overlaps ' + 198 | // @ts-ignore 199 | otherEnt.name + 200 | ' entity (' + 201 | data.position.x + 202 | ', ' + 203 | data.position.y + 204 | ')', 205 | ); 206 | } 207 | } 208 | 209 | createTile(name: string, position: Position) { 210 | return this.createTileWithData({ name: name, position: position }); 211 | } 212 | 213 | createTileWithData(data: any) { 214 | const tile = new Tile(data, this); 215 | if (this.tilePositionGrid[data.position.x + ',' + data.position.y]) 216 | this.removeTile( 217 | this.tilePositionGrid[data.position.x + ',' + data.position.y], 218 | ); 219 | 220 | this.tilePositionGrid[data.position.x + ',' + data.position.y] = tile; 221 | this.tiles.push(tile); 222 | return tile; 223 | } 224 | 225 | // Returns entity at a position (or null) 226 | findEntity(pos: Position) { 227 | return this.entityPositionGrid[Math.floor(pos.x) + ',' + pos.y] || null; 228 | } 229 | 230 | findTile(pos: Position) { 231 | return this.tilePositionGrid[Math.floor(pos.x) + ',' + pos.y] || null; 232 | } 233 | 234 | // Removes a specific entity 235 | removeEntity(ent: Entity) { 236 | if (!ent) return false; 237 | else { 238 | ent.removeCleanup(this.entityPositionGrid); 239 | const index = this.entities.indexOf(ent); 240 | if (index == -1) return ent; 241 | this.entities.splice(index, 1); 242 | return ent; 243 | } 244 | } 245 | 246 | removeTile(tile: Tile) { 247 | if (!tile) return false; 248 | else { 249 | const index = this.tiles.indexOf(tile); 250 | if (index == -1) return tile; 251 | this.tiles.splice(index, 1); 252 | return tile; 253 | } 254 | } 255 | 256 | // Removes an entity at a position (returns false if no entity is there) 257 | removeEntityAtPosition(position: Position) { 258 | if (!this.entityPositionGrid[position.x + ',' + position.y]) return false; 259 | return this.removeEntity( 260 | this.entityPositionGrid[position.x + ',' + position.y], 261 | ); 262 | } 263 | 264 | removeTileAtPosition(position: Position) { 265 | if (!this.tilePositionGrid[position.x + ',' + position.y]) return false; 266 | return this.removeTile( 267 | this.tilePositionGrid[position.x + ',' + position.y], 268 | ); 269 | } 270 | 271 | // Set ids for entities, called in toJSON() 272 | setIds() { 273 | this.entities.forEach((entity, i) => { 274 | entity.id = i + 1; 275 | }); 276 | this.tiles.forEach((tile, i) => { 277 | tile.id = i + 1; 278 | }); 279 | return this; 280 | } 281 | 282 | setSnapping(size: Position, absolute?: boolean, absolutePosition?: Position) { 283 | this.snapping = { 284 | grid: size, 285 | absolute: absolute, 286 | position: absolutePosition, 287 | } 288 | } 289 | 290 | // Get corner/center positions 291 | getPosition( 292 | f: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight', 293 | xcomp: Math['min'] | Math['max'], 294 | ycomp: Math['min'] | Math['max'], 295 | ) { 296 | if (!this.entities.length) return new Victor(0, 0); 297 | return new Victor( 298 | this.entities.reduce( 299 | (best, ent) => xcomp(best, ent[f]().x), 300 | this.entities[0][f]().x, 301 | ), 302 | this.entities.reduce( 303 | (best, ent) => ycomp(best, ent[f]().y), 304 | this.entities[0][f]().y, 305 | ), 306 | ); 307 | } 308 | 309 | center() { 310 | return new Victor( 311 | (this.topLeft().x + this.topRight().x) / 2, 312 | (this.topLeft().y + this.bottomLeft().y) / 2, 313 | ); 314 | } 315 | topLeft() { 316 | return this.getPosition('topLeft', Math.min, Math.min); 317 | } 318 | topRight() { 319 | return this.getPosition('topRight', Math.max, Math.min); 320 | } 321 | bottomLeft() { 322 | return this.getPosition('bottomLeft', Math.min, Math.max); 323 | } 324 | bottomRight() { 325 | return this.getPosition('bottomRight', Math.max, Math.max); 326 | } 327 | 328 | // Center all entities 329 | fixCenter(aroundPoint?: Position) { 330 | if (!this.entities.length) return this; 331 | 332 | let offsetX = aroundPoint 333 | ? -aroundPoint.x 334 | : -Math.floor(this.center().x / 2) * 2; 335 | let offsetY = aroundPoint 336 | ? -aroundPoint.y 337 | : -Math.floor(this.center().y / 2) * 2; 338 | const offset = new Victor(offsetX, offsetY); 339 | this.entities.forEach((entity) => 340 | entity.removeTileData(this.entityPositionGrid), 341 | ); 342 | this.entities.forEach((entity) => { 343 | entity.position.add(offset); 344 | entity.setTileData(this.entityPositionGrid); 345 | }); 346 | this.tiles.forEach( 347 | (tile) => 348 | delete this.tilePositionGrid[tile.position.x + ',' + tile.position.y], 349 | ); 350 | this.tiles.forEach((tile) => { 351 | tile.position.add(offset); 352 | this.tilePositionGrid[tile.position.x + ',' + tile.position.y] = tile; 353 | }); 354 | return this; 355 | } 356 | 357 | // Quickly generate 2 (or num) icons 358 | generateIcons(num?: number) { 359 | if (!num) num = 2; 360 | num = Math.min(this.entities.length, Math.min(Math.max(num, 1), 4)); 361 | for (let i = 0; i < num; i++) { 362 | this.icons[i] = this.entities[i].name; 363 | if ( 364 | this.icons[i] === 'straight_rail' || 365 | this.icons[i] === 'curved_rail' 366 | ) { 367 | this.icons[i] = 'rail'; 368 | } 369 | } 370 | return this; 371 | } 372 | 373 | // Give luaString that gets converted by encode() 374 | toObject({ autoConnectPoles = true }: ToObjectOpt = {}) { 375 | this.setIds(); 376 | if (!this.icons.length) this.generateIcons(); 377 | if (autoConnectPoles) generateElectricalConnections(this); 378 | 379 | const entityInfo = this.entities.map((ent, i) => { 380 | const entData = ent.getData(); 381 | 382 | entData.entity_number = i + 1; 383 | 384 | return entData; 385 | }); 386 | const tileInfo = this.tiles.map((tile, i) => tile.getData()); 387 | const iconData = this.icons 388 | .map((icon, i) => { 389 | return icon 390 | ? { 391 | signal: { 392 | type: entityData[icon].type || 'item', 393 | name: this.fixName(icon), 394 | }, 395 | index: i + 1, 396 | } 397 | : null; 398 | }) 399 | .filter(Boolean); 400 | 401 | return { 402 | blueprint: { 403 | icons: iconData, 404 | entities: this.entities.length ? entityInfo : undefined, 405 | tiles: this.tiles.length ? tileInfo : undefined, 406 | item: 'blueprint', 407 | version: this.version || 0, 408 | label: this.name, 409 | description: this.description || undefined, 410 | "absolute-snapping": this.snapping ? this.snapping.absolute : undefined, 411 | "snap-to-grid": this.snapping ? this.snapping.grid : undefined, 412 | "position-relative-to-grid": this.snapping ? this.snapping.position : undefined 413 | }, 414 | }; 415 | } 416 | 417 | toJSON(opt: ToObjectOpt = {}) { 418 | return JSON.stringify(this.toObject(opt)); 419 | } 420 | 421 | // Blueprint string! Yay! 422 | encode(opt?: EncodeOpt) { 423 | return util.encode[opt?.version || 'latest'](this.toObject(opt || {})); 424 | } 425 | 426 | // Set entityData 427 | static setEntityData(obj: any) { 428 | let keys = Object.keys(obj); 429 | for (let i = 0; i < keys.length; i++) { 430 | entityData[keys[i]] = obj[keys[i]]; 431 | } 432 | } 433 | 434 | // Get entityData 435 | static getEntityData() { 436 | return entityData; 437 | } 438 | 439 | static get UP() { 440 | return 0; 441 | } 442 | 443 | static get RIGHT() { 444 | return 2; 445 | } 446 | 447 | static get DOWN() { 448 | return 4; 449 | } 450 | 451 | static get LEFT() { 452 | return 6; 453 | } 454 | 455 | static get ROTATION_NONE() { 456 | return 0; 457 | } 458 | 459 | static get ROTATION_90_CW() { 460 | return 1; 461 | } 462 | 463 | static get ROTATION_180_CW() { 464 | return 2; 465 | } 466 | 467 | static get ROTATION_270_CW() { 468 | return 3; 469 | } 470 | 471 | static get ROTATION_270_CCW() { 472 | return this.ROTATION_90_CW; 473 | } 474 | 475 | static get ROTATION_180_CCW() { 476 | return this.ROTATION_180_CW; 477 | } 478 | 479 | static get ROTATION_90_CCW() { 480 | return this.ROTATION_270_CW; 481 | } 482 | 483 | checkName(name: string) { 484 | name = this.jsName(name); 485 | if (!entityData[name] && this.checkWithEntityData) 486 | throw new Error( 487 | name + ' does not exist! You can add it by putting it into entityData.', 488 | ); 489 | return name; 490 | } 491 | 492 | jsName(name: string) { 493 | return typeof name == 'string' ? name.replace(/-/g, '_') : name; 494 | } 495 | 496 | fixName(name: string) { 497 | return name.replace(/_/g, '-'); 498 | } 499 | 500 | static getBook(str: string, opt?: BlueprintOptions) { 501 | return getBook(str, opt); 502 | } 503 | 504 | static toBook( 505 | blueprints: (Blueprint | undefined | null)[], 506 | activeIndex = 0, 507 | opt?: EncodeOpt, 508 | ) { 509 | return toBook(blueprints, activeIndex, opt); 510 | } 511 | 512 | static isBook(str: string) { 513 | return isBook(str); 514 | } 515 | } 516 | 517 | function getBook(str: string, opt?: BlueprintOptions) { 518 | return book(str, opt); 519 | } 520 | 521 | function toBook( 522 | blueprints: (Blueprint | undefined | null)[], 523 | activeIndex = 0, 524 | opt: EncodeOpt = {}, 525 | ): string { 526 | const obj = { 527 | blueprint_book: { 528 | blueprints: blueprints 529 | .map((bp, index) => (bp ? { ...bp.toObject(opt), index } : null)) 530 | .filter(Boolean), 531 | item: 'blueprint-book', 532 | active_index: activeIndex, 533 | version: 0, 534 | }, 535 | }; 536 | 537 | return util.encode[opt?.version || 'latest'](obj); 538 | } 539 | 540 | function isBook(str: string): boolean { 541 | const version = str.slice(0, 1); 542 | if (version !== '0') { 543 | throw new Error('No decoder found for blueprint book version ' + version); 544 | } 545 | let obj = util.decode[version](str); 546 | 547 | return typeof obj.blueprint_book === 'object'; 548 | } 549 | 550 | type Version = '0' | 'latest'; 551 | interface Position { 552 | x: number; 553 | y: number; 554 | } 555 | 556 | interface BlueprintLoadOptions { 557 | fixEntityData?: boolean; 558 | allowOverlap?: boolean; 559 | } 560 | 561 | export interface BlueprintOptions extends BlueprintLoadOptions { 562 | checkWithEntityData?: boolean; // Should we validate enitity names with entityData? Default true 563 | } 564 | 565 | interface EncodeOpt extends ToObjectOpt { 566 | version?: Version; 567 | } 568 | 569 | interface ToObjectOpt { 570 | autoConnectPoles?: boolean; 571 | } 572 | -------------------------------------------------------------------------------- /src/tile.ts: -------------------------------------------------------------------------------- 1 | import Victor from 'victor'; 2 | 3 | import entityData from './defaultentities'; 4 | import Blueprint from './index'; 5 | 6 | export default class Tile { 7 | id: number; 8 | bp: Blueprint; 9 | name: string; 10 | position: Victor; 11 | 12 | constructor(data: any, bp: Blueprint) { 13 | this.id = -1; 14 | this.bp = bp; 15 | this.name = this.bp.checkName(data.name); 16 | if ( 17 | !data.position || 18 | data.position.x == undefined || 19 | data.position.y == undefined 20 | ) 21 | throw new Error('Invalid position provided: ' + data.position); 22 | this.position = Victor.fromObject(data.position); 23 | } 24 | 25 | remove() { 26 | return this.bp.removeTile(this); 27 | } 28 | 29 | getData() { 30 | return { 31 | name: this.bp.fixName(this.name), 32 | position: this.position as { x: number; y: number }, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by anth on 21.05.2017. 3 | */ 4 | 5 | import { Buffer } from 'buffer'; 6 | import * as zlib from 'zlib'; 7 | 8 | const toExport = { 9 | /** 10 | * Parse blueprint string in .15 format 11 | * @param str blueprint string to parse 12 | * @returns {Object} Factorio blueprint object 13 | */ 14 | decode: { 15 | 0: (str: string) => { 16 | // Version 0 17 | let data: any = null; 18 | try { 19 | data = JSON.parse( 20 | zlib 21 | .inflateSync(Buffer.from(str.slice(1), 'base64')) 22 | .toString('utf8'), 23 | ); 24 | } catch (e) { 25 | throw e; 26 | } 27 | 28 | return data; 29 | }, 30 | latest: (str: string) => ({} as any), // Set later 31 | }, 32 | 33 | /** 34 | * Encode an arbitrary object 35 | * @param obj 36 | * @returns {string} object encoded in Factorio .15 format 37 | */ 38 | encode: { 39 | 0: (obj: any) => { 40 | // Version 0 41 | return '0' + zlib.deflateSync(JSON.stringify(obj)).toString('base64'); 42 | }, 43 | latest: (obj: any) => '', // Set later 44 | }, 45 | }; 46 | 47 | toExport.decode.latest = toExport.decode[0]; 48 | toExport.encode.latest = toExport.encode[0]; 49 | 50 | export default toExport; 51 | -------------------------------------------------------------------------------- /test/blueprint_generate_tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const Blueprint = require('../dist/factorio-blueprint.min.js'); 3 | const Victor = require('victor'); 4 | const util = require('./util'); 5 | 6 | /* 7 | * 8 | * Generation tests that are aimed at end-to-end tests. 9 | * 10 | * Format should be create a Blueprint object and add things to it; then test 11 | * via the 'toObject' method as that's the easiest way to get assertions. 12 | * Alternatives are: (a) use 'toJSON' and (b) use 'encode', however, 13 | * those two are standard transformations and can be tested elsewhere. 14 | * 15 | */ 16 | 17 | describe('Blueprint Generation', () => { 18 | describe('simple, small', () => { 19 | it('single wall piece', () => { 20 | const bp = new Blueprint(); 21 | bp.createEntity('stone_wall', { x: 3, y: 4 }); 22 | const obj = bp.toObject(); 23 | 24 | assert.equal(obj.blueprint.entities[0].name, 'stone-wall'); 25 | }); 26 | }); 27 | 28 | describe('directions', () => { 29 | it('supports belts going in all directions', () => { 30 | const bp = new Blueprint(); 31 | bp.createEntity('express_transport_belt', { x: 1, y: 0 }, Blueprint.DOWN); 32 | bp.createEntity('express_transport_belt', { x: 2, y: 1 }, Blueprint.LEFT); 33 | bp.createEntity('express_transport_belt', { x: 1, y: 2 }, Blueprint.UP); 34 | bp.createEntity( 35 | 'express_transport_belt', 36 | { x: 0, y: 1 }, 37 | Blueprint.RIGHT, 38 | ); 39 | const obj = bp.toObject(); 40 | 41 | assert.equal(obj.blueprint.entities[0].direction, 4); 42 | assert.equal(obj.blueprint.entities[0].name, 'express-transport-belt'); 43 | assert.equal(obj.blueprint.entities[1].direction, 6); 44 | assert.equal(obj.blueprint.entities[1].name, 'express-transport-belt'); 45 | assert.equal(obj.blueprint.entities[2].direction, 0); 46 | assert.equal(obj.blueprint.entities[2].name, 'express-transport-belt'); 47 | assert.equal(obj.blueprint.entities[3].direction, 2); 48 | assert.equal(obj.blueprint.entities[3].name, 'express-transport-belt'); 49 | }); 50 | }); 51 | 52 | describe('recipes', () => { 53 | it('supports recipes in assemblers', () => { 54 | const bp = new Blueprint(); 55 | bp.name = 'Stone Assembler3'; 56 | const e = bp.createEntity( 57 | 'assembling_machine_3', 58 | { x: 0, y: 0 }, 59 | Blueprint.DOWN, 60 | ); 61 | e.setRecipe('stone_wall'); 62 | 63 | const obj = bp.toObject(); 64 | 65 | assert.equal(obj.blueprint.entities[0].direction, 4); 66 | assert.equal(obj.blueprint.entities[0].recipe, 'stone-wall'); 67 | assert.equal(obj.blueprint.entities[0].name, 'assembling-machine-3'); 68 | }); 69 | }); 70 | 71 | describe('modules', () => { 72 | it('supports modules in assemblers', () => { 73 | const bp = new Blueprint(); 74 | bp.name = 'Stone Assembler3'; 75 | const e = bp.createEntity( 76 | 'assembling_machine_3', 77 | { x: 0, y: 0 }, 78 | Blueprint.UP, 79 | ); 80 | e.setRecipe('stone_wall'); 81 | e.modules['speed_module_3'] = 1; 82 | e.modules['productivity_module_3'] = 2; 83 | e.modules['effectivity_module_3'] = 1; 84 | 85 | const obj = bp.toObject(); 86 | 87 | assert.equal(obj.blueprint.entities[0].direction, 0); 88 | assert.equal(obj.blueprint.entities[0].recipe, 'stone-wall'); 89 | assert.equal(obj.blueprint.entities[0].name, 'assembling-machine-3'); 90 | assert.equal(obj.blueprint.entities[0].items['productivity-module-3'], 2); 91 | assert.equal(obj.blueprint.entities[0].items['effectivity-module-3'], 1); 92 | assert.equal(obj.blueprint.entities[0].items['speed-module-3'], 1); 93 | }); 94 | }); 95 | 96 | describe('filter inserters', () => { 97 | it('stack filter inserters have only one filter', () => { 98 | const bp = new Blueprint(); 99 | bp.name = 'stack filter inserter'; 100 | const e = bp.createEntity( 101 | 'stack_filter_inserter', 102 | { x: 0, y: 0 }, 103 | Blueprint.UP, 104 | ); 105 | e.setFilter(0, 'stone_wall'); 106 | 107 | const obj = bp.toObject(); 108 | 109 | assert.equal(obj.blueprint.entities[0].direction, 0); 110 | assert.equal(obj.blueprint.entities[0].name, 'stack-filter-inserter'); 111 | assert.equal(obj.blueprint.entities[0].filters[0].index, 1); // TODO possible bug here; the parse test has it indexed from 0. 112 | assert.equal(obj.blueprint.entities[0].filters[0].name, 'stone-wall'); 113 | }); 114 | it('have multiple filters', () => { 115 | const bp = new Blueprint(); 116 | bp.name = 'filter inserter'; 117 | const e = bp.createEntity( 118 | 'filter_inserter', 119 | { x: 0, y: 0 }, 120 | Blueprint.UP, 121 | ); 122 | e.setFilter(0, 'stone_wall'); 123 | e.setFilter(4, 'iron_plate'); 124 | 125 | const obj = bp.toObject(); 126 | 127 | assert.equal(obj.blueprint.entities[0].direction, 0); 128 | assert.equal(obj.blueprint.entities[0].name, 'filter-inserter'); 129 | assert.equal(obj.blueprint.entities[0].filters[0].index, 1); 130 | assert.equal(obj.blueprint.entities[0].filters[0].name, 'stone-wall'); 131 | assert.equal(obj.blueprint.entities[0].filters[1].index, 5); 132 | assert.equal(obj.blueprint.entities[0].filters[1].name, 'iron-plate'); 133 | }); 134 | }); 135 | 136 | describe('inventory filters', () => { }); 137 | 138 | describe('logistic request filters', () => { 139 | // it('storage chest ?', () => { 140 | // }); 141 | // it('request chest', () => { 142 | // }); 143 | // it('buffer chest', () => { 144 | // }); 145 | }); 146 | 147 | describe('bars', () => { 148 | it('has a box with no bar', () => { 149 | const bp = new Blueprint(); 150 | bp.name = 'box with bar'; 151 | const e = bp.createEntity('wooden_chest', { x: 0, y: 0 }, Blueprint.UP); 152 | 153 | // XXX BUG? Documentation suggests that -1 should disable the bar 154 | // e.setBar(-1); // disable the bar (all slots available) 155 | 156 | const obj = bp.toObject(); 157 | 158 | assert.equal(obj.blueprint.entities[0].name, 'wooden-chest'); 159 | assert.equal('undefined', typeof obj.blueprint.entities[0].bar); 160 | }); 161 | it('has a box with some bar', () => { 162 | const bp = new Blueprint(); 163 | bp.name = 'box with bar'; 164 | const e = bp.createEntity('wooden_chest', { x: 0, y: 0 }, Blueprint.UP); 165 | 166 | e.setBar(4); // Allow 4 slots to be used by machines. 167 | 168 | const obj = bp.toObject(); 169 | 170 | assert.equal(obj.blueprint.entities[0].name, 'wooden-chest'); 171 | assert.equal(obj.blueprint.entities[0].bar, 4); 172 | }); 173 | it('fails when trying to add a bar to something that does not have an inventory', () => { 174 | const bp = new Blueprint(); 175 | bp.name = 'box with bar'; 176 | const e = bp.createEntity('stone_wall', { x: 0, y: 0 }, Blueprint.UP); 177 | 178 | assert.throws(() => { 179 | e.setBar('not a number'); 180 | }, Error); 181 | }); 182 | it('fails when trying to add a bar with a negative number', () => { 183 | const bp = new Blueprint(); 184 | bp.name = 'box with bar'; 185 | const e = bp.createEntity('wooden_chest', { x: 0, y: 0 }, Blueprint.UP); 186 | 187 | assert.throws(() => { 188 | e.setBar(-9001); 189 | }, Error); 190 | }); 191 | }); 192 | 193 | describe('circuit conditions', () => { 194 | // it('can enable/disable train stops', () => { 195 | // }); 196 | // it('can handle signals', () => { 197 | // }); 198 | // it('can handle coloured lamps', () => { 199 | // }); 200 | // it('can handle programmable speakers', () => { 201 | // }); 202 | // it('can read electric ore miners', () => { 203 | // }); 204 | }); 205 | 206 | describe('connections', () => { 207 | it('has two power poles connected with red wire', () => { 208 | const bp = new Blueprint(); 209 | bp.name = 'Connected Wires'; 210 | const e1 = bp.createEntity( 211 | 'medium_electric_pole', 212 | { x: 0, y: 0 }, 213 | Blueprint.UP, 214 | ); 215 | const e2 = bp.createEntity( 216 | 'medium_electric_pole', 217 | { x: 0, y: 5 }, 218 | Blueprint.UP, 219 | ); 220 | e1.connect(e2, { color: 'red' }); 221 | 222 | const obj = bp.toObject(); 223 | 224 | assert.equal(obj.blueprint.entities[0].name, 'medium-electric-pole'); 225 | assert.equal(obj.blueprint.entities[0].entity_number, 1); 226 | assert.equal( 227 | obj.blueprint.entities[0].connections['1'].red[0].entity_id, 228 | 2, 229 | ); 230 | 231 | assert.equal(obj.blueprint.entities[1].name, 'medium-electric-pole'); 232 | assert.equal(obj.blueprint.entities[1].entity_number, 2); 233 | assert.equal( 234 | obj.blueprint.entities[1].connections['1'].red[0].entity_id, 235 | 1, 236 | ); 237 | }); 238 | }); 239 | 240 | describe('splitters', () => { 241 | it('correctly sets filter and priorities', () => { 242 | const bp = new Blueprint(); 243 | const ent = bp.createEntity( 244 | 'express_splitter', 245 | { x: 0, y: 0 }, 246 | Blueprint.UP, 247 | ); 248 | ent.setSplitterFilter('electronic_circuit'); 249 | ent.setInputPriority('left'); 250 | ent.setOutputPriority('right'); 251 | 252 | const obj = bp.toObject(); 253 | 254 | assert.equal(obj.blueprint.entities[0].filter, 'electronic-circuit'); 255 | assert.equal(obj.blueprint.entities[0].input_priority, 'left'); 256 | assert.equal(obj.blueprint.entities[0].output_priority, 'right'); 257 | }); 258 | }); 259 | 260 | describe('snapping', () => { 261 | const bp = new Blueprint(); 262 | let grid = new Victor(10, 12); 263 | bp.setSnapping(grid); 264 | it('should default to relative snapping', () => { 265 | assert.strictEqual(bp.snapping.absolute, undefined); 266 | }); 267 | it('should save snapping information', () => { 268 | assert.equal(bp.snapping.grid, grid); 269 | }); 270 | it('should support absolute snapping', () => { 271 | bp.setSnapping(grid, true); 272 | assert.strictEqual(bp.snapping.absolute, true); 273 | }); 274 | 275 | }); 276 | 277 | describe('icons', () => { 278 | it('should save icons', () => { 279 | const bp = new Blueprint(); 280 | bp.icons = ['electronic_circuit']; 281 | const obj = bp.toObject(); 282 | assert.equal(obj.blueprint.icons[0].signal.name, 'electronic-circuit'); 283 | assert.equal(obj.blueprint.icons[0].index, 1); 284 | }); 285 | 286 | it('should save icons correctly when first is not set', () => { 287 | const bp = new Blueprint(); 288 | bp.icons[2] = 'electronic_circuit'; 289 | 290 | const obj = bp.toObject(); 291 | assert.equal(obj.blueprint.icons[0].signal.name, 'electronic-circuit'); 292 | assert.equal(obj.blueprint.icons[0].index, 3); 293 | }); 294 | }); 295 | 296 | describe('train stations', () => { 297 | it('should save station name', () => { 298 | const bp = new Blueprint(); 299 | const station = bp.createEntity('train_stop', { x: 0, y: 0 }); 300 | station.setStationName('Test Station'); 301 | 302 | const obj = bp.toObject(); 303 | assert.equal(obj.blueprint.entities[0].name, 'train-stop'); 304 | assert.equal(obj.blueprint.entities[0].station, 'Test Station'); 305 | }); 306 | 307 | it('should set manual trains limit', () => { 308 | const bp = new Blueprint(); 309 | const station = bp.createEntity('train_stop', { x: 0, y: 0 }); 310 | station.setManualTrainsLimit(5); 311 | 312 | const obj = bp.toObject(); 313 | assert.equal(obj.blueprint.entities[0].name, 'train-stop'); 314 | assert.equal(obj.blueprint.entities[0].manual_trains_limit, 5); 315 | }); 316 | 317 | it('should set control behavior', () => { 318 | const bp = new Blueprint(); 319 | const station = bp.createEntity('train_stop', { x: 0, y: 0 }); 320 | station.setCondition({ 321 | left: 'signal-red', 322 | operator: '>', 323 | right: 0, 324 | }); 325 | station.trainControlBehavior = { 326 | circuit_enable_disable: true, 327 | read_from_train: true, 328 | read_stopped_train: true, 329 | train_stopped_signal: { 330 | type: 'virtual', 331 | name: 'signal-T', 332 | }, 333 | }; 334 | 335 | const obj = JSON.parse(JSON.stringify(bp.toObject())); 336 | assert.deepEqual(obj.blueprint.entities[0].control_behavior, { 337 | circuit_condition: { 338 | first_signal: { 339 | type: 'virtual', 340 | name: 'signal-red', 341 | }, 342 | comparator: '>', 343 | constant: 0, 344 | }, 345 | read_from_train: true, 346 | read_stopped_train: true, 347 | train_stopped_signal: { 348 | type: 'virtual', 349 | name: 'signal-T', 350 | }, 351 | }); 352 | }); 353 | }); 354 | 355 | }); 356 | 357 | describe('Blueprint output', () => { 358 | let bp = new Blueprint(); 359 | bp.name = "custom label"; 360 | bp.description = "custom description"; 361 | bp.setSnapping(new Victor(10, 20), true); 362 | let bpObj = bp.toObject().blueprint; 363 | 364 | it('correctly handles blueprint labels', () => { 365 | assert.equal(bpObj.label, "custom label"); 366 | }); 367 | 368 | it('correctly handles blueprint descriptions', () => { 369 | assert.equal(bpObj.description, "custom description"); 370 | }) 371 | 372 | it('correnctly handles snapping size', () => { 373 | assert.equal(bpObj["snap-to-grid"].x, 10); 374 | assert.equal(bpObj["snap-to-grid"].y, 20); 375 | }); 376 | it('correctly handles absolute snapping', () => { 377 | assert.strictEqual(bpObj['absolute-snapping'], true); 378 | }); 379 | 380 | }); 381 | 382 | describe('Blueprint Books', () => { 383 | const bp1 = new Blueprint(); 384 | bp1.name = 'First'; 385 | const bp2 = new Blueprint(); 386 | bp2.name = 'Second'; 387 | 388 | const bookString = Blueprint.toBook([bp1, bp2]); 389 | 390 | it('is a string', () => { 391 | assert.equal(typeof bookString, 'string'); 392 | }); 393 | 394 | it('checks bp string type', () => { 395 | assert.equal(Blueprint.isBook(bookString), true); 396 | assert.equal(Blueprint.isBook(bp1.encode()), false); 397 | }); 398 | 399 | it('parses book', () => { 400 | assert.equal(Blueprint.getBook(bookString).length, 2); 401 | }); 402 | 403 | it('has index on each blueprint', () => { 404 | const decoded = util.decode[0](bookString); 405 | assert.equal(decoded.blueprint_book.blueprints[0].index, 0); 406 | assert.equal(decoded.blueprint_book.blueprints[1].index, 1); 407 | }); 408 | 409 | it('sorts based on index', () => { 410 | const decoded = util.decode[0](bookString); 411 | const [decodedBp1, decodedBp2] = decoded.blueprint_book.blueprints; 412 | decoded.blueprint_book.blueprints = [decodedBp2, decodedBp1]; 413 | 414 | const encoded = util.encode[0](decoded); 415 | const book = Blueprint.getBook(encoded); 416 | // Should have looked at blueprint.index 417 | // and reordered the blueprints accordingly 418 | assert.equal(book[0].name, 'First'); 419 | assert.equal(book[1].name, 'Second'); 420 | }); 421 | 422 | it('encodes with undefined and null values', () => { 423 | const bookString2 = Blueprint.toBook([undefined, null, bp1]); 424 | 425 | const decoded = Blueprint.getBook(bookString2); 426 | assert.equal(decoded[0], undefined); 427 | assert.equal(decoded[1], undefined); 428 | assert.equal(decoded[2].name, 'First'); 429 | }); 430 | }); 431 | 432 | // vi: sts=2 ts=2 sw=2 et 433 | -------------------------------------------------------------------------------- /test/blueprint_parse_tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const Blueprint = require('../dist/factorio-blueprint.min.js'); 3 | const util = require('./util'); 4 | const Victor = require('victor'); 5 | 6 | /* 7 | * 8 | * Parsing tests that are aimed at end-to-end tests. 9 | * 10 | * Format should be a simple blueprint from in-game that demonstrates 11 | * features. Then various assertions based on that. 12 | * 13 | */ 14 | 15 | describe('Blueprint Parsing', () => { 16 | describe('metadata', () => { 17 | const input = 18 | '0eNp9j90OgjAMhd+l18Pg/EH2DL6BMWZANU2gW7ZiJIR3d4Mbr7w7bb6eczpD04/oA7GAmaHD2AbyQo7BAPk4DqCAWscRzG2GSC+2fSZl8pgRwUywHfL0tFEKCZajd0GKBnuBJd1zhx8w++WuAFlICDe7dZgePA4NhgT8NVLgXaSt2gzJr9DlsdydFExZ11XSKayjgO1GnXPg2tD8vKmgt8kw7a4urO3fGOJ6oC/7Y1Xr6lzr8nLQy/IFJUNeXw=='; 19 | let bp = new Blueprint(input); 20 | 21 | it('supports a label', () => { 22 | assert.equal(bp.name, 'Lorem'); 23 | }); 24 | it('supports a description', () => { 25 | assert.equal(bp.description, 'ipsum'); 26 | }); 27 | 28 | }); 29 | 30 | describe('simple, small', () => { 31 | it('2x walls centered on one of them', () => { 32 | const input = 33 | '0eNqNj8EKgzAQRP9lzhGqFiv5lVKKtktZiBsx0TZI/r2JXgrtoadllpm3syt6M9M4sXjoFXyz4qDPKxw/pDN558NI0GBPAxSkG7Jy3goVz84YRAWWO72gy3hRIPHsmXbKJsJV5qGnKRl+5RVG61LESr6WMEWyhTxiVF+I6i/ETkiAVGgrrj/+VFhocpu5aU9V2dbNoT7G+AaeUViT'; 34 | var bp = new Blueprint(input); 35 | var wall1 = bp.findEntity(new Victor(-1, -1)); 36 | 37 | assert.equal(wall1.name, 'stone_wall'); 38 | assert.equal(wall1.position.x, -1); 39 | assert.equal(wall1.position.y, -1); 40 | 41 | var wall2 = bp.findEntity(new Victor(1, 1)); 42 | assert.equal(wall2.name, 'stone_wall'); 43 | assert.equal(wall2.position.x, 1); 44 | assert.equal(wall2.position.y, 1); 45 | }); 46 | }); 47 | 48 | describe('directions', () => { 49 | it('supports belts going in all directions', () => { 50 | const input = 51 | '0eNqV0dFqwzAMBdB/uc82xEnoin9ljJG02jAkirHV0hD874sTGO2atuzRNjq6sia03Yl8cCywE9xh4Aj7PiG6b266fCejJ1g4oR4K3PT5JKHh6IcguqVOkBQcH+kCa5J6WfzVRNEPhTJ9KBCLE0drluUwfvKpbynMLR6lUPBDnMsGzq1nqlAYYbVJOdQfpfxV6OIDxahfadosXDFHPbpAh/Vpt2FXT0e9gzfdcsOt/zm5uSXr/LPLKuzV2hXOFOI6y/6tNPtqV1R1Sj8D1bWS'; 52 | var bp = new Blueprint(input); 53 | var n = bp.findEntity(new Victor(0, -1)); 54 | var e = bp.findEntity(new Victor(1, 0)); 55 | var s = bp.findEntity(new Victor(0, 1)); 56 | var w = bp.findEntity(new Victor(-1, 0)); 57 | 58 | assert.equal(n.name, 'transport_belt'); 59 | assert.equal(n.direction, Blueprint.UP); 60 | assert.equal(n.position.x, 0); 61 | assert.equal(n.position.y, -1); 62 | assert.equal(e.name, 'fast_transport_belt'); 63 | assert.equal(e.direction, Blueprint.RIGHT); 64 | assert.equal(e.position.x, 1); 65 | assert.equal(e.position.y, 0); 66 | assert.equal(s.name, 'transport_belt'); 67 | assert.equal(s.direction, Blueprint.DOWN); 68 | assert.equal(s.position.x, 0); 69 | assert.equal(s.position.y, 1); 70 | assert.equal(w.name, 'express_transport_belt'); 71 | assert.equal(w.direction, Blueprint.LEFT); 72 | assert.equal(w.position.x, -1); 73 | assert.equal(w.position.y, 0); 74 | }); 75 | }); 76 | 77 | describe('recipes', () => { 78 | it('supports recipes in assemblers', () => { 79 | const input = 80 | '0eNp9j8EKwjAQRP9lzim0KrXkV0QkrUtdSDalSdVS+u8m8eLJy8Iss29mN/R2oWlmidAbePASoC8bAo9ibN7FdSJocCQHBTEuKxMCud6yjJUzw4OFqga7Asud3tDNflUgiRyZvrwi1pssrqc5Gf6TFCYf0rGX3CABa4U1zZQw08ClUIg+eV/GWuS00k//vKPwpDkURNudD013bOvjad8/hm1RIQ=='; 81 | var bp = new Blueprint(input); 82 | var assembler = bp.findEntity(new Victor(0, 0)); 83 | 84 | assert.equal(assembler.name, 'assembling_machine_1'); 85 | assert.equal(assembler.direction, Blueprint.UP); 86 | assert.equal(assembler.position.x, -1); 87 | assert.equal(assembler.position.y, -1); 88 | assert.equal(assembler.recipe, 'stone_wall'); 89 | }); 90 | }); 91 | 92 | describe('modules', () => { 93 | it('supports modules in assemblers', () => { 94 | const input = 95 | '0eNp9kNFqwzAMRf9Fzw60zeiKf2WMkTg3rZgtG9spC8H/Pjt96Rj0xXCtq6sjbTTaBSGyZNIbsfGSSH9slPgqg21/eQ0gTZzhSJEMrqkhJbjRslw7N5gbC7qeiiKWCT+kj+VTESRzZjzydrF+yeJGxGp4naQo+FSbvTSCGnhQtNa3TogwvANFb76Ru3mBrf6Gl5o5BWDqnJ8W25LaJMwzTOZ7BfhbCLGq/5VTafj7wvrpPoruiGlnOl/eT8dLfz70b6X8AvmFbMw='; 96 | var bp = new Blueprint(input); 97 | var assembler = bp.findEntity(new Victor(0, 0)); 98 | 99 | assert.equal(assembler.name, 'assembling_machine_3'); 100 | assert.equal(assembler.direction, Blueprint.UP); 101 | assert.equal(assembler.position.x, -1); 102 | assert.equal(assembler.position.y, -1); 103 | assert.equal(assembler.recipe, 'rocket_fuel'); // XXX should be stone-wall? 104 | assert.equal(assembler.modules['speed_module_3'], 1); 105 | assert.equal(assembler.modules['effectivity_module_3'], 1); 106 | assert.equal(assembler.modules['productivity_module_3'], 2); 107 | //assert.equal(assembler.modules, {'speed_module_3': 1, 'productivity_module_3': 2, 'effectivity_module_3': 1}); // Should work out why this assertion does not pass. 108 | }); 109 | }); 110 | 111 | describe('filter inserters', () => { 112 | it('stack filter inserters have only one filter', () => { 113 | const input = 114 | '0eNqFj9EKgzAMRf/lPlfQOZz0V4YMddkI01TaOibSf1+rbOxtLyE35J7crOiGmSbL4qFXcG/EQZ9XOL5LO6SZXyaCBnsaoSDtmJTzbf/Ibjx4shmLIxsbBAWWK72gi9AokHj2TDtwE8tF5rGLm7r4g1KYjItuIylDJOYKS6zxxL66Uz/nvjS2RjJjCaFJEbbU+udJhWf0btiqPh2Kuqzy8hjCG3KaWUM='; 115 | var bp = new Blueprint(input); 116 | 117 | const entity = bp.findEntity(new Victor(0, 0)); 118 | 119 | assert.equal(entity.name, 'stack_filter_inserter'); 120 | assert.equal(entity.direction, Blueprint.UP); 121 | assert.equal(entity.position.x, 0); 122 | assert.equal(entity.position.y, 0); 123 | assert.equal(entity.filters['0'], 'iron_ore'); 124 | }); 125 | it('have multiple filters', () => { 126 | const input = 127 | '0eNp1j90KgzAMhd8l1x34M5z0VYYMddkIaFraOibSd19amexmNyHJSb6TbDBMC1pHHEBvQKNhD/q6gacn91PqhdUiaKCAMyjgfk7Vg6aA7kTs0UkCUQHxHd+gy9gpQA4UCHdULtYbL/Mgk7r8C1FgjZc9w8lXWIWCVaLA99Gd9zU6OOQMn4xDueJQq0MdjbXikvUuHZc/0T+PK3gJO9s27aUq27op6nOMH0+tXzQ='; 128 | var bp = new Blueprint(input); 129 | 130 | const entity = bp.findEntity(new Victor(0, 0)); 131 | 132 | assert.equal(entity.name, 'filter_inserter'); 133 | assert.equal(entity.direction, Blueprint.UP); 134 | assert.equal(entity.position.x, 0); 135 | assert.equal(entity.position.y, 0); 136 | assert.equal(entity.filters['0'], 'iron_ore'); 137 | assert.equal(entity.filters['1'], 'copper_ore'); 138 | }); 139 | }); 140 | 141 | describe('inventory filters', () => { 142 | // What sort of entity has inventory filters? 143 | }); 144 | 145 | describe('logistic filters', () => { 146 | it('knows about requester chests', () => { 147 | const input = 148 | '0eNqFkN1uwjAMhd/F14nUlomhvMqEUEhNsdQ6XX5gVdV3x2kH7G5Xlq1zvmN7hnOfcQzECcwM5DxHMF8zROrY9mWWphHBACUcQAHboXS97ygmctpdMSYd8DtLxQCLAuIWf8DUy1EBcqJEuDHXZjpxHs6iNPX/NAWjjwLwXDYRqBbTBKaSmF/V6UK9SLeEZ/SLbNubZYetdhRcpiRE53M5tqkE8nY0L0uwd333vn1La5EeyzXrD8yflym4SfS63v7w2dSH3b7afSzLA3pZc0M='; 149 | const bp = new Blueprint(input); 150 | 151 | const entity = bp.findEntity(new Victor(-1, 0)); 152 | 153 | assert.equal(entity.name, 'logistic_chest_requester'); 154 | assert.equal(entity.requestFilters['0'].name, 'advanced_circuit'); 155 | assert.equal(entity.requestFilters['0'].count, 200); 156 | assert.equal(entity.requestFilters['11'].name, 'raw_wood'); 157 | assert.equal(entity.requestFilters['11'].count, 100); 158 | }); 159 | it('knows about storage chests', () => { 160 | const input = 161 | '0eNqFjsEKgzAQRP9lzhG0LVbyK6UUtYtd0I0ka1Ek/16TXnrrcYaZx9vRjQvNnkVhd3DvJMDedgQepB1Tp9tMsGClCQbSTimNbuCg3Bf9i4IWQZ1vB0I0YHnSClvFuwGJsjJ9iTlsD1mmjvwx+McymF047k6SxZofG2wZEznb2B95gzf5kMd1cz1Vzbkuz5cYP3muTDQ='; 162 | const bp = new Blueprint(input); 163 | 164 | const entity = bp.findEntity(new Victor(1, 0)); 165 | assert.equal(entity.name, 'logistic_chest_storage'); 166 | }); 167 | it('knows about storage chests with filters', () => { 168 | const input = 169 | '0eNqFj8EKgzAQRP9lzxG0Fiv5lSKidmsXdGOTtSiSf2+itPTW4yw7b2Y2aIcZJ0ssoDegzrADfd3AUc/NEG+yTggaSHAEBdyMUQ2mJyfUJd0DnSROjG16BK+A+IYL6MxXCpCFhPAg7mKteR5btOHhH0vBZFywG44tlt2xgk5DhsXnHF7rOw2C9sB/cr9YsoYTYyOoM3Pcl/oqttqX6J/hCl6BsgcV5eWUlXmR5mfv3w0JYHM='; 170 | const bp = new Blueprint(input); 171 | 172 | const entity = bp.findEntity(new Victor(1, 0)); 173 | assert.equal(entity.name, 'logistic_chest_storage'); 174 | assert.equal(entity.requestFilters['0'].name, 'iron_ore'); 175 | assert.equal(entity.requestFilters['0'].count, 0); 176 | }); 177 | }); 178 | it('knows about buffer chests', () => { 179 | const input = 180 | '0eNqFkNsKgzAMht8l1xU8jE36KkNEXdwCtXU2HYr47ksdO9ztMuX7v+TvCq0JOE5kGfQK1DnrQZ9X8HS1jYlvvIwIGohxAAW2GeJk3JU8U5d0N/SctKHvcYJNAdkLzqCzrVKAlokJX8J9WGobhlZInf1RKRidl7Sz8YZ5DyygU1kx4T0IWfdkGKeX/b32Y/WMaJLRNIzi6lyIBbNU8l84/9Btw6JavmQuZBU77LX1zy8peMjS/a5jecqzsjimxWHbnvgJbh8='; 181 | const bp = new Blueprint(input); 182 | 183 | const entity = bp.findEntity(new Victor(1, 0)); 184 | 185 | assert.equal(entity.name, 'logistic_chest_buffer'); 186 | assert.equal(entity.requestFilters['0'].name, 'steel_plate'); 187 | assert.equal(entity.requestFilters['0'].count, 100); 188 | assert.equal(entity.requestFilters['11'].name, 'battery'); 189 | assert.equal(entity.requestFilters['11'].count, 200); 190 | }); 191 | 192 | describe('bars', () => { 193 | it('has a box with no bar', () => { 194 | const input = 195 | '0eNptjsEKgzAQRP9lzhG0Fiv5lVKK2qVdiKuYVSoh/97EXnrocZY3bzagdyvNC4vCBvAwiYe9Bnh+SufyTfeZYMFKIwykG3PySuSK4UVeEQ1YHvSGreLNgERZmb6aI+x3WceelgT8FRjMk0+dSfJe8hSJ22HLmH3HsP3502CjxR90015OVVs3ZX2O8QPQS0Ob'; 196 | const bp = new Blueprint(input); 197 | 198 | const entity = bp.findEntity(new Victor(-1, 0)); 199 | 200 | assert.equal(entity.name, 'steel_chest'); 201 | assert.equal(entity.bar, -1); 202 | }); 203 | it('has a box with some bar', () => { 204 | const input = 205 | '0eNptjt0KwjAMhd/lXFfYH3P0VURkm0EDXTbWTByj725bb7zwJnBCvi/nwOA2WlYWhT3A4ywe9nLA80N6l3a6LwQLVppgIP2Uklcidxqf5BXBgOVOb9gyXA1IlJXpq8lhv8k2DbTGg78Cg2X2kZkl/YuewmCPM4qHPlJVk7y5gP3pa/Ci1Weq7c5V2dVtUTchfADkJ0Wy'; 206 | const bp = new Blueprint(input); 207 | 208 | const entity = bp.findEntity(new Victor(0, 0)); 209 | 210 | assert.equal(entity.name, 'steel_chest'); 211 | assert.equal(entity.bar, 24); 212 | }); 213 | }); 214 | 215 | describe('arithmetic combinators', () => { 216 | it('describes multiplication', () => { 217 | const input = 218 | '0eNqVktFqwzAMRf9Fj8MZbbJ1Ja/9jDGCk2itILaDLZeV4H+fnEBX1nXdXgyypavjK03QDhFHT5ahnoA6ZwPUrxME2ls95Ds+jQg1EKMBBVabHGlPfDDI1BWdMy1Zzc5DUkC2xw+o1+lNAVomJlwE5+DU2Gha9JJwljLYUzQFDtixF73RDSiNRhek2NmMIILFRsEp6yoQRiu5NKNOsM6Hx/6yC0lUSSb5LhLPoRCllNQVSPlPkOdfOfYe0d4hKW+QVHfcvfZk9bjQlELTk19g5i8JG3s3NC0e9JGkWCq+VBt57ulM/k4+cHNn4i3tv1kjTQNmpSwXWOcVkrG6Eb1eQOBB6l3kMf4gfyTPUW7OHZaMYgfZnb9OOY9VQXnT+8VsWcb5N/XFuis4og8z52b7Uq631WZVPaX0CRchDRw='; 219 | const bp = new Blueprint(input); 220 | 221 | const entity = bp.findEntity(new Victor(-0.5, 2)); 222 | 223 | assert.equal(entity.name, 'arithmetic_combinator'); 224 | assert.equal(entity.condition.left, 'big_electric_pole'); 225 | assert.equal(entity.condition.out, 'signal_C'); 226 | assert.equal(entity.condition.operator, '*'); 227 | }); 228 | it('describes modulo', () => { 229 | const input = 230 | '0eNqVklFqwzAMhu8i2Jsz2mTrSu7QE4wRnERrBbEdbLmsBN99cgJdWdd1ezHIln59/qUJ2iHi6Mky1BNQ52yA+nWCQHurh3zHpxGhBmI0oMBqkyPtiQ8Gmbqic6Ylq9l5SArI9vgB9Tq9KUDLxISL4BycGhtNi14SzlIGe4qmwAE79qI3ugGl0eiCFDubEUSw2Cg4ZV0Fwmgll2bUCdb58NhfdiGJKskk30XiORSilJK6Ain/CfL8K8feI9o7JOUNkuqOu9eerB4XmlJoevILzPwlYWPvhqbFgz6SFEvFl2ojzz2dyd/JB27uTLyl/TdrpGnArJTlAuu8QjJWN6LXCwg8SL2LPMYf5I/kOcrNucOSUewgu/PXKeexKihver+YLcs4/6a+WHcFR/Rh5txsX8r1ttqsqqeUPgEYnQ0h'; 231 | const bp = new Blueprint(input); 232 | 233 | const entity = bp.findEntity(new Victor(-0.5, 2)); 234 | 235 | assert.equal(entity.name, 'arithmetic_combinator'); 236 | assert.equal(entity.condition.left, 'big_electric_pole'); 237 | assert.equal(entity.condition.out, 'signal_M'); 238 | assert.equal(entity.condition.operator, '%'); 239 | }); 240 | }); 241 | 242 | describe('arithmetic combinators', () => { 243 | it('uses "each"', () => { 244 | const input = 245 | '0eNqVkttqwzAMht9F185ok7bbcrEXGSM4idoK4gOKUxaC331yMkJpt7LdGHT8P0uaoO4G9Ew2QDkBNc72UL5P0NPJ6i75wugRSqCABhRYbZLVYkMtctY4U5PVwTFEBWRb/IRyGz8UoA0UCJduszFWdjA1siSsfQy2NJgMO2wCU5N516GoeNdLsbNJXxpmBwVj6qtAAK3k0sw5wTY9jO21ComVSyZxM1CYTSGKMao7kPzRh+4xNk/7FaQlXjgWMWcDu66q8awvJMVS8d2yklhLK/GRuA/V3XgvxGEQzwq0ZGSomzMs/+6DTlvavSbLeM0zZQlvkBx+FKHBhurIzlRk/SC5R931GP8+tjQnBXmKnhjR3saLX+ZY/HOh+4f7/FH6ZqP5TCJ3Nt9leXXGCi7I/ax1eHnOty/FYVPsYvwCygX/SA=='; 246 | const bp = new Blueprint(input); 247 | 248 | const entity = bp.findEntity(new Victor(-0.5, 1)); 249 | 250 | assert.equal(entity.name, 'decider_combinator'); 251 | assert.equal(entity.condition.left, 'signal_each'); 252 | assert.equal(entity.condition.right, 49); 253 | assert.equal(entity.condition.operator, '>'); 254 | }); 255 | }); 256 | 257 | describe('constant combinators', () => { 258 | it('basic setup', () => { 259 | const input = 260 | '0eNqNkdFqwzAMRf9Fzw7UyehCfmWU4jjaJrDl4DihIfjfJ7tQCoXRFxuZq3t15ANGt+IciRMMB5ANvMDwdcBCP2xceUv7jDAAJfSggI0vVdElw6mxwY/EJoUIWQHxhDcYdL4oQE6UCO92tdivvPoRowj+NVIwh0V6A5d88Ws6BbtcWiKkIcXgriP+mo1ELZJvcgnjW4ObaTNscWosRbtSgmq5Fvz+9ADo8+UexWjLHEvx0+WIOD0DkVStiHNWL5DtI9TjRKtv0IlbJNvMweErZVsh9fvBugbLqivi8PSVCjbZR7U+95+t7rvzqfvI+Q/5g6g5'; 261 | const bp = new Blueprint(input); 262 | 263 | const entity = bp.findEntity(new Victor(-3, -1)); 264 | 265 | assert.equal(entity.name, 'constant_combinator'); 266 | assert.equal(entity.constants['7'].name, 'advanced_circuit'); 267 | assert.equal(entity.constants['7'].count, 80); 268 | // XXX should assert on {entity}.control_behaviour.is_on == true / undefined. 269 | }); 270 | it('with "output off" set', () => { 271 | const input = 272 | '0eNqNkeGKwyAQhN9lfxuoydELeZWjFGO2dwu6BjXhQvDdz02hFApH/ygr48x8usPoFpwjcYZhB7KBEwxfOyT6ZuPkLG8zwgCU0YMCNl4m0WXDubHBj8QmhwhFAfGEvzDoclGAnCkT3u2OYbvy4keMVfCvkYI5pHo3sORXv6ZVsNVN14h6IcfgriP+mJWqukpu5DLGt4qbaTVscWosRbtQhsNyEfz+9ADoBYDSVRrcjEt4D2a00iqJu5Yl4vSMR3Vqy6WUol6Q20cFjxMtvkFX3SLZZg4OX5m7A1m/H6yPYOktwMPTxypY6+sc1uf+s9V9dz51H6X8AWGKrQY='; 273 | const bp = new Blueprint(input); 274 | 275 | const entity = bp.findEntity(new Victor(-2, -1)); 276 | 277 | assert.equal(entity.name, 'constant_combinator'); 278 | assert.equal(entity.constants['7'].name, 'advanced_circuit'); 279 | assert.equal(entity.constants['7'].count, 80); 280 | // XXX should assert on {entity}.control_behaviour.is_on == false. 281 | }); 282 | }); 283 | 284 | describe('power switches', () => { 285 | it('simple example', () => { 286 | const input = 287 | '0eNqVUkFugzAQ/MueTWUgoRGHXvKMKEJgts1KYCPbhCLE37uGKEobpLYX5LFnZzxjJqiaHjtL2kM+ASmjHeSnCRx96LIJe37sEHIgjy0I0GUbUGcGtJEbyKsLzAJI1/gJeTyfBaD25AlXnQWMhe7bCi0TthUEQ8dDRgdHForky17AuC5Yn+/lrWmKCi/llYwNNEVW9eQLPqvvs+9knS9+uT02qFhOk4puIrB6OF+GInYyoLYrbemDF7zBvBI0D9LS0QRx+FisH3MSo2Q+M/nYy6cDAQNZXNYyNHXs45+c9DuHbZ86TO45Wqypb6M1DofpTIMbXWZLk/LvCeJgvOGc/tN5fzMOWZcXyB9+NwFXtG4hZ4fXJD6kmUx38/wFX4PeRw=='; 288 | const bp = new Blueprint(input); 289 | 290 | const entity = bp.findEntity(new Victor(0, 0)); 291 | 292 | assert.equal(entity.name, 'power_switch'); 293 | assert.equal(entity.condition.left, 'electronic_circuit'); 294 | assert.equal(entity.condition.right, 40); 295 | assert.equal(entity.condition.operator, '>'); 296 | // XXX should assert on {entity}.connections.Cu0/Cu1 297 | }); 298 | }); 299 | 300 | describe('circuit conditions', () => { 301 | it('can enable/disable train stops', () => { 302 | const input = 303 | '0eNqdlsFu2zAMht+FZ7uIZCf1fOihxwK9DdihKAzFZhMCtmxIctAg8LtPtIcsWxKA7SUGJfHjT1KEcoJtO+LgyAYoT0B1bz2UbyfwtLOm5bVwHBBKoIAdJGBNx5Yz1MKUANkGP6FU03sCaAMFwsV/No6VHbstunjg7Bmiq0196IdIG3ofXXrLcSImVTqBY/zqiG7IYb1sbhKIwoLr22qLe3Og3rFHTa4eKVRxrzljPsj5UF3JP5ALY1w561hOpMYew57sjpPh5IPhSqzY6AbjTOBQ8DRv/wmH1mxbrBry/IUyuBET8GibKvTVnB+UH6b1cXW2Ks52wEau6idMix67lMCzj+Ifh81lfSlaenrn01H6UgN4/fXy8hwVX3VBn+N02NDYpdhGvqM6HfqYyXU/1g/rpSHqYS0XpC5Cs52xwBtysi/KUd9So/9Tk99Rk39RTfEtNfdqsf57BfjO7PYhnWfsxozkc9jVvyOibzA3cqaWMh/lzJWUWYiZhRT5Q4zcSJGcj5Ap7pBSYqa4Q0pLmeIGqUyKlKvMpUh5McUzJO+5eITEN1OJJ0g+QEo8QfcGPT7f8wNfXvwfSOCAzi8PcPGoVZFtVlk+Tb8BQcK+7A=='; 304 | const bp = new Blueprint(input); 305 | const train_stop = bp.findEntity(new Victor(-12, -2)); 306 | 307 | assert.equal(train_stop.name, 'train_stop'); 308 | assert.equal(train_stop.position.x, -12.5); 309 | assert.equal(train_stop.position.y, -2.5); 310 | assert.equal(train_stop.condition.controlEnable, true); 311 | assert.equal(train_stop.condition.left, 'signal_anything'); 312 | assert.equal(train_stop.condition.operator, '>'); 313 | // XXX assert.equal(train_stop.condition.constant, 0); 314 | // XXX assert.equal(train_stop.condition.modes['send_to_train'], 'false'); 315 | }); 316 | // it('can handle signals', () => { 317 | // }); 318 | // it('can handle coloured lamps', () => { 319 | // }); 320 | // it('can handle programmable speakers', () => { 321 | // }); 322 | it('can read electric ore miners', () => { 323 | const input = 324 | '0eNrVVW1rwjAQ/ivjPqejSTt1/StDpC83PUhTSdIxkfz3JSkTdROjg7F9SXu95nm5Nnd7aOSIW03KQrUHagdloHrZg6G1qmV4ZndbhArIYg8MVN2HCCW2VlOb9aRIrbNOk5TgGJDq8B0q7thVDF3T8RbhlgxQWbKEk4YY7FZq7BvUHvMKO4PtYPzuQQVKj5iJxycGu+nGE3Wk/b6YLxl4q1YPctXgpn6jQYdNLel2JLtCVTcSVx2ZcIXqtZYG2SGtse78YoZRt0Gr1eNJdkpMr/VD5wFyFwnVxG8CFw+Lxu7YKvmocEvnQvnO7Itb7ef/0X15wX1xcN9jR2OfHYqwHbzGr9++/HTPo/tEen5EPck5jTm/oK+8UZ+4S544k1OcyxMX5D0d5Bnrz916Y7N4/L7WbRZl5ac/jPgGcpYMWaZCzpMhRSrkIhUyT0V8TkVM1sjzVMjkSvL7m6X4/CF/rz/wn3dHfnd7/Nt+p37oJ2OcndXRuGYg6wZlGKQe2Rt9iEaNT7yhNtHobDEXfFHM8qJ07gOJG7mk'; 325 | const bp = new Blueprint(input); 326 | miner_1 = bp.findEntity(new Victor(-2, -2)); 327 | 328 | assert.equal(miner_1.name, 'electric_mining_drill'); 329 | // TODO circuit_read_resources & circuit_resource_read_mode 330 | }); 331 | }); 332 | 333 | describe('connections', () => { }); 334 | 335 | describe('splitters', () => { 336 | function asserts( 337 | bp, 338 | inputPriority = undefined, 339 | outputPriority = undefined, 340 | filter = undefined, 341 | ) { 342 | assert.equal(bp.entities[0].inputPriority, inputPriority); 343 | assert.equal(bp.entities[0].outputPriority, outputPriority); 344 | assert.equal(bp.entities[0].splitterFilter, filter); 345 | } 346 | 347 | it('normal splitter', () => { 348 | const input = 349 | '0eNp1jsEKgzAQRP9lzrGYSpXur5RStF3Kgq4hiaJI/r3GXnrpcYaZN7Oh6yd2XjSCNshz1AC6bQjy1rbPXlwdgyCRBxhoO2TFi/McQhFcLzGyRzIQffECsuluwBolCn9Zh1gfOg3dniT7n2LgxrAXR83LO6woTxeDFVSmjD1O0M9ng5l9OPJNVdprfbZVU6f0AanySJQ='; 350 | const bp = new Blueprint(input); 351 | asserts(bp); 352 | }); 353 | 354 | it('input priority right', () => { 355 | const input = 356 | '0eNp1j8EKgzAQRP9lzrFopUrzK6WItotd0BiStSiSf2+SXnrpcZadNzMHhmkl69gI9AF+LMZD3w54Hk0/pZvslqDBQjMUTD8nRZt15H3h7cQi5BAU2Dxpg67CXYGMsDB9WVnsnVnnIX7q6j9FwS4+GheTkiOsKE8XhR26zAF2lS6WXVzkRb/j8SVIebmd/hmj8CbnM6ity+ranKu6bUL4AG+1UbE='; 357 | const bp = new Blueprint(input); 358 | asserts(bp, 'right'); 359 | }); 360 | 361 | it('output priority left', () => { 362 | const input = 363 | '0eNp1j0EKwjAQRe/y11Eaiy3mKiKl1VECaRKSibSU3N2kbty4/MPMe382TCaRD9oy1AZ9dzZCXTdE/bKjqTNePUFBM80QsONcEy0+UIyH6I1mpoAsoO2DFiiZbwJkWbOmL2sP62DTPJVNJf9TBLyL5dDZai6wQ3M8C6xQTRG4xD7xUNq6UIAFYOjJqL69nfp5RuBNIe6gvm3kpTvJtu9y/gB0cFG/'; 364 | const bp = new Blueprint(input); 365 | asserts(bp, undefined, 'left'); 366 | }); 367 | 368 | it('filter green circuit', () => { 369 | const input = 370 | '0eNp1j8EKwjAQRP9lzqm0ihXzKyLSxlUW0m1ItmIp/XeTevHicZaZNzsLej9RiCwKu4DdKAn2siDxUzpfbjoHggUrDTCQbiiK3iFSSlUKnlUpYjVgudMbtlmvBiTKyvRlbWK+yTT02Wmb/xSDMKYcHKU0Z1hV744GM2ydCx7siynnPDmNo7CrHEc3saKUbi/an0UGL4ppo50OdXNu983h1K7rBwT3U1A='; 371 | const bp = new Blueprint(input); 372 | asserts(bp, undefined, undefined, 'electronic_circuit'); 373 | }); 374 | 375 | it('input priority left, output priority right, filter green circuit', () => { 376 | const input = 377 | '0eNp1UNtqwzAM/Zfz7IxkZS3zr4xRWk/tBI5sbHk0hPz75OxlDPZ4pHPR0YprbJQLi8Kv4JCkwr+tqHyXS+wzXTLBg5VmOMhl7ogeuVCtQ82RValgc2D5oAf8tL07kCgr04/XDpaztPlqTD/97+KQUzVhkp5sZsP49OKwwI8WcOPYSaaLFLQk4TAELqGxmjI1zU3P1iUVizNa4ftn37D8WUS6KfqZeyn/6wcOX1Tqnn86jNPr8Xk6nI7b9g2VX2WY'; 378 | const bp = new Blueprint(input); 379 | asserts(bp, 'left', 'right', 'electronic_circuit'); 380 | }); 381 | }); 382 | 383 | describe('train station', () => { 384 | it('should have station name and control behavior', () => { 385 | const input = 386 | '0eNqNUs1ugzAMfpXKZ6gKW0fHbZsqbc8wVSiA11mCBDmmWoXy7ktCVbXrDr2A/MXfT2JPUHcjDkxaoJyAGqMtlJ8TWNpr1QVMjgNCCSTYQwJa9aESVqRTK2YAlwDpFn+gzNwuAdRCQjirxOJY6bGvkX3Dmd9jS2OfYoeNMDXpYDr06oOxnmx08PWCxXKdwBHKtFgt197Hp9OeQDHkBFn4MLaXVuSr3O2cc8mNff5f/BvTLDt5ZrOjsOmqGr/VgQyHnoa4GUkq1KrusGrJhj+UwiMmPo9qqy82fRVNruBgOGB7fRKL89HNux+IZfTIOfvckb6Gh7cos5itOupJrjRP2P2SL0EyBj3xGzPqv5oRu1/zDdz9g8vC4PytRM3zgA9tkWWxVVaQF9v9fvGOjBD2LO5jebG+CRyQbeTlm+yxeM6Lp+d8tXnInfsFuMkA7g=='; 387 | const bp = new Blueprint(input); 388 | const trainStop = bp.entities.find((ent) => ent.name === 'train_stop'); 389 | assert.equal(trainStop.stationName, 'Insert Easter Egg Here'); 390 | assert.deepEqual(trainStop.trainControlBehavior, { 391 | circuit_enable_disable: true, 392 | read_from_train: true, 393 | read_stopped_train: true, 394 | train_stopped_signal: { type: 'virtual', name: 'signal-B' }, 395 | set_trains_limit: true, 396 | trains_limit_signal: { type: 'virtual', name: 'signal-A' }, 397 | read_trains_count: true, 398 | trains_count_signal: { type: 'virtual', name: 'signal-C' }, 399 | }); 400 | }); 401 | 402 | it('should have manual trains limit', () => { 403 | const input = 404 | '0eNqNkt1OxCAQhd9lrtnE1p/ucqc+hjGEtqNOUqCBobFp+u4C3Wzs6sVekTmc+ebws0A7RBw9WQa5AHXOBpBvCwT6tHrIGs8jggRiNCDAapMr9prsIbAbYRVAtsdvkNX6LgAtExNulFLMykbTok+G//oFjC6kFmfztIxJthnkoakSOgVi7wbV4peeyPns6ch3kVih1e2AqqeQV5DsIwrwqHv14Z1RZchOzgNH7Pc7pbhs/Tn4RJ5jUi7ZN8fhJZ88IG+woAYyxDvmWbsd+ZyRJei5v3PRXjOLdjvzFdack/V2w5AMRtvkvMpdPdb5+cozy1+/QsCEPpTe+lg9NKe6eTrVd8f7el1/AHgPyCM='; 405 | const bp = new Blueprint(input); 406 | assert.equal(bp.entities[0].manualTrainsLimit, 152); 407 | }); 408 | }); 409 | 410 | describe('snapping', () => { 411 | it('should preserve snapping grid size', () => { 412 | // a simple loop of fast belts in a 3x3 space with relative snapping 413 | const input = '0eNqd001rhDAQBuC/InOOi8bP9bY999ZjKSXq7BLQJCTZUhH/e0e9bFsp1YuSZPLwZjQj1N0djZXKQzVCi66x0nipFVRwCWj+FuhrUGPnHTBwSpjQ6/BmZTvXf0KVMBjoOTGQjVYOqtcRnLwp0c0FfjBIkvTY03Yl+nl0Fc6H3grljLY+nHGY96sWCYynNwaovPQSV24ZDO/q3tdoqeBPiIHRTq4nWAJGp2yJGJ+yaWK/NH5MozdFbqXFZl3lG3ayz+YPSb/Z6YadHrOj/9jZPjve05P8WL/59tcrjiXlP5PmG3Z5rMMbNv3Qyw2oHm4bg04QRHMvsjcdBk+kBs9aG1r6QOvWDpZxWpx5kZ95VCZ8mr4AU8k4pA=='; 414 | const bp = new Blueprint(input); 415 | assert.equal(bp.snapping.grid.x, 3); 416 | assert.equal(bp.snapping.grid.y, 3); 417 | 418 | }); 419 | it('should preserve absolute snapping', () => { 420 | // same as above, but with absolute snapping 421 | const input = '0eNqdk8FugzAMhl8F+RwqSCm03Lrz3mCapgBuFQmSKDHTKsS7z8AO3YamlUsix/bnP048QNX26Lw2BOUADYbaa0faGijhHPH5NbKXqMKWAggIRrmYbHz1upniP6DcC7jxOgpQVbBtTxhPUY4zoSTfowBdWxOgfBkg6KtR7ZRJN4dcQhN2zDWqm6yLChSTVyY46ymeqgKDtWmQK6XjqwA0pEnjgpuN25vpuwo9B/wJEuBs0MvVZuXJ7jBrT3eHcRS/aHIbjXeW3GiP9eKVK+z9Y2x5p/QbO1thZ9vYyX/Yh8fY6SM9ybf1W66/XrFNqfypNF9hH7d1eIXNH3qegPJuDAW0ikHT/H0NVPTE3OjZWsfOd/Rh6eExzYqTLPKTTI57OY6fhfJCzw=='; 422 | const bp = new Blueprint(input); 423 | assert.strictEqual(bp.snapping.absolute, true); 424 | }); 425 | 426 | }); 427 | it('should preserve absolute offset', () => { 428 | // as above, but offst from the absolute grid by 1, 1 429 | const input = 430 | '0eNqd091qgzAUB/BXkXMdS02/veuu9wZjjFhPS0CTkBzLRHz3nWhhXSdj9UbJ1y9/jp4OiqpB57UhyDsoMZy8dqStgRyOCc9fEntOCqwogIBglEvJphevy7j/E/KVgJafvQBVBFs1hGnc5fgk5OQbFOBs0JFMPVaK9BUfiGwgMib0yZoA+VsHQV+MquIGah1yGE1YcwKj6jg6q0ApeWWCs57SmA/ieVNiBPt3AWiIb8WRGwbth2nqAv1w4x/Qd+JbwOViM0ZcbPpe/NLkPI3fHLnUHk/jqpywV8/Z8i7pD3s9Ya/n2cv/2Jvn7OyZmmzn1VtOf73dvKTyMel2wt7Pq/CEzT/00AH5XcMKqBRDsVNvrZe8sJu8Wut48Yo+jDXcZ+vdQe62B7ncr2TffwEVn1DN'; 431 | const bp = new Blueprint(input); 432 | assert.equal(bp.snapping.position.x, 1); 433 | assert.equal(bp.snapping.position.y, 1); 434 | }); 435 | }); 436 | 437 | // vi: sts=2 ts=2 sw=2 et 438 | -------------------------------------------------------------------------------- /test/electric_connection_tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const Blueprint = require('../dist/factorio-blueprint.min.js'); 3 | 4 | describe('Electric Connections', () => { 5 | const bp = new Blueprint(); 6 | const positions = [ 7 | [0, 0], 8 | [5, 0], 9 | [0, 5], 10 | [5, 5], 11 | ]; 12 | for (const [x, y] of positions) { 13 | bp.createEntity('medium_electric_pole', { x, y }); 14 | } 15 | const obj = bp.toObject(); 16 | 17 | assert.deepStrictEqual(obj.blueprint.entities[0].neighbours, [2, 3]); 18 | assert.deepStrictEqual(obj.blueprint.entities[1].neighbours, [1, 4]); 19 | assert.deepStrictEqual(obj.blueprint.entities[2].neighbours, [1, 4]); 20 | assert.deepStrictEqual(obj.blueprint.entities[3].neighbours, [2, 3]); 21 | }); 22 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by anth on 21.05.2017. 3 | */ 4 | 5 | const {Buffer} = require('buffer'); 6 | const zlib = require('zlib'); 7 | 8 | 9 | const toExport = { 10 | /** 11 | * Parse blueprint string in .15 format 12 | * @param str blueprint string to parse 13 | * @returns {Object} Factorio blueprint object 14 | */ 15 | decode: { 16 | 0: (str) => { // Version 0 17 | let data = null; 18 | try { 19 | data = JSON.parse(zlib.inflateSync(Buffer.from(str.slice(1), 'base64')).toString('utf8')); 20 | } catch (e) { 21 | throw e; 22 | } 23 | 24 | return data; 25 | }, 26 | latest: null 27 | }, 28 | 29 | /** 30 | * Encode an arbitrary object 31 | * @param obj 32 | * @returns {string} object encoded in Factorio .15 format 33 | */ 34 | encode: { 35 | 0: (obj) => { // Version 0 36 | return '0' + zlib.deflateSync(JSON.stringify(obj)).toString('base64'); 37 | }, 38 | latest: null 39 | } 40 | }; 41 | 42 | toExport.decode.latest = toExport.decode[0]; 43 | toExport.encode.latest = toExport.encode[0]; 44 | 45 | module.exports = toExport; 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "noImplicitAny": false, 7 | "lib": ["es6", "es2017"], 8 | "module": "es6", 9 | "target": "es5", 10 | "allowJs": true, 11 | "allowSyntheticDefaultImports": true, 12 | "strictNullChecks": true, 13 | "moduleResolution": "node", 14 | "declaration": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: './src/index.ts', 5 | mode: 'production', 6 | output: { 7 | filename: './factorio-blueprint.min.js', 8 | library: 'Blueprint', 9 | libraryTarget: 'umd', 10 | globalObject: 'this', 11 | libraryExport: 'default', 12 | }, 13 | resolve: { 14 | extensions: ['.tsx', '.ts', '.js'], 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['babel-preset-env'], 25 | }, 26 | }, 27 | }, 28 | { 29 | test: /\.tsx?$/, 30 | use: [ 31 | { 32 | loader: 'ts-loader', 33 | options: { 34 | transpileOnly: false, 35 | }, 36 | }, 37 | ], 38 | exclude: /node_modules/, 39 | }, 40 | ], 41 | }, 42 | plugins: [new NodePolyfillPlugin()], 43 | }; 44 | --------------------------------------------------------------------------------