local enums = require("mpp.enums") local mpp_util = require("mpp.mpp_util") local compatibility = require("mpp.compatibility") local floor, ceil = math.floor, math.ceil local min, max = math.min, math.max local algorithm = {} ---@type table local layouts = {} algorithm.layouts = layouts local function require_layout(layout) layouts[layout] = require("layouts."..layout) layouts[#layouts+1] = layouts[layout] end require_layout("simple") require_layout("compact") require_layout("super_compact") require_layout("sparse") require_layout("logistics") require_layout("compact_logistics") require_layout("sparse_logistics") require_layout("blueprints") ---@class MininimumPreservedState ---@field layout_choice string ---@field player LuaPlayer ---@field surface LuaSurface ---@field resources LuaEntity[] Filtered resources ---@field coords Coords ---@field _previous_state MininimumPreservedState? ---@field _collected_ghosts LuaEntity[] ---@field _preview_rectangle nil|uint64 LuaRendering.draw_rectangle ---@field _lane_info_rendering uint64[] ---@field _render_objects uint64[] LuaRendering objects ---@class State : MininimumPreservedState ---@field _callback string -- callback to be used in the tick ---@field tick number ---@field is_space boolean ---@field resource_tiles GridTile ---@field found_resources LuaEntity[] Resource name -> resource category mapping ---@field resource_counts {name: string, count: number}[] Highest count resource first ---@field requires_fluid boolean ---@field mod_version string --- ---@field layout_choice string ---@field direction_choice string ---@field miner_choice string ---@field pole_choice string ---@field belt_choice string Belt name ---@field space_belt_choice string ---@field lamp_choice boolean Lamp placement ---@field coverage_choice boolean ---@field logistics_choice string ---@field landfill_choice boolean ---@field space_landfill_choice string ---@field start_choice boolean ---@field deconstruction_choice boolean ---@field pipe_choice string ---@field module_choice string ---@field force_pipe_placement_choice boolean ---@field print_placement_info_choice boolean ---@field display_lane_filling_choice boolean --- ---@field grid Grid ---@field deconstruct_specification DeconstructSpecification ---@field miner MinerStruct ---@field pole PoleStruct ---@field belt BeltStruct ---@field blueprint_choice LuaGuiElement ---@field blueprint_inventory LuaInventory ---@field blueprint LuaItemStack ---@field cache EvaluatedBlueprint --- Return value for layout callbacks --- string - name of next callback --- true - repeat the current callback --- false - job is finished ---@alias CallbackState string|boolean ---@param event EventData.on_player_selected_area ---@return State|nil ---@return LocalisedString error status local function create_state(event) ---@diagnostic disable-next-line: missing-fields local state = {} --[[@as State]] state._callback = "start" state.tick = 0 state.mod_version = game.active_mods["mining-patch-planner"] state._preview_rectangle = nil state._collected_ghosts = {} state._render_objects = {} state._lane_info_rendering = {} ---@type PlayerData local player_data = global.players[event.player_index] -- game state properties state.surface = event.surface state.is_space = compatibility.is_space(event.surface.index) state.player = game.players[event.player_index] -- fill in player option properties local player_choices = player_data.choices for k, v in pairs(player_choices) do state[k] = util.copy(v) end if state.is_space then if game.entity_prototypes["se-space-pipe"] then state.pipe_choice = "se-space-pipe" else state.pipe_choice = "none" end state.belt_choice = state.space_belt_choice end state.debug_dump = mpp_util.get_dump_state(event.player_index) if state.layout_choice == "blueprints" then local blueprint = player_data.choices.blueprint_choice if blueprint == nil then return nil, {"mpp.msg_unselected_blueprint"} end -- state.blueprint_inventory = game.create_inventory(1) -- state.blueprint = state.blueprint_inventory.find_empty_stack() -- state.blueprint.set_stack(blueprint) state.cache = player_data.blueprints.cache[blueprint.item_number] end return state end ---Filters resource entity list and returns patch coordinates and size ---@param entities LuaEntity[] ---@param available_resource_categories table ---@return Coords @bounds of found resources ---@return LuaEntity[] @Filtered entities ---@return table @key:resource name; value:resource category ---@return boolean @requires fluid ---@return table @resource counts table local function process_entities(entities, available_resource_categories) local filtered, found_resources, counts = {}, {}, {} local x1, y1 = math.huge, math.huge local x2, y2 = -math.huge, -math.huge local _, cached_resource_categories = enums.get_available_miners() local checked, requires_fluid = {}, false for _, entity in pairs(entities) do local name, proto = entity.name, entity.prototype local category = proto.resource_category if not checked[name] then checked[name] = true if proto.mineable_properties.required_fluid then requires_fluid = true end end found_resources[name] = category if cached_resource_categories[category] and available_resource_categories[category] then counts[name] = 1 + (counts[name] or 0) filtered[#filtered+1] = entity local x, y = entity.position.x, entity.position.y if x < x1 then x1 = x end if y < y1 then y1 = y end if x2 < x then x2 = x end if y2 < y then y2 = y end end end local resource_counts = {} for k, v in pairs(counts) do table.insert(resource_counts, {name=k, count=v}) end table.sort(resource_counts, function(a, b) return a.count > b.count end) local coords = { x1 = x1, y1 = y1, x2 = x2, y2 = y2, ix1 = floor(x1), iy1 = floor(y1), ix2 = ceil(x2), iy2 = ceil(y2), gx = x1 - 1, gy = y1 - 1, } coords.w, coords.h = coords.ix2 - coords.ix1, coords.iy2 - coords.iy1 return coords, filtered, found_resources, requires_fluid, resource_counts end ---@param state State ---@param layout Layout local function get_miner_categories(state, layout) if layout.name == "blueprints" then return state.cache:get_resource_categories() else return game.entity_prototypes[state.miner_choice].resource_categories or {} end end --- Algorithm hook --- Returns nil if it fails ---@param event EventData.on_player_selected_area function algorithm.on_player_selected_area(event) ---@type PlayerData local player_data = global.players[event.player_index] local state, err = create_state(event) if not state then return nil, err end local layout = layouts[player_data.choices.layout_choice] if state.miner_choice == "none" then return nil, {"mpp.msg_miner_err_3"} end local layout_categories = get_miner_categories(state, layout) local coords, filtered, found_resources, requires_fluid, resource_counts = process_entities(event.entities, layout_categories) state.coords = coords state.resources = filtered state.found_resources = found_resources state.requires_fluid = requires_fluid state.resource_counts = resource_counts if #state.resources == 0 then for resource, category in pairs(state.found_resources) do if not layout_categories[category] then local miner_name = game.entity_prototypes[state.miner_choice].localised_name if layout.name == "blueprints" then miner_name = {"mpp.choice_none"} for k, v in pairs(state.cache.miners) do miner_name = game.entity_prototypes[k].localised_name break end end local resource_name = game.entity_prototypes[resource].localised_name return nil, {"", {"mpp.msg_miner_err_2_1"}, " \"", miner_name, "\" ", {"mpp.msg_miner_err_2_2"}, " \"", resource_name, "\""} end end return nil, {"mpp.msg_miner_err_0"} end local last_state = player_data.last_state --[[@as MininimumPreservedState]] if last_state ~= nil then local renderables = last_state._render_objects local old_resources = last_state.resources local same = mpp_util.coords_overlap(coords, last_state.coords) -- if same then -- for i, v in pairs(old_resources) do -- if v ~= filtered[i] then -- same = false -- break -- end -- end -- end if same then for _, id in ipairs(renderables) do rendering.destroy(id) end state._previous_state = last_state else local ttl = mpp_util.get_display_duration(event.player_index) for _, id in ipairs(renderables) do if rendering.is_valid(id) then rendering.set_time_to_live(id, ttl) end end end player_data.last_state = nil end local validation_result, error = layout:validate(state) if validation_result then layout:initialize(state) state.player.play_sound{path="utility/blueprint_selection_ended"} -- "Progress" bar local c = state.coords state._preview_rectangle = rendering.draw_rectangle{ surface=state.surface, left_top={state.coords.ix1, state.coords.iy1}, right_bottom={state.coords.ix1 + c.w, state.coords.iy1 + c.h}, filled=false, color={0, 0.8, 0.3, 1}, width = 8, draw_on_ground = true, players={state.player}, } return state else return nil, error end end ---Sets renderables timeout ---@param player_data PlayerData function algorithm.on_gui_open(player_data) local last_state = player_data.last_state if last_state == nil or last_state._render_objects == nil then return end local ttl = mpp_util.get_display_duration(last_state.player.index) if ttl > 0 then for _, id in ipairs(last_state._render_objects) do if rendering.is_valid(id) then rendering.set_time_to_live(id, 0) end end end end ---Sets renderables timeout ---@param player_data PlayerData function algorithm.on_gui_close(player_data) local last_state = player_data.last_state if last_state == nil or last_state._render_objects == nil then return end local ttl = mpp_util.get_display_duration(last_state.player.index) if ttl > 0 then for _, id in ipairs(last_state._render_objects) do if rendering.is_valid(id) then rendering.set_time_to_live(id, ttl) end end else for _, id in ipairs(last_state._render_objects) do rendering.destroy(id) end end end ---@param player_data PlayerData function algorithm.cleanup_last_state(player_data) local state = player_data.last_state if not state then return end local force, ply = state.player.force, state.player if type(state._collected_ghosts) == "table" then for _, ghost in pairs(state._collected_ghosts) do if ghost.valid then ghost.order_deconstruction(force, ply) end end state._collected_ghosts = {} end if type(state._render_objects) == "table" then for _, id in ipairs(state._render_objects) do if rendering.is_valid(id) then rendering.destroy(id) end end state._render_objects = {} end rendering.destroy(state._preview_rectangle) mpp_util.update_undo_button(player_data) player_data.last_state = nil end return algorithm