766 lines
23 KiB
Lua

local enums = require("mpp.enums")
local blacklist = require("mpp.blacklist")
local floor, ceil = math.floor, math.ceil
local min, max = math.min, math.max
local EAST = defines.direction.east
local NORTH = defines.direction.north
local SOUTH = defines.direction.south
local WEST = defines.direction.west
---@alias DirectionString
---| "west"
---| "east"
---| "south"
---| "north"
local mpp_util = {}
---@alias CoordinateConverterFunction fun(number, number, number, number): number, number
---@type table<DirectionString, CoordinateConverterFunction>
local coord_convert = {
west = function(x, y, w, h) return x, y end,
east = function(x, y, w, h) return w-x, h-y end,
south = function(x, y, w, h) return h-y, x end,
north = function(x, y, w, h) return y, w-x end,
}
mpp_util.coord_convert = coord_convert
---@type table<DirectionString, CoordinateConverterFunction>
local coord_revert = {
west = coord_convert.west,
east = coord_convert.east,
north = coord_convert.south,
south = coord_convert.north,
}
mpp_util.coord_revert = coord_revert
mpp_util.miner_direction = {west="south",east="north",north="west",south="east"}
mpp_util.belt_direction = {west="north", east="south", north="east", south="west"}
mpp_util.opposite = {west="east",east="west",north="south",south="north"}
do
-- switch to (direction + EAST) % ROTATION
local d = defines.direction
mpp_util.bp_direction = {
west = {
[d.north] = d.north,
[d.east] = d.east,
[d.south] = d.south,
[d.west] = d.west,
},
north = {
[d.north] = d.east,
[d.east] = d.south,
[d.south] = d.west,
[d.west] = d.north,
},
east = {
[d.north] = d.south,
[d.east] = d.west,
[d.south] = d.north,
[d.west] = d.east,
},
south = {
[d.north] = d.west,
[d.east] = d.north,
[d.south] = d.east,
[d.west] = d.south,
},
}
end
---@type table<defines.direction, MapPosition.0>
local direction_coord = {
[NORTH] = {x=0, y=-1},
[WEST] = {x=-1, y=0},
[SOUTH] = {x=0, y=1},
[EAST] = {x=1, y=0},
}
mpp_util.direction_coord = direction_coord
---@class EntityStruct
---@field name string
---@field type string
---@field w number Collision width
---@field h number Collision height
---@field x number x origin
---@field y number y origin
---@field size number?
---@field extent_w number Half of width
---@field extent_h number Half of height
---@type table<string, EntityStruct>
local entity_cache = {}
---@param entity_name string
---@return EntityStruct
function mpp_util.entity_struct(entity_name)
local cached = entity_cache[entity_name]
if cached then return cached end
---@diagnostic disable-next-line: missing-fields
local struct = {} --[[@as EntityStruct]]
local proto = game.entity_prototypes[entity_name]
local cbox = proto.collision_box
local cbox_tl, cbox_br = cbox.left_top, cbox.right_bottom
local cw, ch = cbox_br.x - cbox_tl.x, cbox_br.y - cbox_tl.y
struct.name = entity_name
struct.type = proto.type
struct.w, struct.h = ceil(cw), ceil(ch)
struct.size = max(struct.w, struct.h)
struct.x = struct.w / 2 - 0.5
struct.y = struct.h / 2 - 0.5
struct.extent_w, struct.extent_h = struct.w / 2, struct.h / 2
entity_cache[entity_name] = struct
return struct
end
---@param struct EntityStruct
---@param direction defines.direction
---@return number, number, number, number
function mpp_util.rotate_struct(struct, direction)
if direction == NORTH or direction == SOUTH then
return struct.x, struct.y, struct.w, struct.h
end
return struct.y, struct.x, struct.h, struct.w
end
---A mining drill's origin (0, 0) is the top left corner
---The spawn location is (x, y), rotations need to rotate around
---@class MinerStruct : EntityStruct
---@field size number Physical miner size
---@field size_sq number Size squared
---@field symmetric boolean
---@field parity (-1|0) Parity offset for even sized drills, -1 when odd
---@field resource_categories table<string, boolean>
---@field radius float Mining area reach
---@field area number Full coverage span of the miner
---@field area_sq number Squared area
---@field outer_span number Lenght between physical size and end of radius
---@field module_inventory_size number
---@field middle number "Center" x position
---@field drop_pos MapPosition Raw drop position
---@field out_x integer Resource drop position x
---@field out_y integer Resource drop position y
---@field extent_negative number
---@field extent_positive number
---@field supports_fluids boolean
---@field skip_outer boolean Skip outer area calculations
---@field pipe_left number Y height on left side
---@field pipe_right number Y height on right side
---@field output_rotated table<defines.direction, MapPosition> Rotated output positions in reference to (0, 0) origin
---@field power_source_tooltip (string|table)?
---@type table<string, MinerStruct>
local miner_struct_cache = {}
---Calculates values for drill sizes and extents
---@param mining_drill_name string
---@return MinerStruct
function mpp_util.miner_struct(mining_drill_name)
local cached = miner_struct_cache[mining_drill_name]
if cached then return cached end
local miner_proto = game.entity_prototypes[mining_drill_name]
---@diagnostic disable-next-line: missing-fields
local miner = mpp_util.entity_struct(mining_drill_name) --[[@as MinerStruct]]
if miner.w ~= miner.h then
-- we have a problem ?
end
miner.size_sq = miner.size ^ 2
miner.symmetric = miner.size % 2 == 1
miner.parity = miner.size % 2 - 1
miner.radius = miner_proto.mining_drill_radius
miner.area = ceil(miner_proto.mining_drill_radius * 2)
miner.area_sq = miner.area ^ 2
miner.outer_span = floor((miner.area - miner.size) / 2)
miner.resource_categories = miner_proto.resource_categories
miner.name = miner_proto.name
miner.module_inventory_size = miner_proto.module_inventory_size
miner.extent_negative = floor(miner.size * 0.5) - floor(miner_proto.mining_drill_radius) + miner.parity
miner.extent_positive = miner.extent_negative + miner.area - 1
miner.middle = floor(miner.size / 2) + miner.parity
local nauvis = game.get_surface("nauvis") --[[@as LuaSurface]]
local dummy = nauvis.create_entity{
name = mining_drill_name,
position = {miner.x, miner.y},
}
if dummy then
miner.drop_pos = dummy.drop_position
miner.out_x = floor(dummy.drop_position.x)
miner.out_y = floor(dummy.drop_position.y)
dummy.destroy()
else
-- hardcoded fallback
local dx, dy = floor(miner.size / 2) + miner.parity, -1
miner.drop_pos = { dx+.5, -0.296875, x = dx+.5, y = -0.296875 }
miner.out_x = dx
miner.out_y = dy
end
local output_rotated = {
[defines.direction.north] = {miner.out_x, miner.out_y},
[defines.direction.south] = {miner.size - miner.out_x - 1, miner.size },
[defines.direction.west] = {miner.size, miner.out_x},
[defines.direction.east] = {-1, miner.size - miner.out_x -1},
}
output_rotated[NORTH].x = output_rotated[NORTH][1]
output_rotated[NORTH].y = output_rotated[NORTH][2]
output_rotated[SOUTH].x = output_rotated[SOUTH][1]
output_rotated[SOUTH].y = output_rotated[SOUTH][2]
output_rotated[WEST].x = output_rotated[WEST][1]
output_rotated[WEST].y = output_rotated[WEST][2]
output_rotated[EAST].x = output_rotated[EAST][1]
output_rotated[EAST].y = output_rotated[EAST][2]
miner.output_rotated = output_rotated
--pipe height stuff
if miner_proto.fluidbox_prototypes and #miner_proto.fluidbox_prototypes > 0 then
local connections = miner_proto.fluidbox_prototypes[1].pipe_connections
for _, conn in pairs(connections) do
---@cast conn FluidBoxConnection
-- pray a mod that does weird stuff with pipe connections doesn't appear
end
miner.pipe_left = floor(miner.size / 2) + miner.parity
miner.pipe_right = floor(miner.size / 2) + miner.parity
miner.supports_fluids = true
else
miner.supports_fluids = false
end
-- If larger than a large mining drill
miner.skip_outer = miner.size > 7 or miner.area > 13
if miner_proto.electric_energy_source_prototype then
miner.power_source_tooltip = {
"", " [img=tooltip-category-electricity] ",
{"tooltip-category.consumes"}, " ", {"tooltip-category.electricity"},
}
elseif miner_proto.burner_prototype then
local burner = miner_proto.burner_prototype --[[@as LuaBurnerPrototype]]
if burner.fuel_categories["nuclear"] then
miner.power_source_tooltip = {
"", "[img=tooltip-category-nuclear]",
{"tooltip-category.consumes"}, " ", {"fuel-category-name.nuclear"},
}
else
miner.power_source_tooltip = {
"", "[img=tooltip-category-chemical]",
{"tooltip-category.consumes"}, " ", {"fuel-category-name.chemical"},
}
end
elseif miner_proto.fluid_energy_source_prototype then
miner.power_source_tooltip = {
"", "[img=tooltip-category-water]",
{"tooltip-category.consumes"}, " ", {"tooltip-category.fluid"},
}
end
return miner
end
---@class PoleStruct : EntityStruct
---@field name string
---@field place boolean Flag if poles are to be actually placed
---@field size number
---@field radius number Power supply reach
---@field supply_width number Full width of supply reach
---@field wire number Max wire distance
---@field supply_area_distance number
---@field extent_negative number Negative extent of the supply reach
---@type table<string, PoleStruct>
local pole_struct_cache = {}
---@param pole_name string
---@return PoleStruct
function mpp_util.pole_struct(pole_name)
local cached_struct = pole_struct_cache[pole_name]
if cached_struct then return cached_struct end
local pole_proto = game.entity_prototypes[pole_name]
if pole_proto then
local pole = mpp_util.entity_struct(pole_name) --[[@as PoleStruct]]
local radius = pole_proto.supply_area_distance --[[@as number]]
pole.supply_area_distance = radius
pole.supply_width = floor(radius * 2)
pole.radius = pole.supply_width / 2
pole.wire = pole_proto.max_wire_distance
-- local distance = beacon_proto.supply_area_distance
-- beacon.area = beacon.size + distance * 2
-- beacon.extent_negative = -distance
local extent = (pole.supply_width - pole.size) / 2
pole.extent_negative = -extent
pole_struct_cache[pole_name] = pole
return pole
end
return {
place = false, -- nonexistent pole, use fallbacks and don't place
size = 1,
supply_width = 7,
radius = 3.5,
wire = 9,
}
end
---@class BeaconStruct : EntityStruct
---@field extent_negative number
---@field area number
local beacon_cache = {}
---@param beacon_name string
---@return BeaconStruct
function mpp_util.beacon_struct(beacon_name)
local cached_struct = beacon_cache[beacon_name]
if cached_struct then return cached_struct end
local beacon_proto = game.entity_prototypes[beacon_name]
local beacon = mpp_util.entity_struct(beacon_name) --[[@as BeaconStruct]]
local distance = beacon_proto.supply_area_distance
beacon.area = beacon.size + distance * 2
beacon.extent_negative = -distance
beacon_cache[beacon_name] = beacon
return beacon
end
local hardcoded_pipes = {}
---@param pipe_name string Name of the normal pipe
---@return string|nil, LuaEntityPrototype|nil
function mpp_util.find_underground_pipe(pipe_name)
if hardcoded_pipes[pipe_name] then
return hardcoded_pipes[pipe_name], game.entity_prototypes[hardcoded_pipes[pipe_name]]
end
local ground_name = pipe_name.."-to-ground"
local ground_proto = game.entity_prototypes[ground_name]
if ground_proto then
return ground_name, ground_proto
end
return nil, nil
end
function mpp_util.revert(gx, gy, direction, x, y, w, h)
local tx, ty = coord_revert[direction](x-.5, y-.5, w, h)
return {gx + tx+.5, gy + ty + .5}
end
---comment
---@param gx any
---@param gy any
---@param direction any
---@param x any
---@param y any
---@param w any
---@param h any
---@return unknown
---@return unknown
function mpp_util.revert_ex(gx, gy, direction, x, y, w, h)
local tx, ty = coord_revert[direction](x-.5, y-.5, w, h)
return gx + tx+.5, gy + ty + .5
end
function mpp_util.revert_world(gx, gy, direction, x, y, w, h)
local tx, ty = coord_revert[direction](x-.5, y-.5, w, h)
return {gx + tx, gy + ty}
end
---@class BeltStruct
---@field name string
---@field related_underground_belt string?
---@field underground_reach number?
---@type table<string, BeltStruct>
local belt_struct_cache = {}
function mpp_util.belt_struct(belt_name)
local cached = belt_struct_cache[belt_name]
if cached then return cached end
---@diagnostic disable-next-line: missing-fields
local belt = {} --[[@as BeltStruct]]
local belt_proto = game.entity_prototypes[belt_name]
belt.name = belt_name
local related = belt_proto.related_underground_belt
if related then
belt.related_underground_belt = related.name
belt.underground_reach = related.max_underground_distance
else
local match_attempts = {
["transport"] = "underground",
["belt"] = "underground-belt",
}
for pattern, replacement in pairs(match_attempts) do
local new_name = string.gsub(belt_name, pattern, replacement)
if new_name == belt_name then goto continue end
related = game.entity_prototypes[new_name]
if related then
belt.related_underground_belt = new_name
belt.underground_reach = related.max_underground_distance
break
end
::continue::
end
end
belt_struct_cache[belt_name] = belt
return belt
end
---@class InserterStruct : EntityStruct
---@field pickup_rotated table<defines.direction, MapPosition.0>
---@field drop_rotated table<defines.direction, MapPosition.0>
---@type table<string, InserterStruct>
local inserter_struct_cache = {}
function mpp_util.inserter_struct(inserter_name)
local cached = inserter_struct_cache[inserter_name]
if cached then return cached end
local inserter_proto = game.entity_prototypes[inserter_name]
local inserter = mpp_util.entity_struct(inserter_name) --[[@as InserterStruct]]
local function rotations(_x, _y)
_x, _y = floor(_x), floor(_y)
return {
[NORTH] = { x = _x, y = _y},
[EAST] = { x = -_y, y = -_x},
[SOUTH] = { x = -_x, y = -_y},
[WEST] = { x = _y, y = _x},
}
end
local pickup_position = inserter_proto.inserter_pickup_position --[[@as MapPosition.1]]
local drop_position = inserter_proto.inserter_drop_position --[[@as MapPosition.1]]
inserter.pickup_rotated = rotations(pickup_position[1], pickup_position[2])
inserter.drop_rotated = rotations(drop_position[1], drop_position[2])
inserter_struct_cache[inserter_name] = inserter
return inserter
end
---Calculates needed power pole count
---@param state SimpleState
function mpp_util.calculate_pole_coverage(state, miner_count, lane_count)
local cov = {}
local m = mpp_util.miner_struct(state.miner_choice)
local p = mpp_util.pole_struct(state.pole_choice)
-- Shift subtract
local covered_miners = ceil(p.supply_width / m.size)
local miner_step = covered_miners * m.size
-- Special handling to shift back small radius power poles so they don't poke out
local capable_span = false
if floor(p.wire) >= miner_step and m.size ~= p.supply_width then
capable_span = true
else
miner_step = floor(p.wire)
end
cov.capable_span = capable_span
local pole_start = m.middle
if capable_span then
if covered_miners % 2 == 0 then
pole_start = m.size-1
elseif miner_count % covered_miners == 0 then
pole_start = pole_start + m.size
end
end
cov.pole_start = pole_start
cov.pole_step = miner_step
cov.full_miner_width = miner_count * m.size
cov.lane_start = 0
cov.lane_step = m.size * 2 + 2
local lane_pairs = floor(lane_count / 2)
local lane_coverage = ceil((p.radius-1) / (m.size + 0.5))
if lane_coverage > 1 then
cov.lane_start = (ceil(lane_pairs / 2) % 2 == 0 and 1 or 0) * (m.size * 2 + 2)
cov.lane_step = lane_coverage * (m.size * 2 + 2)
end
cov.lamp_alter = miner_step < 9 and true or false
return cov
end
---Calculates the spacing for belt interleaved power poles
---@param state State
---@param miner_count number
---@param lane_count number
---@param force_capable (number|true)?
function mpp_util.calculate_pole_spacing(state, miner_count, lane_count, force_capable)
local cov = {}
local m = mpp_util.miner_struct(state.miner_choice)
local p = mpp_util.pole_struct(state.pole_choice)
-- Shift subtract
local covered_miners = ceil(p.supply_width / m.size)
local miner_step = covered_miners * m.size
if force_capable then
miner_step = force_capable == true and miner_step or force_capable --[[@as number]]
force_capable = miner_step
end
-- Special handling to shift back small radius power poles so they don't poke out
local capable_span = false
if floor(p.wire) >= miner_step then
capable_span = true
elseif force_capable and force_capable > 0 then
return mpp_util.calculate_pole_spacing(
state, miner_count, lane_count, miner_step - m.size
)
else
miner_step = floor(p.wire)
end
cov.capable_span = capable_span
local pole_start = m.size-1
if capable_span then
if covered_miners % 2 == 0 then
pole_start = m.size-1
elseif miner_count % covered_miners == 0 and miner_step ~= m.size then
pole_start = pole_start + m.size
end
end
cov.pole_start = pole_start
cov.pole_step = miner_step
cov.full_miner_width = miner_count * m.size
cov.lane_start = 0
cov.lane_step = m.size * 2 + 2
local lane_pairs = floor(lane_count / 2)
local lane_coverage = ceil((p.radius-1) / (m.size + 0.5))
if lane_coverage > 1 then
cov.lane_start = (ceil(lane_pairs / 2) % 2 == 0 and 1 or 0) * (m.size * 2 + 2)
cov.lane_step = lane_coverage * (m.size * 2 + 2)
end
return cov
end
---@param t table
---@param func function
---@return true | nil
function mpp_util.table_find(t, func)
for k, v in pairs(t) do
if func(v) then return true end
end
end
---@param t table
---@param m LuaObject
function mpp_util.table_mapping(t, m)
for k, v in pairs(t) do
if k == m then return v end
end
end
---@param player LuaPlayer
---@param blueprint LuaItemStack
function mpp_util.validate_blueprint(player, blueprint)
if not blueprint.blueprint_snap_to_grid then
player.print({"", "[color=red]", {"mpp.msg_blueprint_undefined_grid"}, "[/color]"}, {sound_path="utility/cannot_build"})
return false
end
local miners, _ = enums.get_available_miners()
local cost = blueprint.cost_to_build
local drills = {}
for name, drill in pairs(miners) do
if cost[name] then
drills[#drills+1] = drill.localised_name
end
end
if #drills > 1 then
local msg = {"", "[color=red]", {"mpp.msg_blueprint_different_miners"}, "[/color]" }
for _, name in pairs(drills) do
msg[#msg+1] = "\n"
msg[#msg+1] = name
end
player.print(msg, {sound_path="utility/cannot_build"})
return false
elseif #drills == 0 then
player.print({"", "[color=red]", {"mpp.msg_blueprint_no_miner"}, "[/color]"}, {sound_path="utility/cannot_build"})
return false
end
return true
end
function mpp_util.keys_to_set(...)
local set, temp = {}, {}
for _, t in pairs{...} do
for k, _ in pairs(t) do
temp[k] = true
end
end
for k, _ in pairs(temp) do
set[#set+1] = k
end
table.sort(set)
return set
end
function mpp_util.list_to_keys(t)
local temp = {}
for _, k in ipairs(t) do
temp[k] = true
end
return temp
end
---@param bp LuaItemStack
function mpp_util.blueprint_label(bp)
local label = bp.label
if label then
if #label > 30 then
return string.sub(label, 0, 28) .. "...", label
end
return label
else
return {"", {"gui-blueprint.unnamed-blueprint"}, " ", bp.item_number}
end
end
---@class CollisionBoxProperties
---@field w number
---@field h number
---@field near number
---@field [1] boolean
---@field [2] boolean
-- LuaEntityPrototype#tile_height was added in 1.1.64, I'm developing on 1.1.61
local even_width_memoize = {}
---Gets properties of entity collision box
---@param name string
---@return CollisionBoxProperties
function mpp_util.entity_even_width(name)
local check = even_width_memoize[name]
if check then return check end
local proto = game.entity_prototypes[name]
local cbox = proto.collision_box
local cbox_tl, cbox_br = cbox.left_top, cbox.right_bottom
local cw, ch = cbox_br.x - cbox_tl.x, cbox_br.y - cbox_tl.y
local w, h = ceil(cw), ceil(ch)
local res = {w % 2 ~= 1, h % 2 ~= 1, w=w, h=h, near=floor(w/2)}
even_width_memoize[name] = res
return res
end
--- local EAST, NORTH, SOUTH, WEST, ROTATION = mpp_util.directions()
function mpp_util.directions()
return
defines.direction.east,
defines.direction.north,
defines.direction.south,
defines.direction.west,
table_size(defines.direction)
end
---@param player_index uint
---@return uint
function mpp_util.get_display_duration(player_index)
return settings.get_player_settings(player_index)["mpp-lane-filling-info-duration"].value * 60 --[[@as uint]]
end
---@param player_index uint
---@return boolean
function mpp_util.get_dump_state(player_index)
return settings.get_player_settings(player_index)["mpp-dump-heuristics-data"].value --[[@as boolean]]
end
function mpp_util.wrap_tooltip(...)
return select(1, ...) and {"", " ", ...} or nil
end
function mpp_util.tooltip_entity_not_available(check, arg)
if check then
return mpp_util.wrap_tooltip(arg, "\n[color=red]", {"mpp.label_not_available"}, "[/color]")
end
return mpp_util.wrap_tooltip(arg)
end
---@param c1 Coords
---@param c2 Coords
function mpp_util.coords_overlap(c1, c2)
local x = (c1.ix1 <= c2.ix1 and c2.ix1 <= c1.ix2) or (c1.ix1 <= c2.ix2 and c2.ix2 <= c1.ix2) or
(c2.ix1 <= c1.ix1 and c1.ix1 <= c2.ix2) or (c2.ix1 <= c1.ix2 and c1.ix2 <= c2.ix2)
local y = (c1.iy1 <= c2.iy1 and c2.iy1 <= c1.iy2) or (c1.iy1 <= c2.iy2 and c2.iy2 <= c1.iy2) or
(c2.iy1 <= c1.iy1 and c1.iy1 <= c2.iy2) or (c2.iy1 <= c1.iy2 and c1.iy2 <= c2.iy2)
return x and y
end
---Checks if thing (entity) should never appear as a choice
---@param thing LuaEntityPrototype|MinerStruct
---@return boolean|nil
function mpp_util.check_filtered(thing)
return
blacklist[thing.name]
or (thing.flags and thing.flags.hidden)
end
---@param player_data any
---@param category MppSettingSections
---@param name string
function mpp_util.set_entity_hidden(player_data, category, name, value)
player_data.filtered_entities[category..":"..name] = value
end
function mpp_util.get_entity_hidden(player_data, category, name)
return player_data.filtered_entities[category..":"..name]
end
---Checks if a player has hidden the entity choice
---@param player_data any
---@param category MppSettingSections
---@param thing MinerStruct|LuaEntityPrototype
---@return false
function mpp_util.check_entity_hidden(player_data, category, thing)
return (not player_data.entity_filtering_mode and player_data.filtered_entities[category..":"..thing.name])
end
---@param player_data PlayerData
function mpp_util.update_undo_button(player_data)
local enabled = false
local undo_button = player_data.gui.undo_button
local last_state = player_data.last_state
if last_state then
local duration = mpp_util.get_display_duration(last_state.player.index)
enabled = enabled or (last_state and last_state._collected_ghosts and #last_state._collected_ghosts > 0 and game.tick < player_data.tick_expires)
end
undo_button.enabled = enabled
undo_button.sprite = enabled and "mpp_undo_enabled" or "mpp_undo_disabled"
undo_button.tooltip = mpp_util.wrap_tooltip(enabled and {"controls.undo"} or {"", {"controls.undo"}," (", {"gui.not-available"}, ")"})
end
return mpp_util