360 lines
11 KiB
Lua

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<string, Layout>
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<string, true>
---@return Coords @bounds of found resources
---@return LuaEntity[] @Filtered entities
---@return table<string, string> @key:resource name; value:resource category
---@return boolean @requires fluid
---@return table<string, number> @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