├── .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 | [](https://badge.fury.io/js/factorio-blueprint)
4 | [](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 |
--------------------------------------------------------------------------------