Добавлены все обновления от сообщества, вплоть до #148

This commit is contained in:
2024-09-12 14:28:43 +03:00
parent 98159766c4
commit 487a0e6e16
8841 changed files with 23077 additions and 20175 deletions

View File

@@ -0,0 +1,24 @@
local b = {}
b["robotMiningSite-extra"] = true
b["robotMiningSite-large"] = true
b["robotMiningSite-new"] = true
b["invisible-logistic-chest-storage"] = true
b["robot-chest-provider"] = true
b["robot-chest-requester"] = true
b["construction-recaller"] = true
b["logistic-recaller"] = true
b["se-space-elevator"] = true
b["se-space-elevator-energy-interface"] = true
b["se-space-elevator-energy-pole"] = true
b["se-spaceship-clamp"] = true
b["se-spaceship-clamp-place"] = true
b["se-spaceship-clamp-power-pole-external-east"] = true
b["se-spaceship-clamp-power-pole-external-west"] = true
b["se-spaceship-clamp-power-pole-internal"] = true
b["kr-quarry-drill"] = true
return b

View File

@@ -0,0 +1,171 @@
local EAST = defines.direction.east
local NORTH = defines.direction.north
local SOUTH = defines.direction.south
local WEST = defines.direction.west
---@class EvaluatedBlueprint
---@field valid boolean Are all blueprint entities valid
---@field w number
---@field h number
---@field tw number Runtime transposed width
---@field th number Runtime transposed height
---@field ox number Start offset x
---@field oy number Start offset y
---@field entities BlueprintEntityEx[] All entities in the blueprint
---@field entity_names table<string, number>
---@field miner_name string Mining drill name
local bp_meta = {}
bp_meta.__index = bp_meta
---@class BlueprintEntityEx : BlueprintEntity
---@field capstone_x boolean
---@field capstone_y boolean
---Blueprint analysis data
---@param bp LuaItemStack
---@return EvaluatedBlueprint
function bp_meta:new(bp)
---@type EvaluatedBlueprint
local new = setmetatable({}, self)
new.valid = true
new.w, new.h = bp.blueprint_snap_to_grid.x, bp.blueprint_snap_to_grid.y
new.ox, new.oy = 0, 0
if bp.blueprint_position_relative_to_grid then
new.ox = bp.blueprint_position_relative_to_grid.x
new.oy = bp.blueprint_position_relative_to_grid.y
end
new.entities = bp.get_blueprint_entities() or {}
new.entity_names = bp.cost_to_build
new:evaluate_tiling()
new:evaluate_miners()
return new
end
---Marks capstone BlueprintEntities
function bp_meta:evaluate_tiling()
local sw, sh = self.w, self.h
local buckets_x, buckets_y = {}, {}
for i, ent in pairs(self.entities) do
local x, y = ent.position.x, ent.position.y
ent.direction = ent.direction or NORTH
if not buckets_x[x] then buckets_x[x] = {} end
table.insert(buckets_x[x], ent)
if not buckets_y[y] then buckets_y[y] = {} end
table.insert(buckets_y[y], ent)
end
for _, bucket in pairs(buckets_y) do
for i = 1, #bucket-1 do
local e1 = bucket[i] ---@type BlueprintEntityEx
local e1x = e1.position.x
for j = 2, #bucket do
local e2 = bucket[j] ---@type BlueprintEntityEx
local e2x = e2.position.x
if e1x + sw == e2x or e1x - sw == e2x then
e2.capstone_x = true
end
end
end
end
for _, bucket in pairs(buckets_x) do
for i = 1, #bucket-1 do
local e1 = bucket[i] ---@type BlueprintEntityEx
local e1y = e1.position.y
for j = 2, #bucket do
local e2 = bucket[j] ---@type BlueprintEntityEx
local e2y = e2.position.y
if e1y + sh == e2y or e1y - sh == e2y then
e2.capstone_y = true
end
end
end
end
end
function bp_meta:evaluate_miners()
for _, ent in pairs(self.entities) do
local name = ent.name
if game.entity_prototypes[name].type == "mining-drill" then
--local proto = game.entity_prototypes[name]
self.miner_name = name
return
end
end
end
---@return BlueprintEntityEx[]
function bp_meta:get_mining_drills()
local miner_name = self.miner_name
local mining_drills = {}
for _, ent in pairs(self.entities) do
if ent.name == miner_name then
mining_drills[#mining_drills+1] = ent
end
end
return mining_drills
end
function bp_meta:check_valid()
for k, v in pairs(self.entity_names) do
if not game.entity_prototypes[k] and not game.item_prototypes[k] then
self.valid = false
return false
end
end
self.valid = true
return true
end
---Returns resource categories for a blueprint
---@return table<string, boolean>
function bp_meta:get_resource_categories()
local categories = {}
local proto = game.entity_prototypes[self.miner_name]
if proto.resource_categories then
for cat, bool in pairs(proto.resource_categories) do
categories[cat] = bool
end
end
return categories
end
---@return table<string, GridBuilding>
function bp_meta:get_entity_categories()
---@type table<string, GridBuilding>
local categories = {
["mining-drill"] = "miner",
["beacon"] = "beacon",
["transport-belt"] = "belt",
["underground-belt"] = "belt",
["splitter"] = "belt",
["inserter"] = "inserter",
["container"] = "container",
["logistic-container"] = "container",
["loader"] = "inserter",
["loader-1x1"] = "inserter",
["electric-pole"] = "pole",
}
---@type table<string, GridBuilding>
local category_map = {}
for _, ent in pairs(self.entities) do
local name = ent.name
local category = category_map[name]
if not category then
local ent_type = game.entity_prototypes[name].type
category = categories[ent_type] or "other"
category_map[name] = category
end
end
return category_map
end
return bp_meta

View File

@@ -0,0 +1,65 @@
local mpp_util = require("mpp.mpp_util")
local coord_revert_world = mpp_util.revert_world
local builder = {}
---@class GhostSpecification : LuaSurface.create_entity_param.entity_ghost
---@field grid_x number Grid x coordinate
---@field grid_y number Grid x coordinate
---@field radius number? Object radius or default to 0.5 if nil
---@field extent_w number? Object extent from origin, converted from radius if nil
---@field extent_h number? Object extent from origin, converted from radius if nil
---@field thing GridBuilding Enum for the grid
---@field items table<string, number>? Item requests
---@class PowerPoleGhostSpecification : GhostSpecification
---@field no_light boolean
---@field ix number
---@field iy number
--- Builder for a convenience function that automatically translates
--- internal grid state for a surface.create_entity call
---@param state State
---@return fun(ghost: GhostSpecification, check_allowed: boolean?): LuaEntity?
function builder.create_entity_builder(state)
local c = state.coords
local grid = state.grid
local DIR = state.direction_choice
local surface = state.surface
local gx, gy, tw, th = c.gx, c.gy, c.tw, c.th
local direction_conv = mpp_util.bp_direction[state.direction_choice]
local collected_ghosts = state._collected_ghosts
local is_space = state.is_space
return function(ghost, check_allowed)
ghost.raise_built = true
ghost.player = state.player
ghost.force = state.player.force
ghost.inner_name=ghost.name
ghost.name="entity-ghost"
ghost.position=coord_revert_world(gx, gy, DIR, ghost.grid_x, ghost.grid_y, tw, th)
ghost.direction=direction_conv[ghost.direction or defines.direction.north]
--local can_place = surface.can_place_entity(ghost)
if check_allowed and not surface.can_place_entity{
name = ghost.inner_name,
-- name = "entity-ghost",
-- inner_name = ghost.inner_name,
force = state.player.force,
position = ghost.position,
direction = ghost.direction,
build_check_type = defines.build_check_type.blueprint_ghost,
forced = true,
} then
return
end
local result = surface.create_entity(ghost)
if result then
grid:build_specification(ghost)
collected_ghosts[#collected_ghosts+1] = result
end
return result
end
end
return builder

View File

@@ -0,0 +1,176 @@
local sin, cos, pi = math.sin, math.cos, math.pi
local min, max = math.min, math.max
local function clamp(x, a, b)
return max(min(x, b or 1), a or 0)
end
local color = {}
-- OKLAB, OKHSV conversion functions yoinked from Björn Ottosson
-- https://bottosson.github.io/posts/colorpicker/
function color.oklab_to_linear_srgb(L, a, b)
local l_ = L + 0.3963377774 * a + 0.2158037573 * b;
local m_ = L - 0.1055613458 * a - 0.0638541728 * b;
local s_ = L - 0.0894841775 * a - 1.2914855480 * b;
local l = l_*l_*l_;
local m = m_*m_*m_;
local s = s_*s_*s_;
return
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
end
-- Finds the maximum saturation possible for a given hue that fits in sRGB
-- Saturation here is defined as S = C/L
-- a and b must be normalized so a^2 + b^2 == 1
local function compute_max_saturation(a, b)
-- Max saturation will be when one of r, g or b goes below zero.
-- Select different coefficients depending on which component goes below zero first
local k0, k1, k2, k3, k4, wl, wm, ws;
if -1.88170328 * a - 0.80936493 * b > 1 then
-- Red component
k0 = 1.19086277; k1 = 1.76576728; k2 = 0.59662641; k3 = 0.75515197; k4 = 0.56771245;
wl = 4.0767416621; wm = -3.3077115913; ws = 0.2309699292;
elseif 1.81444104 * a - 1.19445276 * b > 1 then
-- Green component
k0 = 0.73956515; k1 = -0.45954404; k2 = 0.08285427; k3 = 0.12541070; k4 = 0.14503204;
wl = -1.2684380046; wm = 2.6097574011; ws = -0.3413193965;
else
-- Blue component
k0 = 1.35733652; k1 = -0.00915799; k2 = -1.15130210; k3 = -0.50559606; k4 = 0.00692167;
wl = -0.0041960863; wm = -0.7034186147; ws = 1.7076147010;
end
-- Approximate max saturation using a polynomial:
local S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b;
-- Do one step Halley's method to get closer
-- this gives an error less than 10e6, except for some blue hues where the dS/dh is close to infinite
-- this should be sufficient for most applications, otherwise do two/three steps
local k_l = 0.3963377774 * a + 0.2158037573 * b;
local k_m = -0.1055613458 * a - 0.0638541728 * b;
local k_s = -0.0894841775 * a - 1.2914855480 * b;
do
local l_ = 1 + S * k_l;
local m_ = 1 + S * k_m;
local s_ = 1 + S * k_s;
local l = l_ * l_ * l_;
local m = m_ * m_ * m_;
local s = s_ * s_ * s_;
local l_dS = 3 * k_l * l_ * l_;
local m_dS = 3 * k_m * m_ * m_;
local s_dS = 3 * k_s * s_ * s_;
local l_dS2 = 6 * k_l * k_l * l_;
local m_dS2 = 6 * k_m * k_m * m_;
local s_dS2 = 6 * k_s * k_s * s_;
local f = wl * l + wm * m + ws * s;
local f1 = wl * l_dS + wm * m_dS + ws * s_dS;
local f2 = wl * l_dS2 + wm * m_dS2 + ws * s_dS2;
S = S - f * f1 / (f1*f1 - 0.5 * f * f2);
end
return S
end
function find_cusp(a, b)
-- First, find the maximum saturation (saturation S = C/L)
local S_cusp = compute_max_saturation(a, b);
-- Convert to linear sRGB to find the first point where at least one of r,g or b >= 1:
local rgb_at_max = {color.oklab_to_linear_srgb(1, S_cusp * a, S_cusp * b)}
local L_cusp = (1 / max(max(rgb_at_max[1], rgb_at_max[2]), rgb_at_max[3])) ^ (1/3)
local C_cusp = L_cusp * S_cusp;
return L_cusp , C_cusp
end
local function get_ST_max(a_, b_, cusp)
if not cusp then
cusp = {find_cusp(a_, b_)}
end
local L = cusp[1];
local C = cusp[2];
return C/L, C/(1-L)
end
local function toe_inv(x)
local k_1 = 0.206
local k_2 = 0.03
local k_3 = (1+k_1)/(1+k_2)
return (x*x + k_1*x)/(k_3*(x+k_2))
end
local function srgb_transfer_function(value)
-- if value <= 0.04045 then
-- return value / 12.92
-- end
-- return ((value + 0.055) / 1.055) ^ 2.4;
return .0031308 >= value and (12.92 * value) or (1.055 * math.pow(value, .4166666666666667) - .055)
end
function color.linear_srgb_to_rgb(sr, sg, sb)
return
srgb_transfer_function(sr),
srgb_transfer_function(sg),
srgb_transfer_function(sb)
end
function color.okhsv_to_srgb(h, s, v)
local a_ = cos(2*pi*h)
local b_ = sin(2*pi*h)
local ST_max = {get_ST_max(a_,b_)};
local S_max = ST_max[1];
local S_0 = 0.5;
local T = ST_max[2]
local k = 1 - S_0/S_max;
local L_v = 1 - s*S_0/(S_0+T - T*k*s)
local C_v = s*T*S_0/(S_0+T-T*k*s)
local L = v*L_v;
local C = v*C_v;
local L_vt = toe_inv(L_v);
local C_vt = C_v * L_vt/L_v;
local L_new = toe_inv(L); -- * L_v/L_vt;
C = C * L_new/L;
L = L_new;
local rgb_scale = {color.oklab_to_linear_srgb(L_vt,a_*C_vt,b_*C_vt)};
local scale_L = (1/(max(rgb_scale[1],rgb_scale[2],rgb_scale[3],0))) ^ (1/3)
L = L * scale_L;
C = C * scale_L;
--local rgb = color.oklab_to_linear_srgb(L, C*a_, C*b_);
-- apply srgb transfer function
return color.oklab_to_linear_srgb(L, C*a_, C*b_)
end
function color.hue_sequence(n)
local r,g,b = color.okhsv_to_srgb((n * math.phi) % 1, 1, 1)
return {clamp(r), clamp(g), clamp(b)}
--return {r, g, b}
end
return color

View File

@@ -0,0 +1,131 @@
local enum = require("mpp.enums")
local compatibility = {}
--[[----------------------------------------------------------------------------
Space Exploration
----------------------------------------------------------------------------]]--
local space_exploration_active = nil
---@return boolean
compatibility.is_space_exploration_active = function()
if space_exploration_active == nil then
space_exploration_active = game.active_mods["space-exploration"] and true or false
end
return space_exploration_active
end
--- @class SERemoteViewToggledEventData: EventData
--- @field player_index uint
-- Thanks Raiguard
compatibility.get_se_events = function()
local se, events = remote.interfaces["space-exploration"], {}
if not se then return events end
if se.get_on_remote_view_started_event then
events["get_on_remote_view_started_event"] = remote.call("space-exploration", "get_on_remote_view_started_event")
end
if se.get_on_remote_view_stopped_event then
events["get_on_remote_view_stopped_event"] = remote.call("space-exploration", "get_on_remote_view_stopped_event")
end
return events
end
local memoize_space_surfaces = {}
--- Wrapper for Space Exploration get_zone_is_space remote interface calls
---@param surface_identification SurfaceIdentification
---@return boolean
compatibility.is_space = function(surface_identification)
local surface_index = surface_identification
if type(surface_identification) == "string" then
surface_identification = game.get_surface(surface_identification).index
elseif type(surface_identification) == "userdata" then
---@cast surface_identification LuaSurface
surface_identification = surface_identification.index
end
local memoized = memoize_space_surfaces[surface_index]
if memoized ~= nil then return memoized end
if game.active_mods["space-exploration"] then
local zone = remote.call("space-exploration", "get_zone_from_surface_index", {surface_index = surface_index} --[[@as table]]) --[[@as LuaSurface?]]
if not zone then
memoize_space_surfaces[surface_index] = false
return false
end
local result = remote.call("space-exploration", "get_zone_is_space", {zone_index = zone.index} --[[@as table]]) --[[@as boolean]]
memoize_space_surfaces[surface_index] = result
return result
end
memoize_space_surfaces[surface_index] = false
return false
end
---@type string
local space_collision_mask_name = nil
---@type table<string, boolean>
local memoize_space_buildable = {}
function compatibility.is_buildable_in_space(name)
local buildable_status = memoize_space_buildable[name]
if buildable_status ~= nil then
return buildable_status
end
-- if something goes wrong, give up, allow to build
if space_collision_mask_name == nil then
local scaffold_tile = game.tile_prototypes["se-space-platform-scaffold"]
if not scaffold_tile then
memoize_space_buildable[name] = true
return true
end
space_collision_mask_name = next(scaffold_tile.collision_mask)
if space_collision_mask_name == nil then
memoize_space_buildable[name] = true
return true
end
end
local entity_proto = game.entity_prototypes[name]
local allowed = not entity_proto.collision_mask[space_collision_mask_name]
memoize_space_buildable[name] = allowed
return allowed
end
--- Return true to skip non space item
---@param is_space boolean
---@param protype LuaEntityPrototype
---@return boolean
compatibility.guess_space_item = function(is_space, protype)
if not is_space then return false end
return string.match(protype.name, "^se%-")
end
--[[----------------------------------------------------------------------------
Pyanodons
----------------------------------------------------------------------------]]--
local pyanodons_active = nil
---@return boolean
compatibility.is_pyanodons_active = function()
if pyanodons_active == nil then
local active_mods = game.active_mods
-- pyanodons_active = game.active_mods["space-exploration"] and true or false
for k, v in ipairs{"pyrawores", "pycoalprocessing", "pyalienlife"} do
if active_mods[v] then
pyanodons_active = true
break
end
end
pyanodons_active = false
end
return pyanodons_active
end
return compatibility

View File

@@ -0,0 +1,143 @@
-- debug rendering library
local table_insert = table.insert
---@class DebugRendering
---@field _state State
---@field _enabled boolean Does drawing do anything
---@field _register boolean Add to render objects to be removed
local drawing_meta = {}
drawing_meta.__index = drawing_meta
function drawing_meta:draw_circle(t)
if not self._enabled then return end
local state = self._state --[[@as State]]
local C = state.coords
local x, y
if t.integer then
x, y = C.ix1, C.iy1
else
x, y = C.gx, C.gy
end
local tx, ty = t.x, t.y
t.surface = state.surface
t.players = {state.player}
t.width = t.width or 3
t.color = t.color or {1, 1, 1}
t.radius = t.radius or 0.5
t.target = { x + tx, y + ty }
local id = rendering.draw_circle(t)
if self._register then
table_insert(state._render_objects, id)
end
end
function drawing_meta:draw_line(t)
if not self._enabled then return end
local state = self._state --[[@as State]]
local C = state.coords
local x, y
if t.integer then
x, y = C.ix1, C.iy1
else
x, y = C.gx, C.gy
end
local tx, ty = t.x or t.x1, t.y or t.y1
local target1 = {x + tx, y + ty}
local target2
if t.x2 and t.y2 then
target2 = {x + t.x2, y + t.y2}
elseif t.w and t.h then
target2 = {x + tx + (t.w or 0), y + ty + (t.h or 0)}
else
return
end
t.surface = state.surface
t.players = {state.player}
t.width = t.width or 3
t.color = t.color or {1, 1, 1}
t.from = t.from or target1
t.to = t.to or target2
local id = rendering.draw_line(t)
if self._register then
table_insert(state._render_objects, id)
end
end
function drawing_meta:draw_rectangle(t)
if not self._enabled then return end
local state = self._state --[[@as State]]
local C = state.coords
local x, y
if t.integer then
x, y = C.ix1, C.iy1
else
x, y = C.gx, C.gy
end
local tx, ty = t.x or t.x1, t.y or t.y1
local target1 = {x + tx, y + ty}
local target2
if t.x2 and t.y2 then
target2 = {x + t.x2, y + t.y2}
elseif t.w and t.h then
target2 = {x + tx + (t.w or 0), y + ty + (t.h or 0)}
else
return
end
t.surface = state.surface
t.players = {state.player}
t.width = t.width or 3
t.color = t.color or {1, 1, 1}
t.left_top = t.left_top or target1
t.right_bottom = t.right_bottom or target2
local id = rendering.draw_rectangle(t)
if self._register then
table_insert(state._render_objects, id)
end
end
function drawing_meta:draw_text(t)
if not self._enabled then return end
local state = self._state --[[@as State]]
local C = state.coords
local x, y
if t.integer then
x, y = C.ix1, C.iy1
else
x, y = C.gx, C.gy
end
local tx, ty = t.x, t.y
t.surface = state.surface
t.players = {state.player}
t.width = t.width or 3
t.color = t.color or {1, 1, 1}
t.target = { x + tx, y + ty }
t.scale = t.scale or 1
t.alignment = t.alignment or "center"
t.vertical_alignment = t.vertical_alignment or "middle"
local id = rendering.draw_text(t)
if self._register then
table_insert(state._render_objects, id)
end
end
---comment
---@param state State
---@param enabled boolean Enable drawing
local function drawing(state, enabled, register)
return setmetatable({_state=state, _enabled=(not not enabled), _register=(not not register)}, drawing_meta)
end
return drawing

View File

@@ -0,0 +1,134 @@
local enums = {}
---@type LuaEntityPrototype[]
local cached_miners = {}
---@type table<string, boolean>
local cached_resource_categories = {}
local invalid_resource = { --fluid or otherwise
["se-core-mining"] = true,
}
local miner_blacklist = {
["se-core-miner-drill"] = true
}
function enums.get_default_miner()
if game.active_mods["nullius"] then
return "nullius-medium-miner-1"
end
return "electric-mining-drill"
end
---Get mining drills and resource categories
---@return LuaEntityPrototype[], table
function enums.get_available_miners()
enums.get_available_miners = function() return cached_miners, cached_resource_categories end
local all_miners = game.get_filtered_entity_prototypes{{filter="type", type="mining-drill"}}
---@type table<string, LuaEntityPrototype>
--local all_fluids = game.get_filtered_item_prototypes({filter="type", type="
local all_resources = game.get_filtered_entity_prototypes{{filter="type", type="resource"}}
---@type table<string, LuaResourceCategoryPrototype>
for name, proto in pairs(all_resources) do
---@cast proto LuaEntityPrototype
local mineable_properties = proto.mineable_properties
if mineable_properties.products then
for _, product in ipairs(mineable_properties.products) do
if product.type == "fluid" then
invalid_resource[name] = true
break
end
end
else
invalid_resource[name] = true
break
end
end
for miner_name, miner_proto in pairs(all_miners) do
if miner_blacklist[miner_name] then goto continue_miner end
if miner_proto.resource_categories then
for resource_cat, bool in pairs(miner_proto.resource_categories) do
if invalid_resource[resource_cat] then
miner_blacklist[miner_name] = true
end
end
else
miner_blacklist[miner_name] = true
goto continue_miner
end
---@cast miner_proto LuaEntityPrototype
local fluidboxes = miner_proto.fluidbox_prototypes
for _, fluidbox in pairs(fluidboxes) do
---@cast fluidbox LuaFluidBoxPrototype
if fluidbox.production_type == "output" then
miner_blacklist[miner_name] = true
end
end
::continue_miner::
end
if game.active_mods["Cursed-FMD"] then
local mangled_categories = {}
local miners = {}
for name, proto in pairs(all_miners) do
if string.find(name, ";") then -- Cursed-FMD hack
for resource_name, _ in pairs(proto.resource_categories) do
if not invalid_resource[resource_name] and not string.find(resource_name, "core-fragment") then
mangled_categories[resource_name] = true
end
end
else
if proto.flags and proto.flags.hidden then goto continue_miner end
if miner_blacklist[name] then goto continue_miner end
for resource_name, _ in pairs(proto.resource_categories) do
if not invalid_resource[resource_name] and not string.find(resource_name, "core-fragment") then
miners[name] = proto
end
end
end
::continue_miner::
end
cached_miners = miners
cached_resource_categories = mangled_categories
else
local miners = {}
local resource_categories = {
["basic-solid"] = true,
["hard-resource"] = true,
}
for name, proto in pairs(all_miners) do
if proto.flags and proto.flags.hidden then goto continue_miner end
if miner_blacklist[name] then goto continue_miner end
--if not proto.resource_categories["basic-solid"] then goto continue_miner end
for resource_category, bool in pairs(proto.resource_categories) do
resource_categories[resource_category] = bool
end
miners[name] = proto
::continue_miner::
end
cached_miners = miners
cached_resource_categories = resource_categories
--[[ {
["basic-solid"] = true,
["hard-resource"] = true,
}]]
end
return enums.get_available_miners()
end
enums.space_surfaces = {
["asteroid-belt"] = true,
["asteroid-field"] = true,
["orbit"] = true,
["anomaly"] = true,
}
return enums

View File

@@ -0,0 +1,45 @@
local table_insert = table.insert
List = require("mpp.list")
math.phi = 1.618033988749894
---Filters out a list
---@param t any
---@param func any
function table.filter(t, func)
local new = {}
for k, v in ipairs(t) do
if func(v) then new[#new+1] = v end
end
return new
end
function table.map(t, func)
local new = {}
for k, v in pairs(t) do
new[k] = func(v)
end
return new
end
function table.mapkey(t, func)
local new = {}
for k, v in pairs(t) do
new[func(v)] = v
end
return new
end
---Appends a list to the target table
---@param target table
---@param other any[]
function table.append(target, other)
for _, value in pairs(other) do
table_insert(target, value)
end
end
function math.divmod(a, b)
return math.floor(a / b), a % b
end

View File

@@ -0,0 +1,385 @@
local floor, ceil, min, max = math.floor, math.ceil, math.min, math.max
local mpp_util = require("mpp.mpp_util")
---@class GridRow: table<number, GridTile>
---@field [number] GridTile
---@class Grid
---@field [number] GridRow
local grid_mt = {}
grid_mt.__index = grid_mt
---Coordinate aggregate of the resource patch
---@class Coords
---@field x1 double Top left corner of the patch
---@field y1 double Top left corner of the patch
---@field x2 double Bottom right corner of the patch
---@field y2 double Bottom right corner of the patch
---@field ix1 number Integer top left corner of the patch
---@field iy1 number Integer top left corner of the patch
---@field ix2 number Integer bottom right corner
---@field iy2 number Integer bottom right corner
---@field w integer Width of the patch
---@field h integer Height of the patch
---@field tw integer Width Rotation invariant width
---@field th integer Height Rotation invariant height
---@field gx double x1 but -1 for grid rendering
---@field gy double y1 but -1 for grid rendering
---@field extent_x1 number Internal grid dimensions
---@field extent_y1 number Internal grid dimensions
---@field extent_x2 number Internal grid dimensions
---@field extent_y2 number Internal grid dimensions
---@alias GridBuilding
---| nil
---| "miner"
---| "pole"
---| "beacon"
---| "belt"
---| "inserter"
---| "container"
---| "lamp"
---| "other"
local need_electricity = {
miner = true,
beacon = true,
inserter = true
}
---@class GridTile
---@field amount number Amount of resource on tile
---@field neighbor_amount number Total resource sum of neighbors (performance killer?)
---@field neighbors_inner number Physical drill coverage
---@field neighbors_outer number Drill radius coverage
---@field x integer
---@field y integer
---@field gx double actual coordinate in surface
---@field gy double actual coordinate in surface
---@field built_on GridBuilding Is tile occupied by a building entity
---@field consumed boolean Is a miner consuming this tile
---@class BlueprintGridTile : GridTile
---@field neighbor_counts table<number, number>
---@field neighbors_inner nil
---@field neighbors_outer nil
---comment
---@param x integer Grid coordinate
---@param y integer Grid coordinate
---@return GridTile|nil
function grid_mt:get_tile(x, y)
local row = self[y]
if row then return row[x] end
end
---Convolves resource count for a grid cell
---For usage in blueprint layouts
---@param ox any
---@param oy any
---@param size any
function grid_mt:convolve(ox, oy, size)
local nx1, nx2 = ox, ox+size - 1
local ny1, ny2 = oy, oy+size - 1
for y = ny1, ny2 do
---@type table<number, BlueprintGridTile>
local row = self[y]
if row == nil then goto continue_row end
for x = nx1, nx2 do
local tile = row[x]
if tile == nil then goto continue_column end
local counts = tile.neighbor_counts
counts[size] = counts[size] + 1
::continue_column::
end
::continue_row::
end
end
---Convolves resource count for a grid cell
---@param ox any
---@param oy any
---@param size any
function grid_mt:convolve_inner(ox, oy, size)
local nx1, nx2 = ox, ox+size - 1
local ny1, ny2 = oy, oy+size - 1
for y = ny1, ny2 do
---@type table<number, GridTile>
local row = self[y]
if row == nil then goto continue_row end
for x = nx1, nx2 do
local tile = row[x]
if tile == nil then goto continue_column end
tile.neighbors_inner = tile.neighbors_inner + 1
::continue_column::
end
::continue_row::
end
end
---Convolves resource count for a grid cell
---@param ox any
---@param oy any
---@param size any
function grid_mt:convolve_outer(ox, oy, size, amount)
local nx1, nx2 = ox, ox+size - 1
local ny1, ny2 = oy, oy+size - 1
for y = ny1, ny2 do
---@type table<number, GridTile>
local row = self[y]
if row == nil then goto continue_row end
for x = nx1, nx2 do
local tile = row[x]
if tile == nil then goto continue_column end
tile.neighbors_outer = tile.neighbors_outer + 1
tile.neighbor_amount = tile.neighbor_amount + amount
::continue_column::
end
::continue_row::
end
end
---@param ox number
---@param oy number
---@param drill MinerStruct
function grid_mt:convolve_miner(ox, oy, drill)
local x1, y1 = ox-drill.extent_positive, ox-drill.extent_positive
local x2, y2 = x1+drill.area, y1+drill.area
local ix1, iy1 = ox-drill.size+1, oy-drill.size+1
local ix2, iy2 = x1+drill.size, y1+drill.size
for y = y1, y2 do
local row = self[y]
if row then
for x = x1, x2 do
---@type GridTile
local tile = row[x]
if tile then
tile.neighbors_outer = tile.neighbors_outer + 1
if ix1 < x and x < ix2 and iy1 < y and y < iy2 then
tile.neighbors_outer = tile.neighbors_outer + 1
end
end
end
end
end
end
---Marks tiles (with resources) as consumed by a mining drill
---@param ox integer
---@param oy integer
function grid_mt:consume(ox, oy, size)
local nx1, nx2 = ox, ox+size - 1
local ny1, ny2 = oy, oy+size - 1
for y = ny1, ny2 do
local row = self[y]
if row == nil then goto continue_row end
for x = nx1, nx2 do
local tile = row[x]
if tile and tile.amount > 0 then
tile.consumed = true
end
end
::continue_row::
end
end
---@param cx number
---@param cy number
---@param w number
---@param evenw boolean
---@param evenh boolean
---@deprecated
function grid_mt:consume_custom(cx, cy, w, evenw, evenh)
local ox, oy = evenw and 1 or 0, evenh and 1 or 0
local x1, x2 = cx+ox-w, cx+w
for y = cy+oy-w, cy+w do
local row = self[y]
if row == nil then goto continue_row end
for x = x1, x2 do
local tile = row[x]
if tile and tile.amount then
tile.consumed = true
end
end
::continue_row::
end
end
---Marks tiles as consumed by a miner
---@param tiles GridTile[]
function grid_mt:clear_consumed(tiles)
for _, tile in pairs(tiles) do
---@cast tile GridTile
tile.consumed = false
end
end
---Builder function
---@param cx number x coord
---@param cy number y coord
---@param size_w number
---@param thing GridBuilding Type of building
function grid_mt:build_thing(cx, cy, thing, size_w, size_h)
size_h = size_h or size_w
for y = cy, cy + size_h do
local row = self[y]
if row == nil then goto continue_row end
for x = cx, cx + size_w do
local tile = row[x]
if tile then
tile.built_on = thing
end
end
::continue_row::
end
end
function grid_mt:build_thing_simple(cx, cy, thing)
local row = self[cy]
if row then
local tile = row[cx]
if tile then
tile.built_on = thing
return true
end
end
end
---@param t GhostSpecification
function grid_mt:build_specification(t)
local cx, cy = t.grid_x, t.grid_y
local left, right = t.padding_pre, t.padding_post
local thing = t.thing
if left == nil and right == nil then
local row = self[cy]
if row then
local tile = row[cx]
if tile then
tile.built_on = thing
end
end
else
left, right = left or 0, right or 0
local x1, x2 = cx-left, cx+right
for y = cy-left, cy+right do
local row = self[y]
if row == nil then goto continue_row end
for x = x1, x2 do
local tile = row[x]
if tile then
tile.built_on = thing
end
end
::continue_row::
end
end
end
---Finds if an entity type is built near
---@param cx number x coord
---@param cy number y coord
---@param thing GridBuilding Type of building
---@return boolean
function grid_mt:find_thing(cx, cy, thing, size)
local x1, x2 = cx, cx + size
for y = cy, cy+size do
local row = self[y]
if row == nil then goto continue_row end
for x = x1, x2 do
local tile = row[x]
if tile and tile.built_on == thing then
return true
end
end
::continue_row::
end
return false
end
---Finds if an entity type is built near
---@param cx number x coord
---@param cy number y coord
---@param things table<string, true> Types of entities
---@param r number Radius
---@param even boolean Is even width building
---@return boolean
function grid_mt:find_thing_in(cx, cy, things, r, even)
things = mpp_util.list_to_keys(things)
local o = even and 1 or 0
for y = cy+o-r, cy+r do
local row = self[y]
if row == nil then goto continue_row end
for x = cx+o-r, cx+r do
local tile = row[x]
if tile and things[tile.built_on] then
return true
end
end
::continue_row::
end
return false
end
function grid_mt:build_miner(cx, cy, size)
self:build_thing(cx, cy, "miner", size, size)
end
function grid_mt:get_unconsumed(ox, oy, size)
local nx1, nx2 = ox, ox+size - 1
local ny1, ny2 = oy, oy+size - 1
local count = 0
for y = ny1, ny2 do
local row = self[y]
if row == nil then goto continue_row end
for x = nx1, nx2 do
local tile = row[x]
if tile and tile.amount > 0 and not tile.consumed then
count = count + 1
end
end
::continue_row::
end
return count
end
---@param mx number
---@param my number
---@param pole number|PoleStruct
---@return boolean
function grid_mt:needs_power(mx, my, pole)
local nx1, nx2, ny1, ny2
if type(pole) == "table" then
local size = pole.size
local extent = ceil((pole.supply_width-size) / 2)
nx1, nx2 = mx - extent, mx + extent
ny1, ny2 = my - extent, my + extent
else
nx1, nx2 = mx, mx+pole-1
ny1, ny2 = my, my+pole-1
end
for y = ny1, ny2 do
local row = self[y]
if row == nil then goto continue_row end
for x = nx1, nx2 do
---@type GridTile
local tile = row[x]
if tile and need_electricity[tile.built_on] then
return true
end
end
::continue_row::
end
return false
end
return grid_mt

View File

@@ -0,0 +1,34 @@
local table_insert = table.insert
---@class List
local list_mt = {}
list_mt.__index = list_mt
function list_mt:push(value)
table_insert(self, value)
return self
end
function list_mt:unshift(value)
table_insert(self, 1, value)
return self
end
function list_mt:append(...)
for _, value in pairs({...}) do
table_insert(self, value)
end
return self
end
function list_mt:contitional_append(check, ...)
if not check then return self end
for _, value in pairs({...}) do
table_insert(self, value)
end
return self
end
return function(t)
return setmetatable(t or {}, list_mt)
end

View File

@@ -0,0 +1,765 @@
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

View File

@@ -0,0 +1,236 @@
local table_insert = table.insert
local min, max = math.min, math.max
-- broke: implement power pole connection calculation yourself
-- woke: place ghosts to make factorio calculate the connections
---@class PowerPoleGrid
---@field [number] table<number, GridPole>
local pole_grid_mt = {}
pole_grid_mt.__index = pole_grid_mt
---@class GridPole
---@field ix number Position in the pole grid
---@field iy number Position in the pole grid
---@field grid_x number Position in the full grid
---@field grid_y number Position in the full grid
---@field built boolean? Does the pole need to be built
---@field entity LuaEntity? Pole ghost LuaEntity
---@field has_consumers boolean Does pole cover any powered items
---@field backtracked boolean
---@field connections table<GridPole, true>
---@field set_id number?
---@field no_light boolean?
function pole_grid_mt.new()
local new = {
_max_x = 1,
_max_y = 1,
}
return setmetatable(new, pole_grid_mt)
end
---@param x number
---@param y number
---@param p GridPole
function pole_grid_mt:set_pole(x, y, p)
if p.connections == nil then p.connections = {} end
if not self[y] then self[y] = {} end
self._max_x = max(self._max_x, x)
self._max_y = max(self._max_y, y)
self[y][x] = p
end
---@param p GridPole
function pole_grid_mt:add_pole(p)
self:set_pole(p.ix, p.iy, p)
end
---@param x number
---@param y number
---@return GridPole | nil
function pole_grid_mt:get_pole(x, y)
if self[y] then return self[y][x] end
end
---@param p1 GridPole
---@param p2 GridPole
---@param struct PoleStruct
---@return boolean
function pole_grid_mt:pole_reaches(p1, p2, struct)
local x, y = p1.grid_x - p2.grid_x, p1.grid_y - p2.grid_y
return (x * x + y * y) ^ 0.5 <= (struct.wire)
end
---@param P PoleStruct
---@return table<number, table<GridPole, true>>
function pole_grid_mt:find_connectivity(P)
-- this went off the rails
local all_poles = {}
---@type table<GridPole, true>
local not_visited = {}
-- Make connections
for y1 = 1, #self do
local row = self[y1]
for x1 = 1, #row do
local pole = row[x1]
if pole == nil then goto continue end
table.insert(all_poles, pole)
local right = self:get_pole(x1+1, y1)
local bottom = self:get_pole(x1, y1+1)
if right and self:pole_reaches(pole, right, P) then
pole.connections[right], right.connections[pole] = true, true
end
if bottom and self:pole_reaches(pole, bottom, P) then
pole.connections[bottom], bottom.connections[pole] = true, true
end
::continue::
end
end
-- Process network connection sets
local unconnected = {}
---@type number, table<GridPole, true>
local set_id, current_set = 1, {}
---@type PoleConnectivity
local sets = {[0]=unconnected, [1]=current_set}
for _, v in pairs(all_poles) do not_visited[v] = true end
---@param pole GridPole
---@return number?
local function is_continuation(pole)
for other, _ in pairs(pole.connections) do
if other.set_id then
return other.set_id
end
end
end
---@param start_pole GridPole
local function iterate_connections(start_pole)
local continuation = is_continuation(start_pole)
if not continuation and table_size(current_set) > 0 then
if sets[set_id] == nil then
sets[set_id] = current_set
end
current_set, set_id = {}, set_id + 1
end
---@param pole GridPole
---@param depth_remaining number
local function recurse_pole(pole, depth_remaining)
not_visited[start_pole] = nil
if not pole.has_consumers then
unconnected[pole] = true
return
end
if pole.set_id and pole.set_id < set_id then
--Encountered a different set, merge to it
local target_set_id = pole.set_id
local target_set = sets[target_set_id]
for current_pole, v in pairs(current_set) do
current_set[current_pole], target_set[current_pole] = nil, true
current_pole.set_id = target_set_id
end
set_id = pole.set_id
current_set = target_set
else
pole.set_id = set_id
end
current_set[pole] = true
for other, _ in pairs(pole.connections) do
local other_not_visited = not_visited[other]
if other_not_visited and depth_remaining > 0 then
recurse_pole(other, depth_remaining-1)
end
end
end
recurse_pole(start_pole, 5)
end
local remaining = next(not_visited)
while remaining do
iterate_connections(remaining)
remaining = next(not_visited)
end
sets[set_id] = current_set
return sets
end
---List of sets
---set at index 0 are unconnected power poles
---@alias PoleConnectivity table<number, table<GridPole, true>>
---@param connectivity PoleConnectivity
---@return GridPole[]
function pole_grid_mt:ensure_connectivity(connectivity)
---@type GridPole[]
local connected = {}
local unconnecteds_set, main_set = connectivity[0], connectivity[1]
-- connect trivial power poles
for connector_pole in pairs(unconnecteds_set) do
--Set id is key and value is merge marker
local different_sets = {} --[[@as table<number, boolean>]]
for connection in pairs(connector_pole.connections) do
if connection.set_id ~= nil then
different_sets[connection.set_id] = true
end
end
if table_size(different_sets) < 2 then goto skip_pole end
local target_set_id = different_sets[1] and 1 or next(different_sets)
local target_set = connectivity[target_set_id]
for source_set_id in pairs(different_sets) do
if source_set_id == target_set_id then goto skip_merge_set end
local source_set = connectivity[source_set_id]
if different_sets[source_set_id] == true then
for source_pole in pairs(source_set) do
target_set[source_pole], source_set[source_pole] = true, nil
source_pole.set_id = target_set_id
different_sets[source_set_id] = false
end
end
::skip_merge_set::
end
connector_pole.built = true
connector_pole.set_id = target_set_id
target_set[connector_pole] = true
unconnecteds_set[connector_pole] = nil
::skip_pole::
end
---- append the main set to output list
--for pole in pairs(main_set) do table_insert(connected, pole) end
for set_id, pole_set in pairs(connectivity) do
if set_id == 0 then goto continue_set end
--if set_id == 1 then goto continue_set end
for pole in pairs(pole_set) do table_insert(connected, pole) end
::continue_set::
end
return connected
end
return pole_grid_mt

View File

@@ -0,0 +1,788 @@
local mpp_util = require("mpp.mpp_util")
local color = require("mpp.color")
local floor, ceil = math.floor, math.ceil
local min, max = math.min, math.max
local EAST, NORTH, SOUTH, WEST = mpp_util.directions()
local render_util = {}
---@class RendererParams
---@field origin MapPosition?
---@field target MapPosition?
---@field x number?
---@field y number?
---@field w number?
---@field h number?
---@field r number?
---@field color Color?
---@field width number?
---@field c Color?
---@field left_top MapPosition?
---@field right_bottom MapPosition?
---this went off the rails
---@param event EventData.on_player_reverse_selected_area
---@return MppRendering
function render_util.renderer(event)
---@param t RendererParams
local function parametrizer(t, overlay)
for k, v in pairs(overlay or {}) do t[k] = v end
if t.x and t.y then t.origin = {t.x, t.y} end
local target = t.origin or t.left_top --[[@as MapPosition]]
local left_top, right_bottom = t.left_top or t.origin or target, t.right_bottom or t.origin
if t.origin and t.w or t.h then
t.w, t.h = t.w or t.h, t.h or t.w
right_bottom = {(target[1] or target.x) + t.w, (target[2] or target.y) + t.h}
elseif t.r then
local r = t.r
local ox, oy = target[1] or target.x, target[2] or target.y
left_top = {ox-r, oy-r}
right_bottom = {ox+r, oy+r}
end
local new = {
surface = event.surface,
players = {event.player_index},
filled = false,
radius = t.r or 1,
color = t.c or t.color or {1, 1, 1},
left_top = left_top,
right_bottom = right_bottom,
target = target, -- circles
from = left_top,
to = right_bottom, -- lines
width = 1,
}
for k, v in pairs(t) do new[k]=v end
for _, v in ipairs{"x", "y", "h", "w", "r", "origin"} do new[v]=nil end
return new
end
local meta_renderer_meta = {}
meta_renderer_meta.__index = function(self, k)
return function(t, t2)
return {
rendering[k](
parametrizer(t, t2)
)
}
end end
local rendering = setmetatable({}, meta_renderer_meta)
---@class MppRendering
local rendering_extension = {}
---Draws an x between left_top and right_bottom
---@param params RendererParams
function rendering_extension.draw_cross(params)
rendering.draw_line(params)
rendering.draw_line({
width = params.width,
color = params.color,
left_top={
params.right_bottom[1],
params.left_top[2]
},
right_bottom={
params.left_top[1],
params.right_bottom[2],
}
})
end
function rendering_extension.draw_rectangle_dashed(params)
rendering.draw_line(params, {
from={params.left_top[1], params.left_top[2]},
to={params.right_bottom[1], params.left_top[2]},
dash_offset = 0.0,
})
rendering.draw_line(params, {
from={params.left_top[1], params.right_bottom[2]},
to={params.right_bottom[1], params.right_bottom[2]},
dash_offset = 0.5,
})
rendering.draw_line(params, {
from={params.right_bottom[1], params.left_top[2]},
to={params.right_bottom[1], params.right_bottom[2]},
dash_offset = 0.0,
})
rendering.draw_line(params, {
from={params.left_top[1], params.left_top[2]},
to={params.left_top[1], params.right_bottom[2]},
dash_offset = 0.5,
})
end
local meta = {}
function meta:__index(k)
return function(t, t2)
if rendering_extension[k] then
rendering_extension[k](parametrizer(t, t2))
else
rendering[k](parametrizer(t, t2))
end
end
end
return setmetatable({}, meta)
end
function render_util.draw_clear_rendering(player_data, event)
rendering.clear("mining-patch-planner")
end
---Draws the properties of a mining drill
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_drill_struct(player_data, event)
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
local x, y = fx1 + 0.5, fy1 + 0.5
local fx2, fy2 = event.area.right_bottom.x, event.area.right_bottom.y
fx2, fy2 = ceil(fx2), ceil(fy2)
--renderer.draw_cross{x=fx1, y=fy1, w=fx2-fx1, h=fy2-fy1}
--renderer.draw_cross{x=fx1, y=fy1, w=2}
local drill = mpp_util.miner_struct(player_data.choices.miner_choice)
renderer.draw_circle{
x = fx1 + drill.drop_pos.x,
y = fy1 + drill.drop_pos.y,
c = {0, 1, 0},
r = 0.2,
}
-- drop pos
renderer.draw_cross{
x = fx1 + 0.5 + drill.out_x,
y = fy1 + 0.5 + drill.out_y,
r = 0.3,
}
for _, pos in pairs(drill.output_rotated) do
renderer.draw_cross{
x = fx1 + 0.5 + pos[1],
y = fy1 + 0.5 + pos[2],
r = 0.15,
width = 3,
c={0, 0, 0, .5},
}
end
renderer.draw_line{
from={x + drill.x, y},
to={x + drill.x, y + 2},
width = 2, color={0.5, 0.5, 0.5}
}
renderer.draw_line{
from={x + drill.x, y},
to={x + drill.x-.5, y + .65},
width = 2, color={0.5, 0.5, 0.5}
}
renderer.draw_line{
from={x + drill.x, y},
to={x + drill.x+.5, y + .65},
width = 2, color={0.5, 0.5, 0.5}
}
-- drill origin
renderer.draw_circle{
x = fx1 + 0.5,
y = fy1 + 0.5,
width = 2,
r = 0.4,
}
renderer.draw_text{
target={fx1 + .5, fy1 + .5},
text = "(0, 0)",
alignment = "center",
vertical_alignment="middle",
scale = 0.6,
}
-- negative extent - cyan
renderer.draw_cross{
x = fx1 +.5 + drill.extent_negative,
y = fy1 +.5 + drill.extent_negative,
r = 0.25,
c = {0, 0.8, 0.8},
}
-- positive extent - purple
renderer.draw_cross{
x = fx1 +.5 + drill.extent_positive,
y = fy1 +.5 + drill.extent_positive,
r = 0.25,
c = {1, 0, 1},
}
renderer.draw_rectangle{
x=fx1,
y=fy1,
w=drill.size,
h=drill.size,
width=3,
gap_length=0.5,
dash_length=0.5,
}
renderer.draw_rectangle_dashed{
x=fx1 + drill.extent_negative,
y=fy1 + drill.extent_negative,
w=drill.area,
h=drill.area,
c={0.5, 0.5, 0.5},
width=5,
gap_length=0.5,
dash_length=0.5,
}
if drill.supports_fluids then
-- pipe connections
renderer.draw_line{
width=4, color = {0, .7, 1},
from={fx1-.3, y+drill.pipe_left-.5},
to={fx1-.3, y+drill.pipe_left+.5},
}
renderer.draw_line{
width=4, color = {.7, .7, 0},
from={fx1+drill.size+.3, y+drill.pipe_left-.5},
to={fx1+drill.size+.3, y+drill.pipe_left+.5},
}
end
renderer.draw_text{
target={fx1 + drill.extent_negative, fy1 + drill.extent_negative-1.5},
text = string.format("skip_outer: %s", drill.skip_outer),
alignment = "left",
vertical_alignment="middle",
}
renderer.draw_circle{x = fx1, y = fy1, r = 0.1}
--renderer.draw_circle{ x = fx2, y = fy2, r = 0.15, color={1, 0, 0} }
end
---Preview the pole coverage
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_pole_layout(player_data, event)
rendering.clear("mining-patch-planner")
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
--renderer.draw_cross{x=fx1, y=fy1, w=fx2-fx1, h=fy2-fy1}
--renderer.draw_cross{x=fx1, y=fy1, w=2}
local drill = mpp_util.miner_struct(player_data.choices.miner_choice)
local pole = mpp_util.pole_struct(player_data.choices.pole_choice)
local function draw_lane(x, y, count)
for i = 0, count-1 do
renderer.draw_rectangle{
x = x + drill.size * i + 0.15 , y = y+.15,
w = drill.size-.3, h=1-.3,
color = i % 2 == 0 and {143/255, 86/255, 59/255} or {223/255, 113/255, 38/255},
width=2,
}
end
---@diagnostic disable-next-line: param-type-mismatch
local coverage = mpp_util.calculate_pole_coverage(player_data.choices, count, 1)
renderer.draw_circle{
x=x+.5, y=y-0.5, radius = .25, color={0.7, 0.7, 0.7},
}
for i = coverage.pole_start, coverage.full_miner_width, coverage.pole_step do
renderer.draw_circle{
x = x + i + .5,
y = y - .5,
radius = 0.3, width=2,
color = {0, 1, 1},
}
renderer.draw_line{
x = x + i +.5 - pole.supply_width / 2+.2,
y = y - .2,
h = 0,
w = pole.supply_width-.4,
color = {0, 1, 1},
width = 2,
}
renderer.draw_line{
x = x + i +.5 - pole.supply_width / 2 + .2,
y = y - .7,
h = .5,
w = 0,
color = {0, 1, 1},
width = 2,
}
renderer.draw_line{
x = x + i +.5 + pole.supply_width / 2 - .2,
y = y - .7,
h = .5,
w = 0,
color = {0, 1, 1},
width = 2,
}
end
end
for i = 1, 10 do
draw_lane(fx1, fy1+(i-1)*3, i)
end
end
---Preview the pole coverage
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_pole_layout_compact(player_data, event)
rendering.clear("mining-patch-planner")
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
--renderer.draw_cross{x=fx1, y=fy1, w=fx2-fx1, h=fy2-fy1}
--renderer.draw_cross{x=fx1, y=fy1, w=2}
local drill = mpp_util.miner_struct(player_data.choices.miner_choice)
local pole = mpp_util.pole_struct(player_data.choices.pole_choice)
local function draw_lane(x, y, count)
for i = 0, count-1 do
renderer.draw_rectangle{
x = x + drill.size * i + 0.15 , y = y+.15,
w = drill.size-.3, h=1-.3,
color = i % 2 == 0 and {143/255, 86/255, 59/255} or {223/255, 113/255, 38/255},
width=2,
}
end
---@diagnostic disable-next-line: param-type-mismatch
local coverage = mpp_util.calculate_pole_spacing(player_data.choices, count, 1)
renderer.draw_circle{
x=x+.5, y=y-0.5, radius = .25, color={0.7, 0.7, 0.7},
}
for i = coverage.pole_start, coverage.full_miner_width, coverage.pole_step do
renderer.draw_circle{
x = x + i + .5,
y = y - .5,
radius = 0.3, width=2,
color = {0, 1, 1},
}
renderer.draw_line{
x = x + i +.5 - pole.supply_width / 2+.2,
y = y - .2,
h = 0,
w = pole.supply_width-.4,
color = {0, 1, 1},
width = 2,
}
renderer.draw_line{
x = x + i +.5 - pole.supply_width / 2 + .2,
y = y - .7,
h = .5,
w = 0,
color = {0, 1, 1},
width = 2,
}
renderer.draw_line{
x = x + i +.5 + pole.supply_width / 2 - .2,
y = y - .7,
h = .5,
w = 0,
color = {0, 1, 1},
width = 2,
}
end
end
for i = 1, 10 do
draw_lane(fx1, fy1+(i-1)*3, i)
end
end
---Displays the labels of built things on the grid
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_built_things(player_data, event)
rendering.clear("mining-patch-planner")
local renderer = render_util.renderer(event)
local state = player_data.last_state
if not state then return end
local C = state.coords
local G = state.grid
for _, row in pairs(G) do
for _, tile in pairs(row) do
---@cast tile GridTile
local thing = tile.built_on
if thing then
-- renderer.draw_circle{
-- x = C.gx + tile.x, y = C.gy + tile.y,
-- w = 1,
-- color = {0, 0.5, 0, 0.1},
-- r = 0.5,
-- }
renderer.draw_rectangle{
x = C.ix1 + tile.x -.9, y = C.iy1 + tile.y -.9,
w = .8,
color = {0, 0.2, 0, 0.1},
}
renderer.draw_text{
x = C.gx + tile.x, y = C.gy + tile.y - .3,
alignment = "center",
vertical_alignment = "top",
--vertical_alignment = tile.x % 2 == 1 and "top" or "bottom",
text = thing,
scale = 0.6,
}
end
end
end
end
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_drill_convolution(player_data, event)
rendering.clear("mining-patch-planner")
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
local state = player_data.last_state
if not state then return end
local C = state.coords
local grid = state.grid
for _, row in pairs(grid) do
for _, tile in pairs(row) do
---@cast tile GridTile
--local c1, c2 = tile.neighbor_counts[m_size], tile.neighbor_counts[m_area]
local c1, c2 = tile.neighbors_inner, tile.neighbors_outer
if c1 == 0 and c2 == 0 then goto continue end
rendering.draw_circle{
surface = state.surface, filled=false, color = {0.3, 0.3, 1},
width=1, radius = 0.5,
target={C.gx + tile.x, C.gy + tile.y},
}
local stagger = (.5 - (tile.x % 2)) * .25
local col = c1 == 0 and {0.3, 0.3, 0.3} or {0.6, 0.6, 0.6}
rendering.draw_text{
surface = state.surface, filled = false, color = col,
target={C.gx + tile.x, C.gy + tile.y + stagger},
text = string.format("%i,%i", c1, c2),
alignment = "center",
vertical_alignment="middle",
}
::continue::
end
end
end
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_power_grid(player_data, event)
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
local state = player_data.last_state
if not state then return end
---@cast state SimpleState
local C = state.coords
local grid = state.grid
local connectivity = state.power_connectivity
if not connectivity then
game.print("No connectivity exists")
return
end
local rendered = {}
for set_id, set in pairs(connectivity) do
local set_color = color.hue_sequence(set_id)
if set_id == 0 then set_color = {1, 1, 1} end
for pole, _ in pairs(set) do
---@cast pole GridPole
-- if rendered[pole] then goto continue end
-- rendered[pole] = true
local pole_color = set_color
-- if not pole.backtracked and not pole.has_consumers then
-- pole_color = {0, 0, 0}
-- end
renderer.draw_circle{
surface = state.surface,
filled = not pole.backtracked,
color = pole_color,
width = 5,
target = {C.gx + pole.grid_x, C.gy + pole.grid_y},
radius = 0.65,
}
renderer.draw_text{
surface = state.surface,
target={C.gx + pole.grid_x, C.gy + pole.grid_y},
text = set_id,
alignment = "center",
vertical_alignment="middle",
scale = 2,
}
renderer.draw_text{
surface = state.surface,
target={C.gx + pole.grid_x, C.gy + pole.grid_y-1.25},
text = ("%i,%i"):format(pole.ix, pole.iy),
alignment = "center",
vertical_alignment="middle",
scale = 2,
}
::continue::
end
end --]]
end
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_centricity(player_data, event)
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
local state = player_data.last_state
if not state then return end
local C = state.coords
local grid = state.grid
end
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_blueprint_data(player_data, event)
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
local x, y = fx1 + 0.5, fy1 + 0.5
local id = player_data.choices.blueprint_choice and player_data.choices.blueprint_choice.item_number
if not id then return end
local bp = player_data.blueprints.cache[id]
if not bp then return end
renderer.draw_line{x = fx1, y = fy1-1, w = 0, h = 2, width = 7, color={0, 0, 0}}
renderer.draw_line{x = fx1-1, y = fy1, w = 2, h = 0, width = 7, color={0, 0, 0}}
renderer.draw_rectangle{
x = fx1,
y = fy1,
w = bp.w,
h = bp.h,
}
for _, ent in pairs(bp.entities) do
local struct = mpp_util.entity_struct(ent.name)
local clr = {1, 1, 1}
if ent.capstone_x and ent.capstone_y then
clr = {0, 1, 1}
elseif ent.capstone_x then
clr = {0, 1, 0}
elseif ent.capstone_y then
clr = {0, 0, 1}
end
renderer.draw_circle{
x = fx1 + ent.position.x,
y = fy1 + ent.position.y,
r = struct.size / 2,
color = clr,
}
end
end
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_deconstruct_preview(player_data, event)
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
local state = player_data.last_state
if not state then return end
local c = state.coords
local grid = state.grid
local layout = algorithm.layouts[state.layout_choice]
if not layout._get_deconstruction_objects then return end
local objects = layout:_get_deconstruction_objects(state)
local DIR = state.direction_choice
for _, t in pairs(objects) do
for _, object in ipairs(t) do
---@cast object GhostSpecification
local extent_w = object.extent_w or object.radius or 0.5
local extent_h = object.extent_h or extent_w
local x1, y1 = object.grid_x-extent_w, object.grid_y-extent_h
local x2, y2 = object.grid_x+extent_w, object.grid_y+extent_h
x1, y1 = mpp_util.revert_ex(c.gx, c.gy, DIR, x1, y1, c.tw, c.th)
x2, y2 = mpp_util.revert_ex(c.gx, c.gy, DIR, x2, y2, c.tw, c.th)
rendering.draw_rectangle{
surface=state.surface,
players={state.player},
filled=false,
width=3,
color={1, 0, 0},
left_top={x1+.1,y1+.1},
right_bottom={x2-.1,y2-.1},
}
end
end
end
---@param player_data PlayerData
---@param event EventData.on_player_reverse_selected_area
function render_util.draw_can_place_entity(player_data, event)
local renderer = render_util.renderer(event)
local fx1, fy1 = event.area.left_top.x, event.area.left_top.y
fx1, fy1 = floor(fx1), floor(fy1)
local x, y = fx1 + 0.5, fy1 + 0.5
local id = player_data.choices.blueprint_choice and player_data.choices.blueprint_choice.item_number
if not id then return end
local bp = player_data.blueprints.cache[id]
if not bp then return end
renderer.draw_line{x = fx1, y = fy1-1, w = 0, h = 1, width = 7, color={0.3, 0.3, 0.3}}
renderer.draw_line{x = fx1-1, y = fy1, w = 1, h = 0, width = 7, color={0.3, 0.3, 0.3}}
local build_check_type = defines.build_check_type
local can_forced = {
[{false, false}] = 0,
[{true, false}] = 1,
[{false, true}] = 2,
[{true, true}] = 3,
}
for check_type, i1 in pairs(build_check_type) do
for forced_ghost, i2 in pairs(can_forced) do
local forced, ghost = forced_ghost[1], forced_ghost[2]
local nx = x + (bp.w + 3) * i1
local ny = y + (bp.h + 3) * i2
renderer.draw_rectangle{
x = nx-.5,
y = ny-.5,
w = bp.w,
h = bp.h,
color = {0.2, 0.2, .7},
}
-- renderer.draw_text{
-- x = nx+(bp.w-1)/2, y = ny-.5,
-- text = ("%i,%i"):format(i1, i2),
-- alignment = "center",
-- vertical_alignment="bottom",
-- }
renderer.draw_text{
x = nx+(bp.w-1)/2, y = ny-1.5,
text = ("%s"):format(check_type),
alignment = "center",
vertical_alignment="bottom",
}
renderer.draw_text{
x = nx+(bp.w-1)/2, y = ny-1,
text = ("forced: %s"):format(forced),
alignment = "center",
vertical_alignment="bottom",
}
renderer.draw_text{
x = nx+(bp.w-1)/2, y = ny-.5,
text = ("ghost: %s"):format(ghost),
alignment = "center",
vertical_alignment="bottom",
}
for _, ent in pairs(bp.entities) do
local ex = nx + ent.position.x - .5
local ey = ny + ent.position.y - .5
local t = {
name = ent.name,
position = {ex, ey},
direction = ent.direction,
build_check_type = defines.build_check_type[check_type],
force = game.get_player(event.player_index).force,
forced = forced,
}
if ghost then
t.name, t.inner_name = "entity-ghost", t.name
end
local can_place = event.surface.can_place_entity(t)
renderer.draw_circle{x = ex, y = ey, r = 0.5, width = 3,
color = can_place and {0.1, .9, .1} or {0.9, .1, .1},
}
if can_place then
if not ghost then
t.name, t.inner_name = "entity-ghost", t.name
end
event.surface.create_entity(t)
end
end
end
end
end
return render_util

View File

@@ -0,0 +1,25 @@
local set_mt = {}
set_mt.__index = set_mt
function set_mt.new(t)
local new = {}
for k, v in pairs(t or {}) do
new[v] = true
end
return setmetatable(new, set_mt)
end
local list_mt = {}
list_mt.__index = list_mt
function list_mt.new(t)
local new = {}
for k, v in pairs(t or {}) do
end
return setmetatable(new, list_mt)
end
return set_mt, list_mt