├── .gitignore ├── LICENSE ├── README.md ├── blueprint_gen ├── assembler_node.lua ├── blueprint_graph.lua ├── blueprint_section.lua ├── independent_repeating_entity.lua ├── mixer_node.lua └── util.lua ├── changelog.txt ├── control.lua ├── data.lua ├── demo └── crafting-unit.gif ├── gui ├── inputs_select_frame.lua ├── outputs_select_frame.lua └── root_names.lua ├── info.json ├── player_info.lua ├── prototype_info.lua ├── release_mode.lua ├── test.lua └── thumbnail.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FactorioBlueprinter 2 | This factorio mod helps you create crafting blueprints easily by selecting inputs and outputs 3 | 4 | ## Supporting Functions 5 | 6 | * Easily get a recipe's blueprint crafting unit 7 | * Change preferences for crafting machines and belts 8 | * Future features can be tracked in [Projects](https://github.com/Seancheey/FactorioBlueprinter/projects) 9 | -------------------------------------------------------------------------------- /blueprint_gen/assembler_node.lua: -------------------------------------------------------------------------------- 1 | require("prototype_info") 2 | --- @type Logger 3 | local logging = require("__MiscLib__/logging") 4 | --- @type ArrayList 5 | local ArrayList = require("__MiscLib__/array_list") 6 | --- @type Vector2D 7 | local Vector2D = require("__MiscLib__/vector2d") 8 | local PlayerInfo = require("player_info") 9 | local PrototypeInfo = require("prototype_info") 10 | local BlueprintGeneratorUtil = require("blueprint_gen.util") 11 | local average_amount_of = BlueprintGeneratorUtil.average_amount_of 12 | --- @type BlueprintSection 13 | local BlueprintSection = require("blueprint_gen.blueprint_section") 14 | 15 | --- @class AssemblerNode represent a group of crafting machines for crafting a single recipe 16 | --- @field recipe LuaRecipePrototype 17 | --- @field recipe_speed number how fast the recipe should be done per second 18 | --- @field targets table target assemblers that outputs are delivered to 19 | --- @field sources table assemblers that inputs are received from 20 | --- @field player_index player_index 21 | --- @type AssemblerNode 22 | local AssemblerNode = {} 23 | 24 | -- AssemblerNode class inherent Table class 25 | function AssemblerNode.__index (t, k) 26 | return AssemblerNode[k] or ArrayList[k] or t.recipe[k] 27 | end 28 | 29 | function AssemblerNode.__tostring() 30 | return serpent.line(self) 31 | end 32 | 33 | function AssemblerNode.new(o) 34 | assert(o.recipe and o.player_index) 35 | o.recipe_speed = o.recipe_speed or 0 36 | o.targets = o.targets or ArrayList.new {} 37 | o.sources = o.sources or ArrayList.new {} 38 | setmetatable(o, AssemblerNode) 39 | return o 40 | end 41 | 42 | --- generate a blueprint section with a single crafting machine unit 43 | --- @return BlueprintSection, number, InternalDirectionSpec 44 | function AssemblerNode:generate_crafting_unit() 45 | local section = BlueprintSection.new() 46 | local crafting_machine = PlayerInfo.get_crafting_machine_prototype(self.player_index, self.recipe) 47 | local available_inserters = PlayerInfo.unlocked_inserters(self.player_index) 48 | local crafting_machine_size = PrototypeInfo.get_size(crafting_machine) 49 | local crafter_width = crafting_machine_size.x 50 | local crafter_height = crafting_machine_size.y 51 | --- ideal crafting speed of the recipe, unit is recipe/second 52 | local ideal_crafting_speed = crafting_machine.crafting_speed / self.recipe.energy 53 | 54 | logging.log("crafter_width = " .. tostring(crafter_width) .. ", crafter_height = " .. tostring(crafter_height)) 55 | 56 | section:add({ 57 | -- set top-left corner of crafting machine to 0,0 58 | position = { x = math.floor(crafter_width / 2), y = math.floor(crafter_height / 2) }, 59 | name = crafting_machine.name, 60 | recipe = self.recipe.name 61 | }) 62 | 63 | -- specify available parallel transporting lines 64 | --- @class fulfilled_line 65 | --- @field item boolean true if it is unavailable 66 | --- @field fluid boolean true if fluid is unavailable 67 | 68 | --- @type table key is y coordinate of the line 69 | local fulfilled_lines = {} 70 | --- @type number[] 71 | local line_check_order = {} 72 | -- populate available transporting lines in order like -2, 2, -3, 3 ... 73 | -- available transporting line starting 2 block away from crafting machine, 74 | -- since 1 block away are all inserters 75 | for yoff = 2, 10, 1 do 76 | for side = -1, 1, 2 do 77 | local y = yoff * side + (side < 0 and 0 or crafter_height - 1) 78 | line_check_order[#line_check_order + 1] = y 79 | fulfilled_lines[y] = {} 80 | end 81 | end 82 | 83 | -- populate actual transporting lines for each ingredient 84 | local preferred_belt = PlayerInfo.get_preferred_belt(self.player_index) 85 | --- @class ConnectionSpec 86 | --- @field replaceable boolean required, if this connection point could be replaced by others 87 | --- @field entity LuaEntityPrototype nullable, inserter/pipe prototype 88 | --- @field direction defines.direction nullable, inserter/pipe direction 89 | --- @field transport_line_y number the connection point's corresponding transport line 90 | --- @field line_info TransportLineInfo transport line information 91 | 92 | --- @type table connection entity specification table which is keyed by its coordinate 93 | local connection_positions = setmetatable({}, { __index = function(t, k) 94 | for test_key, v in pairs(t) do 95 | if test_key == k then 96 | return v 97 | end 98 | end 99 | end 100 | }) 101 | do 102 | -- populate all connection positions 103 | for _, y in ipairs({ -1, crafter_height }) do 104 | for x = 0, crafter_width - 1, 1 do 105 | connection_positions[Vector2D.new(x, y)] = { 106 | replaceable = true 107 | } 108 | end 109 | end 110 | end 111 | 112 | --- @type table<'"input"'|'"output"', ArrayList|Vector2D[]> fluid connection point positions of the crafting machine, if available 113 | local fluid_box_positions = {} 114 | for _, connection_type in ipairs({ "output", "input" }) do 115 | fluid_box_positions[connection_type] = ArrayList.new(crafting_machine.fluid_boxes) 116 | :filter( 117 | function(box) 118 | local out = type(box) == "table" and box.production_type == connection_type 119 | return out 120 | end) 121 | :map( 122 | function(b) 123 | local connection_position = Vector2D.new( 124 | b.pipe_connections[1].position[1] + math.floor(crafter_width / 2), 125 | b.pipe_connections[1].position[2] + math.floor(crafter_height / 2) 126 | ) 127 | -- fill position with fluid box, so that inserters can't occupy this position 128 | connection_positions[connection_position].replaceable = false 129 | return connection_position 130 | end) 131 | end 132 | --- true for output fluid box index, false for input fluid box index 133 | local fluid_box_indices = { ["output"] = 1, ["input"] = 1 } 134 | 135 | --- @class TransportLineInfo 136 | --- @field crafting_items (Product|Ingredient)[] 137 | --- @field direction '"input"' | '"output"' 138 | --- @field type '"fluid"' | '"item"' 139 | 140 | --- @param recipe LuaRecipePrototype 141 | --- @return TransportLineInfo[] | ArrayList 142 | local function create_transport_line_info_list(recipe) 143 | --- @type TransportLineInfo[]|ArrayList 144 | local item_info_list = ArrayList.new() 145 | --- @type TransportLineInfo[]|ArrayList 146 | local fluid_info_list = ArrayList.new() 147 | -- iterate input ingredients, combine 2 input items into a single belt 148 | do 149 | local i = 1 150 | local next_line_items = ArrayList.new() 151 | while i <= #recipe.ingredients do 152 | local ingredient = recipe.ingredients[i] 153 | if ingredient.type == "fluid" then 154 | fluid_info_list:add { type = 'fluid', crafting_items = { ingredient }, direction = "input" } 155 | else 156 | next_line_items:add(ingredient) 157 | if #next_line_items == 2 then 158 | item_info_list:add { type = "item", crafting_items = next_line_items, direction = "input" } 159 | next_line_items = ArrayList.new() 160 | end 161 | end 162 | i = i + 1 163 | end 164 | if #next_line_items > 0 then 165 | item_info_list:add { type = 'item', direction = "input", crafting_items = next_line_items } 166 | end 167 | end 168 | -- iterate output products 169 | for _, product in ipairs(recipe.products) do 170 | if product.type == "item" then 171 | item_info_list:add { type = "item", direction = "output", crafting_items = { product } } 172 | else 173 | fluid_info_list:add { type = "fluid", direction = "output", crafting_items = { product } } 174 | end 175 | end 176 | item_info_list:addAll(fluid_info_list) 177 | return item_info_list 178 | end 179 | 180 | local transport_line_infos = create_transport_line_info_list(self.recipe) 181 | 182 | local direction_spec 183 | -- determine the directions of transport belts 184 | do 185 | local item_line_num = #transport_line_infos:filter(function(line_info) 186 | return line_info.type == "item" 187 | end) 188 | local item_output_direction = item_line_num % 2 == 0 and defines.direction.south or defines.direction.north 189 | direction_spec = PlayerInfo.get_internal_direction_spec(self.player_index, item_output_direction) 190 | end 191 | -- concatenate ingredients and products together 192 | for _, line_info in ipairs(transport_line_infos) do 193 | -- find next available transporting line to fill 194 | for _, y in ipairs(line_check_order) do 195 | local line = fulfilled_lines[y] 196 | if not line[line_info.type] then 197 | local y_closer_to_factory = y - (y > 0 and 1 or -1) 198 | local corresponding_fluid_box_position = fluid_box_positions[line_info.direction][fluid_box_indices[line_info.direction]] 199 | -- pre-check for any crafting machine prototypes with unknown fluid box support 200 | if line_info.type == "fluid" and corresponding_fluid_box_position == nil then 201 | logging.log("This mod recipe's crafting machine needs fluid box connection, which is not supported by the mod yet. Consider prioritize a built-in crafting machine instead? Failed to make blueprint :(", logging.E) 202 | logging.log("You can add support for this recipe by contributing it's fluid box connections in github: https://github.com/Seancheey/FactorioBlueprinter/blob/master/prototype_info.lua") 203 | return 204 | end 205 | if line_info.type == "fluid" and 206 | -- fluid box's connection position and transport line is at same side 207 | corresponding_fluid_box_position.y * y > 0 and 208 | -- line next to factory can use pipe directly, so allowed 209 | (fulfilled_lines[y_closer_to_factory] == nil or 210 | -- line's side towards factory will be used for underground pipe, which can't be fulfilled 211 | not fulfilled_lines[y_closer_to_factory]["item"]) then 212 | -- different fluid line can't be neighboring each other 213 | if fulfilled_lines[y + 1] then 214 | fulfilled_lines[y + 1].fluid = true 215 | end 216 | if fulfilled_lines[y - 1] then 217 | fulfilled_lines[y - 1].fluid = true 218 | end 219 | -- fluid line's side towards factory are used for underground pipe, so can't use 220 | if fulfilled_lines[y_closer_to_factory] then 221 | fulfilled_lines[y_closer_to_factory].item = true 222 | end 223 | -- occupy this line 224 | line.item = true 225 | line.fluid = true 226 | -- populate transportation line to section 227 | for x = 0, crafter_width - 1, 1 do 228 | section:add({ 229 | name = "pipe", 230 | position = { x = x, y = y } 231 | }) 232 | end 233 | 234 | if fulfilled_lines[y_closer_to_factory] == nil then 235 | -- pipe line next to factory only needs one connection pipe 236 | section:add({ 237 | name = "pipe", 238 | position = corresponding_fluid_box_position, 239 | direction = defines.direction.north 240 | }) 241 | else 242 | -- pipe line further needs a pair of underground connection pipe 243 | section:add({ 244 | name = "pipe-to-ground", 245 | position = { x = corresponding_fluid_box_position.x, y = y_closer_to_factory }, 246 | direction = y > 0 and defines.direction.south or defines.direction.north 247 | }) 248 | section:add({ 249 | name = "pipe-to-ground", 250 | position = corresponding_fluid_box_position, 251 | direction = y > 0 and defines.direction.north or defines.direction.south 252 | }) 253 | end 254 | fluid_box_indices[line_info.direction] = fluid_box_indices[line_info.direction] + 1 255 | break 256 | elseif line_info.type == "item" then 257 | line.item = true 258 | line.fluid = true 259 | -- populate transportation line to section 260 | for x = 0, crafter_width - 1, 1 do 261 | section:add({ 262 | name = preferred_belt.name, 263 | position = { x = x, y = y }, 264 | direction = line_info.direction == "input" and direction_spec.linearIngredientDirection or direction_spec.linearOutputDirection 265 | }) 266 | end 267 | local connection_y = y > 0 and crafter_height or -1 268 | --- @type string 269 | local inserter_type 270 | -- number of inserter needed to full-fill ideal crafting speed, this number is not guaranteed to be in blueprint 271 | local inserter_num_need = 1 272 | do 273 | -- determine what kind of inserter to use for the transport line 274 | local transport_line_distance = math.abs(y - connection_y) 275 | -- TODO should handle transport_line_distance = 3 situation 276 | transport_line_distance = (transport_line_distance <= 1) and 1 or 2 277 | local inserter_order = available_inserters[transport_line_distance] 278 | if inserter_order then 279 | local required_rotation_per_sec = 0 280 | for _, crafting_item in ipairs(line_info.crafting_items) do 281 | local avg_amount = average_amount_of(crafting_item) 282 | required_rotation_per_sec = required_rotation_per_sec + avg_amount * ideal_crafting_speed 283 | end 284 | -- use lower-level inserters if it's enough 285 | local found_satisfying_inserter = false 286 | for _, inserter in ipairs(inserter_order) do 287 | if PlayerInfo.inserter_items_speed(self.player_index, inserter) >= required_rotation_per_sec then 288 | inserter_type = inserter.name 289 | found_satisfying_inserter = true 290 | break 291 | end 292 | end 293 | -- even fastest inserter doesn't support required speed, use fastest 294 | if not found_satisfying_inserter then 295 | local fastest_inserter = inserter_order[#inserter_order] 296 | inserter_type = fastest_inserter.name 297 | inserter_num_need = math.ceil(required_rotation_per_sec / PlayerInfo.inserter_items_speed(self.player_index, fastest_inserter)) 298 | logging.log(serpent.line(line_info.crafting_items) .. " requires " .. tostring(inserter_num_need) .. " " .. fastest_inserter.name) 299 | end 300 | else 301 | if transport_line_distance == 1 then 302 | inserter_type = "inserter" 303 | else 304 | game.players[self.player_index].print("fail to find an unlocked inserter with arm length " .. transport_line_distance .. ", use long handed inserter instead") 305 | inserter_type = "long-handed-inserter" 306 | end 307 | end 308 | end 309 | 310 | -- iterate through possible positions for placing inserter 311 | for coordinate, connection_spec in pairs(connection_positions) do 312 | if coordinate.y == connection_y and connection_spec.replaceable == true then 313 | connection_positions[coordinate] = { 314 | replaceable = inserter_num_need ~= 1, 315 | direction = (line_info.direction == "output" and 1 or -1) * (connection_y < 0 and 1 or -1) > 0 and defines.direction.south or defines.direction.north, 316 | entity = game.entity_prototypes[inserter_type], 317 | transport_line_y = y, 318 | line_info = line_info 319 | } 320 | inserter_num_need = inserter_num_need - 1 321 | if inserter_num_need == 0 then 322 | break 323 | end 324 | end 325 | end 326 | break 327 | end 328 | end 329 | end 330 | end 331 | -- fill inserters and calculate corresponding item transfer speed 332 | do 333 | --- @type table 334 | local inlet_line_spec = {} 335 | --- @type table 336 | local outlet_line_spec = {} 337 | for coordinate, connection_spec in pairs(connection_positions) do 338 | -- logging.log("coordinate: " .. serpent.line(coordinate) .. " connection spec: " .. serpent.line(connection_spec)) 339 | if connection_spec.entity then 340 | section:add({ 341 | name = connection_spec.entity.name, 342 | direction = connection_spec.direction, 343 | position = coordinate 344 | }) 345 | local spec_table = (connection_spec.line_info.direction == "input") and inlet_line_spec or outlet_line_spec 346 | -- initialize inlet/outlet table if entry not exists 347 | if not spec_table[connection_spec.transport_line_y] then 348 | local connection_point_x = ( 349 | (direction_spec.linearIngredientDirection == defines.direction.east) == (connection_spec.line_info.direction == "input") 350 | ) and (crafter_width - 1) or 0 351 | spec_table[connection_spec.transport_line_y] = { 352 | position = Vector2D.new(connection_point_x, connection_spec.transport_line_y), 353 | entity = connection_spec.line_info.type == "item" and preferred_belt or game.entity_prototypes["pipe"], 354 | ingredients = ArrayList.mapToTable(connection_spec.line_info.crafting_items, function(x) 355 | return x.name, 0 356 | end) 357 | } 358 | end 359 | 360 | --- a inserter's speed is split by two items in the belt according to it's recipe items' amount ratios 361 | --- @type table item name to it's speed 362 | local crafting_item_ratios = {} 363 | do 364 | local crafting_item_recipe_nums = {} 365 | local total_amount = 0 366 | for _, crafting_item in ipairs(connection_spec.line_info.crafting_items) do 367 | local average_amount = average_amount_of(crafting_item) 368 | crafting_item_recipe_nums[crafting_item.name] = average_amount 369 | total_amount = total_amount + average_amount 370 | end 371 | for item_name, amount in pairs(crafting_item_recipe_nums) do 372 | crafting_item_ratios[item_name] = amount / total_amount 373 | end 374 | logging.log("crafting_item_ratios = " .. serpent.line(crafting_item_ratios)) 375 | end 376 | 377 | for _, crafting_item in ipairs(connection_spec.line_info.crafting_items) do 378 | spec_table[connection_spec.transport_line_y].ingredients[crafting_item.name] = spec_table[connection_spec.transport_line_y].ingredients[crafting_item.name] + PlayerInfo.inserter_items_speed(self.player_index, connection_spec.entity) * crafting_item_ratios[crafting_item.name] 379 | end 380 | end 381 | -- TODO also add inlet/outlet for fluid 382 | end 383 | section.inlets = ArrayList.new(inlet_line_spec) 384 | section.outlets = ArrayList.new(outlet_line_spec) 385 | end 386 | 387 | local max_speed_unit_repetition_num = 1 / 0 388 | local max_recipe_speed = 1 / 0 389 | --- @type ConnectionPoint[][] 390 | local all_connections = { section.inlets, section.outlets } 391 | for _, connections in ipairs(all_connections) do 392 | for _, connection_point in ipairs(connections) do 393 | local belt_lane_num = #ArrayList.new(connection_point.ingredients) 394 | for _, speed in pairs(connection_point.ingredients) do 395 | local max_belt_speed = preferred_belt.belt_speed * 480 / belt_lane_num 396 | logging.log("max belt speed " .. tostring(preferred_belt.belt_speed * 480)) 397 | local repetition = math.ceil(max_belt_speed / speed) 398 | if repetition < max_speed_unit_repetition_num then 399 | max_speed_unit_repetition_num = repetition 400 | end 401 | if speed < max_recipe_speed then 402 | max_recipe_speed = speed 403 | end 404 | end 405 | end 406 | end 407 | --logging.log("inlets = " .. serpent.line(section.inlets) .. ",\n outlets = " .. serpent.line(section.outlets).. "\n ---") 408 | return section, max_speed_unit_repetition_num, direction_spec, max_recipe_speed 409 | end 410 | 411 | --- @return BlueprintSection 412 | function AssemblerNode:generate_section() 413 | local unit_section, max_unit_per_row, _, unit_crafting_speed = self:generate_crafting_unit() 414 | local unit_needed = math.ceil(self.recipe_speed / unit_crafting_speed) 415 | 416 | local section = BlueprintSection.new() 417 | local unit_height = unit_section:height() 418 | local y_shift = 0 419 | while unit_needed > 0 do 420 | local unit_num_in_row = (unit_needed >= max_unit_per_row) and max_unit_per_row or unit_needed 421 | local new_unit_row = unit_section:copy():repeat_self(unit_num_in_row) 422 | new_unit_row:shift(0, y_shift) 423 | section:addSection(new_unit_row) 424 | 425 | y_shift = y_shift + unit_height 426 | unit_needed = unit_needed - unit_num_in_row 427 | end 428 | 429 | return section 430 | end 431 | 432 | return AssemblerNode -------------------------------------------------------------------------------- /blueprint_gen/blueprint_graph.lua: -------------------------------------------------------------------------------- 1 | require("prototype_info") 2 | local PlayerInfo = require("player_info") 3 | local assertNotNull = require("__MiscLib__/assert_not_null") 4 | --- @type Logger 5 | local logging = require("__MiscLib__/logging") 6 | --- @type ArrayList 7 | local ArrayList = require("__MiscLib__/array_list") 8 | --- @type Copier 9 | local Copier = require("__MiscLib__/copy") 10 | local shallow_copy = Copier.shallow_copy 11 | local BlueprintGeneratorUtil = require("blueprint_gen.util") 12 | local average_amount_of = BlueprintGeneratorUtil.average_amount_of 13 | --- @type AssemblerNode 14 | local AssemblerNode = require("blueprint_gen.assembler_node") 15 | 16 | --- @alias recipe_name string 17 | --- @alias ingredient_name string 18 | 19 | --- @class Entity 20 | --- @field entity_number number unique identifier of entity 21 | --- @field name string entity name 22 | --- @field position Vector2D 23 | --- @field direction any defines.direction.east/south/west/north 24 | 25 | --- @class ConnectionPoint 26 | --- @field ingredients table ingredients that the connection point is transporting to number of items transported per second 27 | --- @field position Vector2D 28 | --- @field connection_entity LuaEntityPrototype 29 | 30 | 31 | --- @class BlueprintGraph 32 | --- @field inputs table input ingredients 33 | --- @field outputs table output ingredients 34 | --- @field dict table all assembler nodes 35 | --- @field player_index player_index 36 | local BlueprintGraph = {} 37 | function BlueprintGraph.__index(t, k) 38 | return BlueprintGraph[k] or ArrayList[k] or t.dict[k] 39 | end 40 | 41 | --- @return BlueprintGraph 42 | function BlueprintGraph.new(player_index) 43 | local o = { player_index = player_index } 44 | o.inputs = ArrayList.new {} 45 | o.outputs = ArrayList.new {} 46 | o.dict = ArrayList.new {} 47 | setmetatable(o, BlueprintGraph) 48 | return o 49 | end 50 | 51 | --- @param output_specs OutputSpec[] 52 | function BlueprintGraph:generate_graph_by_outputs(output_specs) 53 | assertNotNull(self, output_specs) 54 | 55 | for _, requirement in ipairs(output_specs) do 56 | if requirement.ingredient and requirement.crafting_speed then 57 | self:__generate_assembler(requirement.ingredient, requirement.crafting_speed, true) 58 | end 59 | end 60 | end 61 | 62 | function BlueprintGraph:use_products_as_input(item_name) 63 | self.__index = BlueprintGraph.__index 64 | setmetatable(self, self) 65 | local nodes = self:__assemblers_whose_ingredients_have(item_name) 66 | if nodes:any(function(x) 67 | return self.outputs:has(x) 68 | end) then 69 | logging.log("ingredient can't be more advanced", logging.I) 70 | return 71 | end 72 | 73 | self.inputs[item_name] = nil 74 | for _, node in pairs(nodes) do 75 | for _, product in ipairs(node.recipe.products) do 76 | for _, target in pairs(self:__assemblers_whose_ingredients_have(product.name)) do 77 | self.inputs[product.name] = target 78 | end 79 | end 80 | end 81 | 82 | -- remove unnecessary input sources that are fully covered by other sources 83 | local others = shallow_copy(self.inputs) 84 | local to_remove = {} 85 | for input_name, input_node in pairs(self.inputs) do 86 | others[input_name] = nil 87 | if self:__ingredient_fully_used_by(input_name, ArrayList.fromKeys(others)) then 88 | to_remove[input_name] = input_node 89 | end 90 | others[input_name] = input_node 91 | end 92 | for input_name, _ in pairs(to_remove) do 93 | self.inputs[input_name] = nil 94 | end 95 | end 96 | 97 | function BlueprintGraph:use_ingredients_as_input(item_name) 98 | self.__index = BlueprintGraph.__index 99 | setmetatable(self, self) 100 | local viable = false 101 | for _, node in pairs(self:__assemblers_whose_products_have(item_name)) do 102 | for _, ingredient in pairs(node.recipe.ingredients) do 103 | self.inputs[ingredient.name] = node 104 | viable = true 105 | end 106 | end 107 | if viable then 108 | self.inputs[item_name] = nil 109 | end 110 | end 111 | 112 | --- insert a new blueprint item into player's inventory 113 | function BlueprintGraph:generate_blueprint() 114 | -- TODO use full blueprint rather then first output 115 | for _, output_node in pairs(self.outputs) do 116 | PlayerInfo.insert_blueprint(self.player_index, output_node:generate_section().entities) 117 | break 118 | end 119 | end 120 | 121 | function BlueprintGraph:__generate_assembler(recipe_name, crafting_speed, is_final) 122 | assert(self and recipe_name and crafting_speed and (is_final ~= nil)) 123 | local recipe = game.recipe_prototypes[recipe_name] 124 | if recipe then 125 | -- setup current node 126 | self.dict[recipe_name] = self.dict[recipe_name] or AssemblerNode.new { recipe = recipe, player_index = self.player_index } 127 | local node = self.dict[recipe_name] 128 | if is_final then 129 | for _, product in ipairs(node.products) do 130 | logging.log("output:" .. product.name) 131 | self.outputs[product.name] = node 132 | end 133 | end 134 | local new_speed 135 | for _, product in ipairs(node.recipe.products) do 136 | if product.name == recipe_name then 137 | new_speed = crafting_speed / average_amount_of(product) 138 | node.recipe_speed = node.recipe_speed + new_speed 139 | break 140 | end 141 | end 142 | if new_speed == nil then 143 | logging.log("speed setup for " .. recipe_name .. " failed", logging.E) 144 | new_speed = 1 145 | end 146 | -- setup children nodes 147 | for _, ingredient in ipairs(node.ingredients) do 148 | local child_recipe = game.recipe_prototypes[ingredient.name] 149 | if child_recipe then 150 | local child = self:__generate_assembler(child_recipe.name, ingredient.amount * new_speed, false) 151 | node.sources[ingredient.name] = child 152 | child.targets[recipe_name] = node 153 | else 154 | self.inputs[ingredient.name] = node 155 | end 156 | end 157 | return node 158 | else 159 | logging.log(recipe_name .. " doesn't have a recipe.", logging.E) 160 | end 161 | end 162 | 163 | --- @return AssemblerNode[] 164 | function BlueprintGraph:__assemblers_whose_products_have(item_name) 165 | if self[item_name] then 166 | return ArrayList.new { self[item_name] } 167 | end 168 | local out = ArrayList.new {} 169 | for _, node in pairs(self.dict) do 170 | for _, product in pairs(node.recipe.products) do 171 | if product.name == item_name then 172 | out[#out + 1] = node 173 | break 174 | end 175 | end 176 | end 177 | return out 178 | end 179 | 180 | --- @return AssemblerNode[] 181 | function BlueprintGraph:__assemblers_whose_ingredients_have(item_name) 182 | assert(self and item_name) 183 | local out = ArrayList.new {} 184 | if self[item_name] then 185 | for _, node in pairs(self[item_name].targets) do 186 | out[#out + 1] = node 187 | end 188 | return out 189 | end 190 | for _, node in pairs(self.dict) do 191 | for _, ingredient in ipairs(node.recipe.ingredients) do 192 | if ingredient.name == item_name then 193 | out[#out + 1] = node 194 | break 195 | end 196 | end 197 | end 198 | return out 199 | end 200 | 201 | function BlueprintGraph:__ingredient_fully_used_by(ingredient_name, item_list) 202 | if self.outputs[ingredient_name] then 203 | return false 204 | end 205 | if item_list:has(ingredient_name) then 206 | return true 207 | end 208 | local products = ArrayList.new {} 209 | for _, node in ipairs(self:__assemblers_whose_ingredients_have(ingredient_name)) do 210 | for _, p in ipairs(node.recipe.products) do 211 | products[#products + 1] = p.name 212 | end 213 | end 214 | return products:all(function(p) 215 | return self:__ingredient_fully_used_by(p, item_list) 216 | end) 217 | end 218 | 219 | return BlueprintGraph -------------------------------------------------------------------------------- /blueprint_gen/blueprint_section.lua: -------------------------------------------------------------------------------- 1 | local assertNotNull = require("__MiscLib__/assert_not_null") 2 | --- @type Logger 3 | local logging = require("__MiscLib__/logging") 4 | --- @type ArrayList 5 | local ArrayList = require("__MiscLib__/array_list") 6 | --- @type Copier 7 | local Copier = require("__MiscLib__/copy") 8 | --- @type Vector2D 9 | local Vector2D = require("__MiscLib__/vector2d") 10 | local deep_copy = Copier.deep_copy 11 | 12 | --- @class BlueprintSection 13 | --- @field entities Entity[] 14 | --- @field inlets ConnectionPoint[] 15 | --- @field outlets ConnectionPoint[] 16 | local BlueprintSection = {} 17 | BlueprintSection.__index = BlueprintSection 18 | 19 | --- @return BlueprintSection 20 | function BlueprintSection.new() 21 | --- @type BlueprintSection 22 | local o = { entities = ArrayList.new {}, inlets = ArrayList.new {}, outlets = ArrayList.new {} } 23 | setmetatable(o, BlueprintSection) 24 | return o 25 | end 26 | 27 | function BlueprintSection:copy(xoff, yoff) 28 | assert(self) 29 | xoff = xoff or 0 30 | yoff = yoff or 0 31 | --- @param old Entity 32 | local function shift_func(old) 33 | local new = deep_copy(old) 34 | new.position.x = new.position.x + xoff 35 | new.position.y = new.position.y + yoff 36 | return new 37 | end 38 | local new_section = BlueprintSection.new() 39 | new_section.entities = ArrayList.new(self.entities):map(shift_func) 40 | new_section.inlets = ArrayList.new(self.inlets):map(shift_func) 41 | new_section.outlets = ArrayList.new(self.outlets):map(shift_func) 42 | return new_section 43 | end 44 | 45 | --- @param entity Entity 46 | function BlueprintSection:add(entity) 47 | assertNotNull(self, entity) 48 | entity.entity_number = #self.entities + 1 49 | self.entities[entity.entity_number] = entity 50 | end 51 | 52 | --- @param section BlueprintSection 53 | function BlueprintSection:addSection(section) 54 | assertNotNull(self, section) 55 | 56 | for _, entity in ipairs(section.entities) do 57 | self:add(deep_copy(entity)) 58 | end 59 | for _, inlet in ipairs(section.inlets) do 60 | self.inlets:add(deep_copy(inlet)) 61 | end 62 | for _, outlet in ipairs(section.outlets) do 63 | self.inlets:add(deep_copy(outlet)) 64 | end 65 | end 66 | 67 | --- concatenate with another section, assuming that self's outlets and other's inlets are connected. 68 | --- If no offsets are provided, default to concatenate other to right side. 69 | --- @param other BlueprintSection 70 | --- @param xoff number optional, x-offset of the other section, default to width of self 71 | --- @return BlueprintSection new self 72 | function BlueprintSection:concat(other, xoff) 73 | assertNotNull(self, other) 74 | xoff = xoff or self:width() 75 | 76 | self.outlets = {} 77 | for _, entity in ipairs(other.entities) do 78 | local new_entity = deep_copy(entity) 79 | new_entity.position.x = new_entity.position.x + xoff 80 | self:add(new_entity) 81 | end 82 | 83 | local outlet_increase = other:width() 84 | for _, outlet in ipairs(self.outlets) do 85 | outlet.position.x = outlet.position.x + outlet_increase 86 | end 87 | 88 | return self 89 | end 90 | 91 | function BlueprintSection:width() 92 | local min, max 93 | for _, entity in ipairs(self.entities) do 94 | local test_min = entity.position.x + game.entity_prototypes[entity.name].selection_box.left_top.x 95 | if not min or test_min < min then 96 | min = test_min 97 | end 98 | local test_max = entity.position.x + game.entity_prototypes[entity.name].selection_box.right_bottom.x 99 | if not max or test_max > max then 100 | max = test_max 101 | end 102 | end 103 | local width = math.floor((max or 0) - (min or 0) + 0.5) 104 | return width 105 | end 106 | 107 | function BlueprintSection:height() 108 | local min, max 109 | for _, entity in ipairs(self.entities) do 110 | local test_min = entity.position.y + game.entity_prototypes[entity.name].selection_box.left_top.y 111 | if not min or test_min < min then 112 | min = test_min 113 | end 114 | local test_max = entity.position.y + game.entity_prototypes[entity.name].selection_box.right_bottom.y 115 | if not max or test_max > max then 116 | max = test_max 117 | end 118 | end 119 | local height = math.floor((max or 0) - (min or 0) + 0.5) 120 | return height 121 | end 122 | 123 | --- @return Vector2D, Vector2D left_top and right_bottom 124 | function BlueprintSection:boundingBox() 125 | local x_min, x_max, y_min, y_max 126 | for _, entity in ipairs(self.entities) do 127 | local x_test_min = entity.position.x + game.entity_prototypes[entity.name].selection_box.left_top.x 128 | if not x_min or x_test_min < x_min then 129 | x_min = math.floor(x_test_min + 0.5) 130 | end 131 | local test_max = entity.position.x + game.entity_prototypes[entity.name].selection_box.right_bottom.x 132 | if not x_max or test_max > x_max then 133 | x_max = math.floor(test_max + 0.5) 134 | end 135 | local y_test_min = entity.position.y + game.entity_prototypes[entity.name].selection_box.left_top.y 136 | if not y_min or y_test_min < y_min then 137 | y_min = math.floor(y_test_min + 0.5) 138 | end 139 | local y_test_max = entity.position.y + game.entity_prototypes[entity.name].selection_box.right_bottom.y 140 | if y_max or y_test_max > y_max then 141 | y_max = math.floor(y_test_max + 0.5) 142 | end 143 | end 144 | return Vector2D.new(x_min, y_min), Vector2D.new(x_max, y_max) 145 | end 146 | 147 | function BlueprintSection:shift(x_off, y_off) 148 | for _, entity in ipairs(self.entities) do 149 | entity.position.x = entity.position.x + x_off 150 | entity.position.y = entity.position.y + y_off 151 | end 152 | for _, inlet in ipairs(self.inlets) do 153 | inlet.position.x = inlet.position.x + x_off 154 | inlet.position.y = inlet.position.y + y_off 155 | end 156 | for _, outlet in ipairs(self.outlets) do 157 | outlet.position.x = outlet.position.x + x_off 158 | outlet.position.y = outlet.position.y + y_off 159 | end 160 | end 161 | 162 | --- clear overlapped units, last-in entity get saved 163 | function BlueprintSection:clear_overlap() 164 | local position_dict = {} 165 | for _, entity in ipairs(self.entities) do 166 | local pos = tostring(entity.position[1] or entity.position.x) .. "," .. tostring(entity.position[2] or entity.position.y) 167 | position_dict[pos] = entity 168 | end 169 | self.entities = {} 170 | for _, entity in pairs(position_dict) do 171 | self:add(entity) 172 | end 173 | end 174 | 175 | --- @param position Vector2D 176 | --- @return boolean 177 | function BlueprintSection:positionIsOccupied(position) 178 | -- TODO: use dictionary to avoid iterating all entity and achieve better performance 179 | for _, entity in pairs(self.entities) do 180 | if entity.position == position then 181 | return true 182 | end 183 | end 184 | return false 185 | end 186 | 187 | --- @param n_times number 188 | --- @return BlueprintSection 189 | function BlueprintSection:repeat_self(n_times) 190 | assertNotNull(self, n_times) 191 | 192 | local unit = self:copy() 193 | local unit_width = unit:width() 194 | logging.log(serpent.line(unit.entities, { maxlevel = 4 })) 195 | 196 | for i = 1, n_times - 1, 1 do 197 | self:concat(unit, unit_width * i, 0) 198 | end 199 | return self 200 | end 201 | 202 | -- rotate clockwise 90*n degrees 203 | function BlueprintSection:rotate(n) 204 | local rotate_matrices = { [0] = function(x, y) 205 | return { x = x, y = y } 206 | end, [1] = function(x, y) 207 | return { x = y, y = -x } 208 | end, [2] = function(x, y) 209 | return { x = -x, y = -y } 210 | end, [3] = function(x, y) 211 | return { x = -y, y = x } 212 | end } 213 | 214 | n = -n % 4 215 | local rotate_func = rotate_matrices[n] 216 | for _, entity in ipairs(self.entities) do 217 | local prototype = game.entity_prototypes[entity.name] 218 | -- entity with even number of width/height will have a small centering offset 219 | local x_offset = (math.ceil(prototype.selection_box.right_bottom.x - prototype.selection_box.left_top.x + 1) % 2) / 2 220 | local y_offset = (math.ceil(prototype.selection_box.right_bottom.y - prototype.selection_box.left_top.y + 1) % 2) / 2 221 | if entity.name == "stone-furnace" then 222 | logging.log("before position: " .. serpent.line(entity.position)) 223 | end 224 | entity.position = rotate_func(entity.position.x - x_offset, entity.position.y - y_offset) 225 | if entity.name == "stone-furnace" then 226 | logging.log("after position: " .. serpent.line(entity.position)) 227 | end 228 | entity.direction = ((entity.direction or 0) - 2 * n) % 8 229 | end 230 | end 231 | 232 | return BlueprintSection -------------------------------------------------------------------------------- /blueprint_gen/independent_repeating_entity.lua: -------------------------------------------------------------------------------- 1 | local assertNotNull = require("__MiscLib__/assert_not_null") 2 | --- @type Vector2D 3 | local Vector2D = require("__MiscLib__/vector2d") 4 | 5 | --- @class IndependentRepeatingEntity 6 | --- @field preferredStepSize number 7 | --- @field entityName string 8 | --- @field nextPosition Vector2D 9 | --- @field repeatingDirection defines.direction 10 | --- @field additionalEntitySpec table additional specifications for the entity, such as entity direction/beacon's module etc. 11 | local IndependentRepeatingEntity = {} 12 | IndependentRepeatingEntity.__index = IndependentRepeatingEntity 13 | 14 | --- @param o IndependentRepeatingEntity 15 | --- @return IndependentRepeatingEntity 16 | function IndependentRepeatingEntity:new(o) 17 | assertNotNull(o.entityName, o.nextPosition, o.preferredStepSize) 18 | o.repeatingDirection = o.repeatingDirection or defines.direction.east 19 | setmetatable(o, IndependentRepeatingEntity) 20 | return o 21 | end 22 | 23 | --- @param blueprintSection BlueprintSection 24 | --- @return boolean true if we can still place next entity 25 | function IndependentRepeatingEntity:placeNextEntity(blueprintSection) 26 | assertNotNull(self, blueprintSection) 27 | if not self.nextPosition then 28 | return 29 | end 30 | local newEntitySpec = { 31 | name = self.entity, 32 | position = self.nextPosition 33 | } 34 | if self.additionalEntitySpec then 35 | for k, v in pairs(self.additionalEntitySpec) do 36 | newEntitySpec[k] = v 37 | end 38 | end 39 | blueprintSection:add(newEntitySpec) 40 | local leftTop, rightBottom = blueprintSection:boundingBox() 41 | -- infer next position 42 | for tryStepSize = self.preferredStepSize, 1, -1 do 43 | local newNextPosition = Vector2D.fromDirection(self.repeatingDirection):scale(tryStepSize) 44 | if newNextPosition.x >= leftTop.x and newNextPosition.y >= leftTop.y and 45 | newNextPosition.x <= rightBottom.x and newNextPosition.y <= rightBottom.y then 46 | if not blueprintSection:positionIsOccupied(newNextPosition) then 47 | self.nextPosition = newNextPosition 48 | return true 49 | end 50 | end 51 | end 52 | self.nextPosition = nil 53 | return false 54 | end 55 | -------------------------------------------------------------------------------- /blueprint_gen/mixer_node.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by EmmyLua(https://github.com/EmmyLua) 3 | --- Created by seancheey. 4 | --- DateTime: 10/23/20 2:41 AM 5 | --- -------------------------------------------------------------------------------- /blueprint_gen/util.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by EmmyLua(https://github.com/EmmyLua) 3 | --- Created by seancheey. 4 | --- DateTime: 10/17/20 3:53 PM 5 | --- 6 | 7 | local BlueprintGeneratorUtil = {} 8 | 9 | --- @param crafting_item Product|Ingredient 10 | --- @return number average amount 11 | function BlueprintGeneratorUtil.average_amount_of(crafting_item) 12 | assert(crafting_item.amount or crafting_item.amount_max, serpent.line(crafting_item) .. "has no amount or amount_max field") 13 | return crafting_item.amount or ((crafting_item.amount_max + crafting_item.amount_min) / 2) 14 | end 15 | 16 | --- @param recipe LuaRecipePrototype 17 | --- @param crafter LuaEntityPrototype 18 | --- @param inserterIngredientsMapping table inserter's name to the ingredients it can grab 19 | --- @return table speed table of ingredient/product name to number of items crafted per sec 20 | function BlueprintGeneratorUtil.calculateRecipeCraftingSpeed(recipe, crafter, inserterIngredientsMapping) 21 | 22 | end 23 | 24 | return BlueprintGeneratorUtil -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------------------------- 2 | Version: 0.8.0 3 | Date: 2020.11.10 4 | Optimisations: 5 | - Fix bug that crash scenario edit mode 6 | - Massive migration of util library to MiscLib 7 | 8 | --------------------------------------------------------------------------------------------------- 9 | Version: 0.7.0 10 | Date: 2020.09.20 11 | Feature: 12 | - Add support to choose blueprint input direction and output direction 13 | Bugfixes: 14 | - Fix bug that makes full fluid crafting recipe crashes game 15 | - Fix bug that calculates width of a crafting unit wrongly and makes some repeating units strangely 16 | 17 | --------------------------------------------------------------------------------------------------- 18 | Version: 0.6.0 19 | Date: 2020.09.18 20 | Feature: 21 | - UI improvement for choosing crafting unit 22 | Bugfixes: 23 | - Fix bug that makes choosing blueprint whole factory raise error 24 | - Fix bug that makes double transport belt blueprint break 25 | - Fix bug that makes belt direction option not working after choosing recipe 26 | 27 | --------------------------------------------------------------------------------------------------- 28 | Version: 0.5.0 29 | Date: 2020.09.17 30 | Feature: 31 | - Now player can choose the repeat number of the crafting unit. The max transportation cap of belt is also calculated 32 | 33 | --------------------------------------------------------------------------------------------------- 34 | Version: 0.4.0 35 | Date: 2020.09.15 36 | Bugfixes: 37 | - Fixed fluid box connection ordering 38 | Features: 39 | - Added speed calculation 40 | 41 | --------------------------------------------------------------------------------------------------- 42 | Version: 0.3.0 43 | Date: 2020.09.14 44 | Features: 45 | - Added support for choosing belt direction 46 | 47 | --------------------------------------------------------------------------------------------------- 48 | Version: 0.2.0 49 | Date: 2020.09.13 50 | Features: 51 | - Now Blueprinter can use the right type and right amount of inserters for a crafting unit 52 | 53 | --------------------------------------------------------------------------------------------------- 54 | Version: 0.1.0 55 | Date: 2020.09.12 56 | Features: 57 | - Supports selecting crafting unit and get blueprint 58 | - Supports selecting preference for crafting machine and transport belt -------------------------------------------------------------------------------- /control.lua: -------------------------------------------------------------------------------- 1 | require("gui.root_names") 2 | require("gui.outputs_select_frame") 3 | require("gui.inputs_select_frame") 4 | 5 | local PlayerInfo = require("player_info") 6 | --- @type Logger 7 | local logging = require("__MiscLib__/logging") 8 | --- @type GuiLib 9 | local GuiLib = require("__MiscLib__/guilib") 10 | --- @type GuiRootChildrenNames 11 | local GuiRootNames = require("gui.root_names") 12 | local releaseMode = require("release_mode") 13 | 14 | --- @alias player_index number 15 | 16 | if releaseMode then 17 | logging.disableCategory(logging.D) 18 | logging.disableCategory(logging.I) 19 | logging.disableCategory(logging.V) 20 | logging.disableCategory(logging.W) 21 | logging.disableCategory("guilib") 22 | end 23 | 24 | GuiLib.listenToEvents { 25 | defines.events.on_gui_click, 26 | defines.events.on_gui_opened, 27 | defines.events.on_gui_elem_changed, 28 | defines.events.on_gui_selection_state_changed, 29 | defines.events.on_gui_text_changed, 30 | defines.events.on_gui_value_changed, 31 | } 32 | 33 | -- initialize global data as empty if they are nil 34 | -- Note that global data cannot be metatable 35 | local function init_all_global() 36 | --- @type table 37 | global.settings = global.settings or {} 38 | end 39 | 40 | local function init_player_mod(player_index) 41 | if not global.settings[player_index] then 42 | PlayerInfo.set_default_settings(player_index) 43 | end 44 | for _, childName in pairs(GuiRootNames) do 45 | GuiLib.removeGuiElementWithName(player_index, childName) 46 | end 47 | GuiLib.gui_root(player_index).add { 48 | type = "button", 49 | tooltip = "Click to open Blueprinter.", 50 | caption = "Blueprinter", 51 | name = GuiRootNames.main_button 52 | } 53 | 54 | end 55 | 56 | GuiLib.registerGuiHandlerForAllPlayers(GuiLib.rootName .. "|" .. GuiRootNames.main_button, { 57 | [defines.events.on_gui_click] = function(e) 58 | if not GuiLib.gui_root(e.player_index)[GuiRootNames.main_function_frame] then 59 | create_main_function_frame(e.player_index) 60 | else 61 | GuiLib.removeGuiElementWithName(e.player_index, GuiRootNames.main_function_frame) 62 | GuiLib.removeGuiElementWithName(e.player_index, GuiRootNames.inputs_select_frame) 63 | end 64 | end 65 | }) 66 | 67 | -- Only called when starting a new game / loading a game without this mod 68 | script.on_init(function() 69 | logging.log("initialize game") 70 | init_all_global() 71 | for _, player in pairs(game.players) do 72 | init_player_mod(player.index) 73 | end 74 | end) 75 | 76 | -- Besides when on_init, Called everytime the script is loaded 77 | script.on_load(function() 78 | end) 79 | 80 | script.on_event(defines.events.on_player_joined_game, function(e) 81 | logging.log("player joined game, initialize mod") 82 | init_player_mod(e.player_index) 83 | end) 84 | 85 | script.on_configuration_changed(function() 86 | logging.log("configuration changed, reset default settings") 87 | for player_index, _ in ipairs(global.settings) do 88 | PlayerInfo.set_default_settings(player_index) 89 | init_player_mod(player_index) 90 | end 91 | end) 92 | 93 | -- used for debugging purpose only 94 | if script.active_mods["gvv"] then 95 | require("__gvv__.gvv")() 96 | end -------------------------------------------------------------------------------- /data.lua: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seancheey/FactorioBlueprinter/9fd10a5fc8e28bce2d841034904d0919bc93b036/data.lua -------------------------------------------------------------------------------- /demo/crafting-unit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seancheey/FactorioBlueprinter/9fd10a5fc8e28bce2d841034904d0919bc93b036/demo/crafting-unit.gif -------------------------------------------------------------------------------- /gui/inputs_select_frame.lua: -------------------------------------------------------------------------------- 1 | --- @type BlueprintGraph 2 | local BlueprintGraph = require("blueprint_gen.blueprint_graph") 3 | local assertNotNull = require("__MiscLib__/assert_not_null") 4 | --- @type GuiLib 5 | local GuiLib = require("__MiscLib__/guilib") 6 | local gui_root = GuiLib.gui_root 7 | --- @type GuiRootChildrenNames 8 | local GuiRootNames = require("gui.root_names") 9 | 10 | --- @param gui_parent LuaGuiElement 11 | local function create_input_buttons(player_index, gui_parent, blueprint_graph) 12 | local function recreate_input_buttons() 13 | -- clear frame's all child element buttons 14 | for _, child in ipairs(gui_parent.children) do 15 | GuiLib.removeGuiElement(child) 16 | end 17 | create_input_buttons(player_index, gui_parent, blueprint_graph) 18 | end 19 | for ingredient_name, _ in pairs(blueprint_graph.inputs) do 20 | GuiLib.addGuiElementWithHandler(gui_parent, 21 | { 22 | type = "sprite-button", 23 | name = "left_button_" .. ingredient_name, 24 | sprite = "utility/left_arrow", 25 | tooltip = "use it's ingredient instead" 26 | }, 27 | { 28 | [defines.events.on_gui_click] = function() 29 | BlueprintGraph.use_ingredients_as_input(blueprint_graph, ingredient_name) 30 | recreate_input_buttons() 31 | end 32 | } 33 | ) 34 | gui_parent.add { type = "sprite", name = "sprite_" .. ingredient_name, sprite = sprite_of(ingredient_name) } 35 | GuiLib.addGuiElementWithHandler(gui_parent, 36 | { type = "sprite-button", 37 | name = "right_button_" .. ingredient_name, 38 | sprite = "utility/right_arrow", 39 | tooltip = "use it's targets instead" 40 | }, { 41 | [defines.events.on_gui_click] = function() 42 | BlueprintGraph.use_products_as_input(blueprint_graph, ingredient_name) 43 | recreate_input_buttons() 44 | end 45 | } 46 | ) 47 | end 48 | end 49 | 50 | function create_inputs_select_frame(player_index, output_specs) 51 | assertNotNull(player_index, output_specs) 52 | local blueprint_graph = BlueprintGraph.new(player_index) 53 | blueprint_graph:generate_graph_by_outputs(output_specs) 54 | local frame = gui_root(player_index).add { name = GuiRootNames.inputs_select_frame, type = "frame", caption = "Input Source Select", direction = "vertical" } 55 | local hori_flow = frame.add { type = "table", name = "hori_flow", column_count = 2 } 56 | local input_select_frame = hori_flow.add { type = "frame", name = "input_select_frame", caption = "Input Items Select" } 57 | local inputs_flow = input_select_frame.add { type = "flow", name = "inputs_flow", direction = "vertical" } 58 | local inputs_table = inputs_flow.add { type = "table", name = "inputs_table", column_count = 3 } 59 | create_input_buttons(player_index, inputs_table, blueprint_graph) 60 | inputs_flow.style.vertically_stretchable = true 61 | local outputs_frame = hori_flow.add { type = "frame", name = "outputs_frame", caption = "Final Products" } 62 | local outputs_view_flow = outputs_frame.add { type = "flow", name = "outputs_view_flow", direction = "vertical", caption = "target outputs" } 63 | for output_name, _ in pairs(blueprint_graph.outputs) do 64 | outputs_view_flow.add { type = "sprite", sprite = sprite_of(output_name) } 65 | end 66 | outputs_view_flow.style.vertically_stretchable = true 67 | GuiLib.addGuiElementWithHandler(frame, { type = "button", name = "confirm_button", caption = "confirm" }, 68 | { 69 | [defines.events.on_gui_click] = function(e) 70 | -- TODO generate a real blueprint 71 | game.players[e.player_index].print("This function is currently not yet completed, it's under active development.") 72 | end 73 | }) 74 | end 75 | 76 | -------------------------------------------------------------------------------- /gui/outputs_select_frame.lua: -------------------------------------------------------------------------------- 1 | local PlayerInfo = require("player_info") 2 | --- @type AssemblerNode 3 | local AssemblerNode = require("blueprint_gen.assembler_node") 4 | local assertNotNull = require("__MiscLib__/assert_not_null") 5 | --- @type ArrayList 6 | local ArrayList = require("__MiscLib__/array_list") 7 | --- @type Vector2D 8 | local Vector2D = require("__MiscLib__/vector2d") 9 | --- @type Pointer 10 | local Pointer = require("__MiscLib__/pointer") 11 | --- @type GuiLib 12 | local GuiLib = require("__MiscLib__/guilib") 13 | local elem_of = GuiLib.elem_of 14 | local path_of = GuiLib.path_of 15 | local gui_root = GuiLib.gui_root 16 | --- @type GuiRootChildrenNames 17 | local GuiRootNames = require("gui.root_names") 18 | 19 | local unit_values = { ["item/s"] = 1, ["item/min"] = 60 } 20 | local output_units = { "item/s", "item/min" } 21 | 22 | --- add a new output item selection box for player with *player_index* at *parent* gui element 23 | --- @param parent LuaGuiElement 24 | local function add_output_item_selection_box(player_index, parent, outputs_specifications) 25 | assertNotNull(player_index, parent, outputs_specifications) 26 | 27 | local row_num = #outputs_specifications + 1 28 | outputs_specifications[row_num] = { crafting_speed = 1, unit = output_units[1] } 29 | local choose_button = parent.add { name = "bp_output_choose_button" .. tostring(row_num), type = "choose-elem-button", elem_type = "recipe" } 30 | GuiLib.registerGuiHandler(choose_button, defines.events.on_gui_elem_changed, 31 | function(e) 32 | outputs_specifications[row_num].ingredient = e.element.elem_value 33 | -- expand table if full 34 | if outputs_specifications:all(function(x) 35 | return x.ingredient 36 | end) then 37 | add_output_item_selection_box(e.player_index, parent, outputs_specifications) 38 | end 39 | -- any change to output makes input frame disappear 40 | GuiLib.removeGuiElementWithName(e.player_index, GuiRootNames.inputs_select_frame) 41 | end 42 | ) 43 | local num_field = parent.add { name = "bp_output_numfield" .. tostring(row_num), type = "textfield", text = "1", numeric = true, allow_negative = false } 44 | num_field.style.maximal_width = 50 45 | GuiLib.registerGuiHandler(num_field, defines.events.on_gui_text_changed, 46 | function(e) 47 | local item_choice = outputs_specifications[row_num] 48 | item_choice.crafting_speed = (e.element.text ~= "" and tonumber(e.element.text) or 0) / unit_values[item_choice.unit] 49 | end 50 | ) 51 | local unit_box = parent.add { name = "bp_output_dropdown" .. tostring(row_num), type = "drop-down", items = output_units, selected_index = 1 } 52 | GuiLib.registerGuiHandler(unit_box, defines.events.on_gui_selection_state_changed, 53 | function(e) 54 | local item_choice = outputs_specifications[row_num] 55 | item_choice.unit = output_units[e.element.selected_index] 56 | -- recalculate item crafting speed according to new unit 57 | local new_num = item_choice.crafting_speed * unit_values[item_choice.unit] 58 | num_field.text = tostring(new_num) 59 | end 60 | ) 61 | end 62 | 63 | local function create_settings_tab(player_index, tab_pane) 64 | PlayerInfo.update_crafting_machine_priorities(player_index) 65 | 66 | local setting_tab = tab_pane.add { type = "tab", name = "setting_tab", caption = "settings" } 67 | local vertical_flow = tab_pane.add { type = "flow", name = "vertical_flow", direction = "vertical" } 68 | vertical_flow.add { type = "label", name = "priority_label", caption = "Factory Priorities", tooltip = "Factories in front will be picked to generate blueprints first" } 69 | local priority_table = vertical_flow.add { type = "table", name = "priority_table", column_count = 8 } 70 | for i, factory in ipairs(global.settings[player_index].factory_priority) do 71 | priority_table.add { type = "sprite", name = "sprite_" .. tostring(i), sprite = sprite_of(factory.name) } 72 | local factory_button = priority_table.add { type = "sprite-button", name = "sort_up_" .. tostring(i), sprite = "utility/left_arrow" } 73 | GuiLib.registerGuiHandler(factory_button, defines.events.on_gui_click, 74 | function(e) 75 | -- eliminate first button 76 | if i <= 1 then 77 | return 78 | end 79 | -- swap global setting 80 | local priority = global.settings[e.player_index].factory_priority 81 | local this = priority[i] 82 | local swapped = priority[i - 1] 83 | priority[i] = swapped 84 | priority[i - 1] = this 85 | -- swap gui 86 | elem_of(path_of(priority_table) .. "|sprite_" .. tostring(i), e.player_index).sprite = sprite_of(swapped.name) 87 | elem_of(path_of(priority_table) .. "|sprite_" .. tostring(i - 1), e.player_index).sprite = sprite_of(this.name) 88 | end 89 | ) 90 | end 91 | 92 | vertical_flow.add { type = "label", name = "belt_label", caption = "Belt Preference" } 93 | local belt_choose_table = vertical_flow.add { type = "table", name = "choose_table", column_count = 2 * #ALL_BELTS } 94 | for i, belt_name in ipairs(ALL_BELTS) do 95 | belt_choose_table.add { type = "sprite", sprite = sprite_of(belt_name) } 96 | local choose_button = belt_choose_table.add { name = "bp_setting_choose_belt_button" .. tostring(i), type = "radiobutton", state = global.settings[player_index].belt == i } 97 | GuiLib.registerGuiHandler(choose_button, defines.events.on_gui_click, 98 | function(e) 99 | global.settings[e.player_index].belt = i 100 | for other = 1, 3 do 101 | if other ~= i then 102 | elem_of(path_of(belt_choose_table) .. "|bp_setting_choose_belt_button" .. tostring(other), e.player_index).state = false 103 | end 104 | end 105 | end 106 | ) 107 | end 108 | tab_pane.add_tab(setting_tab, vertical_flow) 109 | end 110 | 111 | local function create_outputs_select_tab(player_index, tab_pane) 112 | --- @type OutputSpec[] player's specified blueprint outputs 113 | local output_specifications = ArrayList.new {} 114 | 115 | local output_tab = tab_pane.add { type = "tab", name = "outputs_tab", caption = "whole factory" } 116 | local output_flow = tab_pane.add { type = "flow", name = "output_flow", direction = "vertical" } 117 | local output_table = output_flow.add { type = "table", name = "output_table", caption = "select output items", column_count = 3 } 118 | add_output_item_selection_box(player_index, output_table, output_specifications) 119 | local confirm_button = output_flow.add { name = "bp_output_confirm_button", type = "button", caption = "confirm" } 120 | GuiLib.registerGuiHandler(confirm_button, defines.events.on_gui_click, 121 | function(e) 122 | GuiLib.removeGuiElementWithName(e.player_index, GuiRootNames.inputs_select_frame) 123 | create_inputs_select_frame(e.player_index, output_specifications) 124 | end 125 | ) 126 | tab_pane.add_tab(output_tab, output_flow) 127 | end 128 | 129 | local CraftingUnitSelectTab = {} 130 | 131 | --- @param recipe_max_repetition number 132 | --- @param gui_parent LuaGuiElement 133 | --- @return LuaGuiElement repeat number selector 134 | function CraftingUnitSelectTab.create_repeat_num_selector(player_index, gui_parent, repetition_num_pointer, recipe_max_repetition) 135 | assertNotNull(player_index, gui_parent, repetition_num_pointer, recipe_max_repetition) 136 | 137 | local repeat_num_frame = gui_parent.add { name = "repeat_num_frame", type = "frame", direction = "horizontal", caption = "Choose unit number" } 138 | repeat_num_frame.add { name = "repeat_num_label", type = "label", caption = "repeat unit" } 139 | local repeat_num_field = repeat_num_frame.add { name = "repeat_num_field", type = "textfield", numeric = true, allow_decimal = false } 140 | repeat_num_frame.add { name = "repeat_times_label", type = "label", caption = "times" } 141 | repeat_num_field.style.maximal_width = 30 142 | repeat_num_field.text = tostring(Pointer.get(repetition_num_pointer)) 143 | local repeat_num_slider = repeat_num_frame.add { name = "repeat_num_slider", type = "slider", minimum_value = 1, maximum_value = 10, value = 1, value_step = 1, discrete_slider = true, discrete_values = true } 144 | repeat_num_slider.style.maximal_width = 80 145 | repeat_num_slider.set_slider_minimum_maximum(1, (recipe_max_repetition > 100) and 100 or recipe_max_repetition) 146 | repeat_num_frame.add { name = "max_repetition_lavel", type = "label", caption = "(capacity: " .. tostring(recipe_max_repetition) .. ")" } 147 | GuiLib.registerGuiHandler(repeat_num_field, defines.events.on_gui_text_changed, function(e) 148 | local new_repeat = tonumber(e.element.text) 149 | if new_repeat then 150 | new_repeat = new_repeat > recipe_max_repetition and recipe_max_repetition or new_repeat 151 | if new_repeat ~= Pointer.get(repetition_num_pointer) then 152 | Pointer.set(repetition_num_pointer, new_repeat) 153 | repeat_num_slider.slider_value = new_repeat 154 | end 155 | if new_repeat ~= tonumber(e.element.text) then 156 | repeat_num_field.text = tostring(new_repeat) 157 | end 158 | end 159 | end) 160 | GuiLib.registerGuiHandler(repeat_num_slider, defines.events.on_gui_value_changed, function(e) 161 | local new_repeat = math.ceil(e.element.slider_value) 162 | if new_repeat ~= Pointer.get(repetition_num_pointer) then 163 | Pointer.set(repetition_num_pointer, new_repeat) 164 | repeat_num_field.text = tostring(new_repeat) 165 | end 166 | end) 167 | return repeat_num_frame 168 | end 169 | 170 | --- @param gui_parent LuaGuiElement 171 | --- @param blueprint_pointer Pointer|BlueprintSection[] 172 | function CraftingUnitSelectTab.create_confirm_button(player_index, gui_parent, recipe, repetition_pointer) 173 | assertNotNull(player_index, gui_parent, recipe, repetition_pointer) 174 | 175 | local confirm_button = gui_parent.add { name = "confirm_button", type = "button", caption = "Confirm" } 176 | GuiLib.registerGuiHandler(confirm_button, defines.events.on_gui_click, function(e) 177 | local new_unit, _, direction_spec = AssemblerNode.new({ recipe = recipe, player_index = e.player_index }):generate_crafting_unit() 178 | local blueprint_section = new_unit:repeat_self(Pointer.get(repetition_pointer)) 179 | blueprint_section:rotate(direction_spec.blueprintRotation) 180 | local blueprint = PlayerInfo.insert_blueprint(e.player_index, blueprint_section.entities) 181 | if blueprint then 182 | blueprint.label = recipe.name .. " crafting unit" 183 | game.players[e.player_index].print("blueprint \"" .. blueprint.label .. "\" created.") 184 | end 185 | GuiLib.removeGuiElementWithName(e.player_index, GuiRootNames.main_function_frame) 186 | end) 187 | return confirm_button 188 | end 189 | 190 | local direction_table = { [0] = "hint_arrow_up", [2] = "hint_arrow_right", [4] = "hint_arrow_down", [6] = "hint_arrow_left" } 191 | local function direction_sprite(direction) 192 | return direction_table[direction] and ("utility/" .. direction_table[direction]) or nil 193 | end 194 | 195 | --- @param gui_parent LuaGuiElement 196 | --- @return LuaGuiElement 197 | function CraftingUnitSelectTab.create_direction_select_frame(player_index, gui_parent, crafting_machine_name) 198 | assertNotNull(player_index, gui_parent, crafting_machine_name) 199 | 200 | local direction_frame = gui_parent.add { name = "direction_select_frame", type = "frame", direction = "vertical", caption = "Choose flow direction" } 201 | do 202 | direction_frame.add { name = "belt_direction_label", type = "label", caption = "Change input direction by clicking arrows in edges" } 203 | direction_frame.add { name = "output_direction_label", type = "label", caption = "Change output direction by clicking arrows in corners" } 204 | local choose_direction_tables_flow = direction_frame.add { name = "choose_direction_tables_flow", type = "flow", direction = "horizontal" } 205 | do 206 | --- @type table 207 | local preview_sprites = {} 208 | --- @type table 209 | local direction_buttons = {} 210 | 211 | --- update the direction preference for the player 212 | --- @param pressed_button_pos defines.direction position of the button that's pressed 213 | local function update_direction_preference(pressed_button_pos) 214 | local direction_spec = PlayerInfo.direction_settings(player_index) 215 | if pressed_button_pos % 2 == 0 then 216 | direction_spec.ingredientDirection = Vector2D.fromDirection(pressed_button_pos):reverse():toDirection() 217 | -- if pressed button is input direction button, update the position and direction of outputs 218 | local direction_shift = (pressed_button_pos % 4 == 0) and -1 or 1 219 | for output_button_pos = 1, 7, 2 do 220 | local new_output_direction = (output_button_pos + direction_shift) % 8 221 | direction_buttons[output_button_pos].sprite = direction_sprite(new_output_direction) 222 | if preview_sprites[output_button_pos].sprite ~= "" then 223 | preview_sprites[output_button_pos].sprite = direction_buttons[output_button_pos].sprite 224 | direction_spec.productDirection = new_output_direction 225 | direction_spec.productPosition = (new_output_direction - 2 * direction_shift) % 8 226 | end 227 | direction_shift = direction_shift * -1 228 | end 229 | else 230 | local input_button_pos 231 | for i = 0, 6, 2 do 232 | if preview_sprites[i].sprite ~= "" then 233 | input_button_pos = i 234 | break 235 | end 236 | end 237 | local input_direction_vector = Vector2D.fromDirection(input_button_pos):reverse() 238 | local output_button_pos_vector = Vector2D.fromDirection(pressed_button_pos) 239 | direction_spec.productDirection = Vector2D.new( 240 | input_direction_vector.x == 0 and 0 or output_button_pos_vector.x, 241 | input_direction_vector.y == 0 and 0 or output_button_pos_vector.y 242 | ) :toDirection() 243 | direction_spec.productPosition = Vector2D.new( 244 | input_direction_vector.x == 0 and output_button_pos_vector.x or 0, 245 | input_direction_vector.y == 0 and output_button_pos_vector.y or 0 246 | ) :toDirection() 247 | end 248 | preview_sprites[pressed_button_pos].sprite = direction_buttons[pressed_button_pos].sprite 249 | for other_same_type_button_offset = 2, 6, 2 do 250 | preview_sprites[(pressed_button_pos + other_same_type_button_offset) % 8].sprite = nil 251 | end 252 | end 253 | 254 | local belt_direction_table = choose_direction_tables_flow.add { name = "belt_direction_table", type = "table", column_count = 3 } 255 | belt_direction_table.style.left_margin = 20 256 | for _, direction in ipairs({ 7, 0, 1, 6, -1, 2, 5, 4, 3 }) do 257 | if direction >= 0 then 258 | -- 4 inputs direction button should reverse its arrow direction, other 4 outputs direction button should stay the same 259 | local arrow_direction = (direction % 2 == 0) and ((direction + 4) % 8) or direction 260 | direction_buttons[direction] = belt_direction_table.add { type = "sprite-button", name = "direction_button_" .. tostring(direction), sprite = direction_sprite(arrow_direction) } 261 | GuiLib.registerGuiHandler(direction_buttons[direction], defines.events.on_gui_click, function() 262 | -- create a mapping from (4 input direction + 4 output direction) to (inputs belt left/right + outputs belt left/right + 4 rotation) 263 | update_direction_preference(direction) 264 | end) 265 | else 266 | -- center of the table is a crafting machine icon 267 | belt_direction_table.add { type = "sprite-button", name = "crafting_machine_sprite", sprite = sprite_of(crafting_machine_name), ignored_by_interaction = true } 268 | end 269 | end 270 | local preview_flow = choose_direction_tables_flow.add { name = "preview_frame", type = "table", direction = "vertical", column_count = 1, draw_horizontal_line_after_headers = true } 271 | preview_flow.style.left_margin = 20 272 | do 273 | preview_flow.add { name = "preview_label", type = "label", caption = "Preview" } 274 | local preview_table = preview_flow.add { name = "preview_table", type = "table", column_count = 3 } 275 | local sprite_size = 28 276 | for _, direction in ipairs({ 7, 0, 1, 6, -1, 2, 5, 4, 3 }) do 277 | if direction >= 0 then 278 | preview_sprites[direction] = preview_table.add { type = "sprite", name = "direction_" .. tostring(direction) } 279 | preview_sprites[direction].style.minimal_height = sprite_size 280 | preview_sprites[direction].style.minimal_width = sprite_size 281 | preview_sprites[direction].style.stretch_image_to_widget_size = true 282 | else 283 | -- center of the table is a crafting machine icon 284 | local crafting_machine_sprite = preview_table.add { type = "sprite", name = "crafting_machine_sprite", sprite = sprite_of(crafting_machine_name), ignored_by_interaction = true } 285 | crafting_machine_sprite.style.minimal_height = sprite_size 286 | crafting_machine_sprite.style.minimal_width = sprite_size 287 | crafting_machine_sprite.style.stretch_image_to_widget_size = true 288 | end 289 | end 290 | end 291 | 292 | -- Preset default direction preference 293 | update_direction_preference(defines.direction.west) 294 | update_direction_preference(defines.direction.northeast) 295 | end 296 | end 297 | return direction_frame 298 | end 299 | 300 | --- @param tab_pane LuaGuiElement 301 | local function create_crafting_unit_select_tab(player_index, tab_pane) 302 | --- @type BlueprintSection[] 303 | 304 | local crafting_unit_tab = tab_pane.add { type = "tab", name = "crafting_unit_tab", caption = "crafting unit" } 305 | local crafting_unit_flow = tab_pane.add { type = "flow", name = "crafting_unit_flow", direction = "vertical" } 306 | do 307 | crafting_unit_flow.add { name = "recipe_select_label", type = "label", caption = "Select your new crafting unit's recipe:" } 308 | local choose_button = crafting_unit_flow.add { name = "recipe_choose_button", type = "choose-elem-button", elem_type = "recipe", elem_filters = { 309 | enabled = true 310 | } } 311 | 312 | local direction_frame, repeat_num_selector, confirm_button 313 | GuiLib.registerGuiHandler(choose_button, defines.events.on_gui_elem_changed, function(e) 314 | GuiLib.removeGuiElement(direction_frame) 315 | direction_frame = nil 316 | GuiLib.removeGuiElement(repeat_num_selector) 317 | repeat_num_selector = nil 318 | GuiLib.removeGuiElement(confirm_button) 319 | confirm_button = nil 320 | if e.element.elem_value then 321 | local recipe = game.recipe_prototypes[e.element.elem_value] 322 | local blueprint, max_repetition_num = AssemblerNode.new({ recipe = recipe, player_index = e.player_index }):generate_crafting_unit() 323 | if blueprint and max_repetition_num then 324 | direction_frame = CraftingUnitSelectTab.create_direction_select_frame(player_index, crafting_unit_flow, PlayerInfo.get_crafting_machine_prototype(player_index, recipe).name) 325 | local repetition_pointer = Pointer.new(1) 326 | if max_repetition_num > 1 then 327 | repeat_num_selector = CraftingUnitSelectTab.create_repeat_num_selector(player_index, crafting_unit_flow, repetition_pointer, max_repetition_num) 328 | end 329 | confirm_button = CraftingUnitSelectTab.create_confirm_button(player_index, crafting_unit_flow, recipe, repetition_pointer) 330 | end 331 | end 332 | end) 333 | 334 | end 335 | tab_pane.add_tab(crafting_unit_tab, crafting_unit_flow) 336 | end 337 | 338 | function create_main_function_frame(player_index) 339 | assertNotNull(player_index) 340 | 341 | local frame = gui_root(player_index).add { type = "frame", name = GuiRootNames.main_function_frame, caption = "Blueprinter" } 342 | local tab_pane = frame.add { type = "tabbed-pane", name = "outputs_tab_pane", caption = "outputs", direction = "vertical" } 343 | 344 | create_crafting_unit_select_tab(player_index, tab_pane) 345 | create_outputs_select_tab(player_index, tab_pane) 346 | create_settings_tab(player_index, tab_pane) 347 | tab_pane.selected_tab_index = 1 348 | end 349 | -------------------------------------------------------------------------------- /gui/root_names.lua: -------------------------------------------------------------------------------- 1 | --- @class OutputSpec one blueprint output specification 2 | --- @field crafting_speed number speed 3 | --- @field unit number crafting speed unit multiplier, with item/s as 1 4 | --- @field ingredient string recipe name of specification 5 | 6 | --- @class GuiRootChildrenNames 7 | --- @type GuiRootChildrenNames 8 | local GuiRootChildrenNames = {} 9 | 10 | GuiRootChildrenNames.main_function_frame = "bp-outputs-frame" 11 | GuiRootChildrenNames.inputs_select_frame = "bp-inputs-frame" 12 | GuiRootChildrenNames.main_button = "bp-main-button" 13 | 14 | return GuiRootChildrenNames -------------------------------------------------------------------------------- /info.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blueprinter", 3 | "version": "0.8.1", 4 | "title": "Blueprinter", 5 | "author": "Qiyi Shan", 6 | "contact": "adls371@outlook.com", 7 | "homepage": "https://github.com/Seancheey/FactorioBlueprinter", 8 | "factorio_version": "1.1", 9 | "dependencies": ["base", "MiscLib>=0.8.5"], 10 | "description": "This mod gives you a new menu for creating various handy crafting blueprints. you can:\n 1. create a single recipe crafting unit blueprint\n 2. (developing) create a massive whole factory blueprints by specifying inputs, outputs and production rates.\nThis mod is currently under active development, please check for updates." 11 | } 12 | -------------------------------------------------------------------------------- /player_info.lua: -------------------------------------------------------------------------------- 1 | local PlayerInfo = {} 2 | 3 | local assertNotNull = require("__MiscLib__/assert_not_null") 4 | --- @type ArrayList 5 | local ArrayList = require("__MiscLib__/array_list") 6 | PlayerInfo.__index = PlayerInfo 7 | --- @type Logger 8 | local logging = require("__MiscLib__/logging") 9 | 10 | --- @class PlayerSetting 11 | --- @field factory_priority LuaEntityPrototype[] factory prototype 12 | --- @field belt number 13 | --- @field direction_spec BlueprintDirectionSpec 14 | 15 | --- @class BlueprintDirectionSpec 16 | --- @field ingredientDirection defines.direction player UI visible transformation 17 | --- @field productPosition defines.direction player UI visible transformation 18 | --- @field productDirection defines.direction player UI visible transformation 19 | 20 | --- @return LuaEntityPrototype[] 21 | function PlayerInfo.unlocked_crafting_machines(player_index) 22 | local all_factory_list = ArrayList.new() 23 | for _, entity in pairs(game.get_filtered_entity_prototypes({ 24 | { filter = "crafting-machine" }, 25 | { filter = "hidden", invert = true, mode = "and" }, 26 | { filter = "blueprintable", mode = "and" } })) do 27 | all_factory_list[#all_factory_list + 1] = entity 28 | end 29 | local unlocked_recipes = PlayerInfo.unlocked_recipes(player_index) 30 | 31 | local unlocked_factories = {} 32 | for _, factory in ipairs(all_factory_list) do 33 | for _, recipe in pairs(unlocked_recipes) do 34 | if ArrayList.has(recipe.products, factory, function(a, b) 35 | return a.name == b.name 36 | end) then 37 | unlocked_factories[#unlocked_factories + 1] = factory 38 | break 39 | end 40 | end 41 | end 42 | return unlocked_factories 43 | end 44 | 45 | --- @return table inserters table, keyed by insert arm length, list is ordered by inserter speed, ascending, this list doesn't contain any filter/burner inserter since we are not using them anyway 46 | function PlayerInfo.unlocked_inserters(player_index) 47 | --- @type LuaEntityPrototype[] 48 | local inserter_list = ArrayList.new(PlayerInfo.unlocked_recipes(player_index)):filter(function(recipe) 49 | return game.entity_prototypes[recipe.name] and game.entity_prototypes[recipe.name].inserter_rotation_speed ~= nil and game.entity_prototypes[recipe.name].filter_count == 0 and recipe.name ~= "burner-inserter" 50 | end) :map(function(recipe) 51 | return game.entity_prototypes[recipe.name] 52 | end) 53 | local sorted_lists = ArrayList.new() 54 | for _, inserter in ipairs(inserter_list) do 55 | local pickup_location = inserter.inserter_pickup_position 56 | logging.log(serpent.line(pickup_location)) 57 | local inserter_arm_length = math.max(math.floor(math.abs(pickup_location[1])), math.floor(math.abs(pickup_location[2]))) 58 | if not sorted_lists[inserter_arm_length] then 59 | sorted_lists[inserter_arm_length] = ArrayList.new() 60 | end 61 | sorted_lists[inserter_arm_length]:insert_by_order(inserter, function(a, b) 62 | return a.inserter_rotation_speed < b.inserter_rotation_speed 63 | end) 64 | end 65 | --logging.log(serpent.block(sorted_lists:map(function(list) 66 | -- return list:map(function(inserter) 67 | -- return { name = inserter.name, filter_count = inserter.filter_count, inserter_rotation_speed = inserter.inserter_rotation_speed} 68 | -- end) 69 | --end))) 70 | return sorted_lists 71 | end 72 | 73 | --- @return LuaRecipePrototype[] 74 | function PlayerInfo.unlocked_recipes(player_index) 75 | assertNotNull(player_index) 76 | 77 | local all_recipes = game.get_player(player_index).force.recipes 78 | local unlocked_recipes = ArrayList.filter(all_recipes, 79 | function(recipe) 80 | return not recipe.hidden and recipe.enabled 81 | end) 82 | return unlocked_recipes 83 | end 84 | 85 | --- @return LuaEntityPrototype[] 86 | function PlayerInfo.crafting_machine_priorities(player_index) 87 | return global.settings[player_index].factory_priority 88 | end 89 | 90 | --- get a crafting machine prototype that user preferred 91 | --- @return LuaEntityPrototype crafting machine prototype 92 | function PlayerInfo.get_crafting_machine_prototype(player_index, recipe) 93 | -- get all crafting machines 94 | local filter = { filter = "crafting-machine" } 95 | local crafting_machines = game.get_filtered_entity_prototypes({ filter }) 96 | -- get recipe category 97 | local recipe_category = recipe.category 98 | -- match category 99 | local matching_prototypes = {} 100 | for _, prototype in pairs(crafting_machines) do 101 | if prototype.crafting_categories[recipe_category] ~= nil then 102 | matching_prototypes[#matching_prototypes + 1] = prototype 103 | end 104 | end 105 | 106 | -- select first preferred 107 | for _, crafting_machine in ipairs(PlayerInfo.crafting_machine_priorities(player_index)) do 108 | for _, matching_prototype in ipairs(matching_prototypes) do 109 | if crafting_machine.name == matching_prototype.name then 110 | return get_entity_prototype(matching_prototype.name) 111 | end 112 | end 113 | end 114 | -- if there is no player preference, select first available 115 | logging.log("no player preference matches recipe prototype, the recipe is probably uncraftable for now.", logging.D) 116 | return get_entity_prototype(matching_prototypes[1].name) 117 | end 118 | 119 | --- @return LuaEntityPrototype 120 | function PlayerInfo.get_preferred_belt(player_index) 121 | return game.entity_prototypes[ALL_BELTS[global.settings[player_index].belt]] 122 | end 123 | 124 | --- @return LuaTechnology[]|ArrayList 125 | function PlayerInfo.researched_technologies(player_index) 126 | local all_technologies = game.players[player_index].force.technologies 127 | local researched_technologies = ArrayList.new() 128 | for _, technology in pairs(all_technologies) do 129 | if technology.researched then 130 | researched_technologies:add(technology) 131 | end 132 | end 133 | return researched_technologies 134 | end 135 | 136 | --- @param inserter_prototype LuaEntityPrototype 137 | --- @return number stack size 138 | function PlayerInfo.inserter_stack_size(player_index, inserter_prototype) 139 | assertNotNull(player_index, inserter_prototype) 140 | --- @type LuaForce 141 | local force = game.players[player_index].force 142 | return inserter_prototype.stack and (force.stack_inserter_capacity_bonus + 2) or (force.inserter_stack_size_bonus + 1) 143 | end 144 | 145 | --- @param inserter_prototype LuaEntityPrototype 146 | --- @return number number of items that the inserter can transfer per second 147 | function PlayerInfo.inserter_items_speed(player_index, inserter_prototype) 148 | assertNotNull(player_index, inserter_prototype) 149 | return (inserter_prototype.inserter_rotation_speed * 60) * PlayerInfo.inserter_stack_size(player_index, inserter_prototype) 150 | end 151 | 152 | --- @class InternalDirectionSpec 153 | --- @field linearIngredientDirection defines.direction internal specification 154 | --- @field linearOutputDirection defines.direction internal specification 155 | --- @field blueprintRotation number num of 90 degrees to rotate 156 | 157 | --- @param original_output_position defines.direction 158 | --- @return InternalDirectionSpec 159 | function PlayerInfo.get_internal_direction_spec(player_index, original_output_position) 160 | assertNotNull(player_index, original_output_position) 161 | 162 | local ui_spec = PlayerInfo.direction_settings(player_index) 163 | --- @type InternalDirectionSpec 164 | local internal_spec = {} 165 | 166 | internal_spec.blueprintRotation = ((ui_spec.productPosition - original_output_position) % 8) / 2 167 | internal_spec.linearIngredientDirection = (ui_spec.ingredientDirection - (internal_spec.blueprintRotation * 2)) % 8 168 | internal_spec.linearOutputDirection = (ui_spec.productDirection - (internal_spec.blueprintRotation * 2)) % 8 169 | 170 | return internal_spec 171 | end 172 | 173 | --- @return BlueprintDirectionSpec a modifiable direction specification 174 | function PlayerInfo.direction_settings(player_index) 175 | if not global.settings[player_index].direction_spec then 176 | global.settings[player_index].direction_spec = { productPosition = 0, productDirection = 0, ingredientDirection = 0 } 177 | end 178 | return global.settings[player_index].direction_spec 179 | end 180 | 181 | function PlayerInfo.set_default_settings(player_index) 182 | global.settings[player_index] = { 183 | belt = 1, 184 | factory_priority = {}, 185 | direction_spec = { ingredientDirection = defines.direction.east, productDirection = defines.direction.east, productPosition = defines.direction.north } 186 | } 187 | end 188 | 189 | --- checks for newly unlocked crafting machines and add it to the priority list 190 | function PlayerInfo.update_crafting_machine_priorities(player_index) 191 | local unlocked_factories = PlayerInfo.unlocked_crafting_machines(player_index) 192 | local factory_priority = PlayerInfo.crafting_machine_priorities(player_index) 193 | 194 | for _, unlocked_factory in ipairs(unlocked_factories) do 195 | if not ArrayList.has(factory_priority, unlocked_factory, function(a, b) 196 | return a.name == b.name 197 | end) then 198 | ArrayList.insert(factory_priority, unlocked_factory) 199 | end 200 | end 201 | end 202 | 203 | --- insert an blueprint to player's inventory, fail if inventory is full 204 | --- @param player_index player_index 205 | --- @param entities Entity[] 206 | --- @return nil|LuaItemStack nilable, item stack representing the blueprint in the player's inventory 207 | function PlayerInfo.insert_blueprint(player_index, entities) 208 | assertNotNull(player_index, entities) 209 | 210 | local player_inventory = game.players[player_index].get_main_inventory() 211 | if not player_inventory.can_insert("blueprint") then 212 | logging.log("player's inventory is full, can't insert a new blueprint", logging.I) 213 | return 214 | end 215 | player_inventory.insert("blueprint") 216 | for i = 1, #player_inventory, 1 do 217 | local item = player_inventory[i] 218 | if item.is_blueprint and not item.is_blueprint_setup() then 219 | item.set_blueprint_entities(entities) 220 | return item 221 | end 222 | end 223 | end 224 | 225 | return PlayerInfo -------------------------------------------------------------------------------- /prototype_info.lua: -------------------------------------------------------------------------------- 1 | --- @type ArrayList 2 | local ArrayList = require("__MiscLib__/array_list") 3 | --- @type Vector2D 4 | local Vector2D = require("__MiscLib__/vector2d") 5 | --- @type Logger 6 | local logging = require("__MiscLib__/logging") 7 | 8 | --- Since we cannot access prototype information outside game loading stage, we need extra prototype information to support any crafting machines with fluid boxes. 9 | local prototype_addition = { 10 | -- Below is fluid prototypes of the base game 11 | ["assembling-machine-2"] = { 12 | fluid_boxes = { 13 | { 14 | pipe_connections = { { position = { 0, -2 }, type = "input" } }, 15 | production_type = "input" 16 | }, 17 | { 18 | pipe_connections = { { position = { 0, 2 }, type = "output" } }, 19 | production_type = "output", 20 | }, 21 | off_when_no_fluid_recipe = true 22 | }, 23 | }, 24 | ["assembling-machine-3"] = { 25 | fluid_boxes = { 26 | { 27 | pipe_connections = { { position = { 0, -2 }, type = "input" } }, 28 | production_type = "input" 29 | }, 30 | { 31 | pipe_connections = { { position = { 0, 2 }, type = "output" } }, 32 | production_type = "output", 33 | }, 34 | off_when_no_fluid_recipe = true 35 | }, 36 | }, 37 | ["chemical-plant"] = { 38 | fluid_boxes = { 39 | { 40 | pipe_connections = { { position = { -1, -2 }, type = "input" } }, 41 | production_type = "input" 42 | }, 43 | { 44 | pipe_connections = { { position = { 1, -2 }, type = "input" } }, 45 | production_type = "input" 46 | }, 47 | { 48 | pipe_connections = { { position = { -1, 2 }, type = "output" } }, 49 | production_type = "output" 50 | }, 51 | { 52 | pipe_connections = { { position = { 1, 2 } }, type = "output" }, 53 | production_type = "output" 54 | } 55 | }, 56 | }, 57 | ["oil-refinery"] = { 58 | fluid_boxes = { 59 | { 60 | pipe_connections = { { position = { 1, 3 }, type = "input" } }, 61 | production_type = "input" 62 | }, 63 | { 64 | pipe_connections = { { position = { -1, 3 }, type = "input" } }, 65 | production_type = "input" 66 | }, 67 | { 68 | pipe_connections = { { position = { 2, -3 } } }, 69 | production_type = "output" 70 | }, 71 | { 72 | pipe_connections = { { position = { 0, -3 } } }, 73 | production_type = "output" 74 | }, 75 | { 76 | pipe_connections = { { position = { -2, -3 } } }, 77 | production_type = "output" 78 | } 79 | }, 80 | } 81 | -- Below is prototypes of mods, add your mod fluid box prototype here 82 | } 83 | 84 | -- TODO: remove this field and replace it with automatic inferred belts 85 | ALL_BELTS = { "transport-belt", "fast-transport-belt", "express-transport-belt" } 86 | 87 | 88 | --- fields that allows prototype.field == nil to exist. 89 | local nullable_fields = ArrayList.new{ "fluid_boxes" } 90 | 91 | --- @return any an entity prototype with additional information if available 92 | function get_entity_prototype(name) 93 | local entity = game.entity_prototypes[name] 94 | return setmetatable({}, { 95 | __index = function(_, k) 96 | if nullable_fields:has(k) then 97 | if prototype_addition[name] then 98 | return prototype_addition[name][k] 99 | else 100 | return nil 101 | end 102 | else 103 | if prototype_addition[name] then 104 | return prototype_addition[name][k] or entity[k] 105 | end 106 | return entity[k] 107 | end 108 | end 109 | }) 110 | end 111 | 112 | 113 | local PrototypeInfo = {} 114 | --- @param prototype LuaEntityPrototype 115 | --- @return Vector2D 116 | function PrototypeInfo.get_size(prototype) 117 | return Vector2D.new( 118 | math.floor(prototype.selection_box.right_bottom.x - prototype.selection_box.left_top.x + 0.5), 119 | math.floor(prototype.selection_box.right_bottom.y - prototype.selection_box.left_top.y + 0.5) 120 | ) 121 | end 122 | 123 | local corresponding_underground_transport_line_table = { 124 | ["pipe"] = "pipe-to-ground", 125 | ["transport-belt"] = "underground-belt", 126 | ["fast-transport-belt"] = "fast-underground-belt", 127 | ["express-transport-belt"] = "express-underground-belt" 128 | } 129 | 130 | --- @param transport_name string prototype name of either a transport belt or a pipe 131 | --- @return LuaEntityPrototype 132 | function PrototypeInfo.underground_transport_prototype(transport_name) 133 | if corresponding_underground_transport_line_table[transport_name] ~= nil then 134 | return game.entity_prototypes[corresponding_underground_transport_line_table[transport_name]] 135 | else 136 | if game.entity_prototypes[transport_name].belt_speed then 137 | logging.log(transport_name .. " is not one of known transport belt with underground version") 138 | return game.entity_prototypes["express-underground-belt"] 139 | elseif game.entity_prototypes[transport_name].fluid_capacity then 140 | logging.log(transport_name .. "is not one of known pipe with underground version") 141 | return game.entity_prototypes["pipe-to-ground"] 142 | end 143 | end 144 | 145 | assert(false, transport_name .. " is neither a transport belt nor a pipe, and hence shall not have a corresponding underground version of it") 146 | end 147 | 148 | function PrototypeInfo.is_underground_transport(name) 149 | if game.entity_prototypes[name].max_underground_distance then 150 | return true 151 | end 152 | return false 153 | end 154 | 155 | function sprite_of(name) 156 | assert(type(name) == "string") 157 | if game.item_prototypes[name] then 158 | return "item/" .. name 159 | elseif game.fluid_prototypes[name] then 160 | return "fluid/" .. name 161 | elseif game.entity_prototypes[name] then 162 | return "entity/" .. name 163 | else 164 | logging.log("failed to find sprite path for name " .. name) 165 | end 166 | end 167 | 168 | return PrototypeInfo -------------------------------------------------------------------------------- /release_mode.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by EmmyLua(https://github.com/EmmyLua) 3 | --- Created by seancheey. 4 | --- DateTime: 11/10/20 3:40 AM 5 | --- 6 | 7 | return false -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | require("gui.root_names") 2 | local PlayerInfo = require("player_info") 3 | 4 | local testing_recipes = { "copper-plate", "transport-belt", "steel-chest", "advanced-oil-processing", "coal-liquefaction", "explosives" } 5 | local testing_speed = { 0.01, 1, 50 } 6 | 7 | function start_unit_tests(player_index) 8 | if false then 9 | for _, recipe_name in ipairs(testing_recipes) do 10 | for _, speed in ipairs(testing_speed) do 11 | insert_assembler_node(recipe_name, player_index, speed) 12 | end 13 | end 14 | end 15 | end 16 | 17 | function insert_test_blueprint(recipe_name, player_index) 18 | local graph = BlueprintGraph.new(player_index) 19 | --- @type OutputSpec[] 20 | local output_specs = { { ingredient = recipe_name, unit = 1, crafting_speed = 10 } } 21 | graph:generate_graph_by_outputs(output_specs) 22 | graph:generate_blueprint() 23 | end 24 | 25 | function insert_assembler_node(recipe_name, player_index, recipe_speed) 26 | local node = AssemblerNode.new({ recipe = game.recipe_prototypes[recipe_name], player_index = player_index, recipe_speed = recipe_speed }) 27 | local section = node:generate_section() 28 | PlayerInfo.insert_blueprint(player_index, section.entities) 29 | end 30 | 31 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seancheey/FactorioBlueprinter/9fd10a5fc8e28bce2d841034904d0919bc93b036/thumbnail.png --------------------------------------------------------------------------------