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 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 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 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 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 ---@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 Rotated output positions in reference to (0, 0) origin ---@field power_source_tooltip (string|table)? ---@type table 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 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 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 ---@field drop_rotated table ---@type table 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