UnrealisticReactors/scripts/heat/math.lua

365 lines
10 KiB
Lua

local rpath = (...):match("(.-)[^%.]+$")
local rroot = rpath:match("^([^%.]+%.)")
local heat_buffer_transition_position = require(rpath .. "buffer").heat_buffer_transition_position
local heat_buffer_transitions = require(rpath .. "buffer").heat_buffer_transitions
local isempty = require(rroot .. "util").isempty
local noop = require(rroot .. "util").noop
local util = require(rpath .. "util")
local imerge = util.imerge
local set3d = util.set3d
local get3d = util.get3d
local get2d = util.get2d
local rm2d = util. rm2d
local inf = 1/0
local function distance(x1,y1,x2,y2)
-- pythagoras
-- return math.sqrt(math.pow(x2 - x1,2) + math.pow(y2 - y1, 2))
-- and
-- chebyshev
-- return math.max(math.abs(x2 - x1), math.abs(y2 - y1)) -- I AM THE KING!
-- in a
-- taxicab
return math.abs(x2 - x1) + math.abs(y2 - y1)
end
local function heuristic(a,b)
return distance(a.x, a.y, b.x, b.y)
end
local function lowest_score(set, score)
local lowest, best = inf
for node,_ in pairs(set) do
local value = score[node]
if value < lowest then
lowest, best = value, node
end
end
return best
end
local function calculate_score(score, node, goal)
local a = node.position or node.chunk.position
local b = goal.position or goal.chunk.position
return score[node] + heuristic(a,b)
end
local function astar(callback, start, goal, neighbours, ...) -- A*
if start == goal then return start end -- connected
local current
local count = 1
local closedSet = {}
local openSet = {[start] = true}
local gScore = {[start] = 0}
local fScore = {[start] = calculate_score(gScore,start,goal)}
local function score(node)
if not closedSet[node] then
local tentative_gScore = calculate_score(gScore, current, node)
if not openSet[node] or tentative_gScore < gScore[node] then
callback(current, node)
gScore[node] = tentative_gScore
fScore[node] = calculate_score(gScore, node, goal)
if not openSet[node] then
openSet[node] = true
count = count + 1
end
end
end
end
while count > 0 do
current = lowest_score(openSet, fScore)
if current == goal then return goal end -- connected
count = count - 1
openSet[current] = nil
closedSet[current] = true
neighbours(current, score, ...)
end
return nil -- not connected
end
local function testpath(...) -- A* → bool
return astar(noop, ...)
end
local function getpath(...) -- A* → {node,...}
local cameFrom = {}
local goal = astar(function (current,node) cameFrom[node] = current end, ...)
if not goal then return nil end
local path = {goal}
while cameFrom[goal] do
goal = cameFrom[goal]
table.insert(path, 1, goal) -- prepend
end
return path
end
local function is_connected_outside(path)
if not path then return false end
local before = path[1]
for i = 2,#path do
local current = path[i]
if heuristic(before.position, current.position) > 1 then -- distance
return true -- must have moved trough linked entities
end
end
return false
end
local function cleanup_heat_network(id)
-- log("cleanup heat network " .. tostring(id))
HEAT.network[id] = nil
HEAT.ids[id] = nil
end
local function clear_heat_network_for_surface(z)
HEAT.cells [z] = nil
HEAT.outlets[z] = nil
for id,network in pairs(HEAT.network) do
local chunks = select(2,next(network.chunks))
local chunk = chunks and select(2,next(chunks))
if chunk and chunk.position.z == z then
cleanup_heat_network(id)
end
end
end
-- x,y,z = position.x, position.y, surface.index
-- using z,x,y order for easier removal
local function rm_heat_network_cell(x,y,z) return rm2d(HEAT.cells[z] or {}, x,y) end
local function get_heat_network_cell(x,y,z,d) return get3d(HEAT.cells, z,x,y, d) end
local function set_heat_network_cell(x,y,z,v) return set3d(HEAT.cells, z,x,y, v) end
local function get_heat_outlet(x,y,z,d) return get3d(HEAT.outlets, z,x,y, d) end
local function set_heat_outlet(x,y,z,v) return set3d(HEAT.outlets, z,x,y, v) end
-- cps instead of coroutine
local function direct_neighbour_nodes(entities, entity, callback, ...)
local z = entity.surface.index
local is_reactor = entity.type == 'reactor'
for i,p,o in heat_buffer_transitions(entity) do
-- local neighbour = get2d(is_reactor and (get_heat_network_cell(o.x,o.y,z) or {}).entities or entities, o.x,o.y)
local neighbour = get2d(entities, o.x,o.y)
if neighbour and neighbour.valid then
callback(neighbour, o, ...)
end
if is_reactor or i == 1 then
if is_reactor then callback(entity, p, ...) end -- THIS LINE IS MAGIC
local reactors = get_heat_outlet(p.x,p.y,z, {})
for _,reactor in pairs(reactors) do
local r = reactor.position
local neighbour = get2d(reactor.cell.entities, r.x,r.y)
if neighbour and neighbour.valid then
if get2d(entities, r.x,r.y) then -- same cell
callback(neighbour, r, ...)
else
local position = heat_buffer_transition_position(reactor, r,p)
if position then callback(neighbour, position, ...) end
end
end
end
end
end
end
local function cell_nodes(entities)
return function (...) return direct_neighbour_nodes(entities, ...) end
end
local function linked_neighbour_nodes(entities, linked, entity, callback, ...)
direct_neighbour_nodes(entities, entity, callback, ...)
if not linked[entity] then return end
for position,neighbour in pairs(linked[entity]) do
if neighbour ~= entity then
callback(neighbour, position, ...)
end
end
end
local function chunk_nodes(entities, linked)
return function (...) return linked_neighbour_nodes(entities, linked, ...) end
end
local function neighbour_chunks(chunk, callback, chunks)
local x,y = chunk.position.x, chunk.position.y
for _,o in pairs(O) do
local neighbour = get2d(chunks, o.x+x, o.y+y)
if neighbour then callback(neighbour) end
end
end
local function neighbour_cells(chunk, filter, callback, ...)
local network,area = chunk.network,chunk.area
local x,y,z = chunk.position.x, chunk.position.y
for d,o in pairs(O) do
local corner,k = next(M[d])
local a = area[corner][k]
local other = get2d(network.chunks, o.x+x, o.y+y)
if other then
for c,cell in pairs(chunk.border[d]) do
if cell == filter then
local neighbour = other.border[R[d]][c] -- of same network
if neighbour then
local p = {[k]=a, [V[k]]=c}
callback(neighbour,p,o,...)
end
end
end
end
end
end
local function connected_cells(cell, callback, ignore)
if ignore == cell then return end
neighbour_cells(cell.chunk, cell, callback)
end
local function chunked(v) return math.floor(v/32) end
local function get_chunk_neighbour(cell, x,y, callback, ...)
local p = {x=x,y=y} -- entity.position
local cx,cy = chunked(x), chunked(y) -- chunk position
for _,corner in ipairs{'left_top','right_bottom'} do
for k,v in pairs(p) do
if cell.chunk.area[corner][k] == v then
local d = A[corner][k]
local o = O[d]
local neighbour = get2d(cell.chunk.network.chunks, cx+o.x, cy+o.y)
local other = neighbour and neighbour.border[R[d]][p[V[k]]] -- of same network
if other then callback(other,p,o,...) end
end
end
end
end
local function Linked(others)
-- gather result
-- log"linked entities:"
local linked = {}
for _,cells in pairs(others) do
if #cells > 1 then
local entities = {}
-- log"linked:"
for _,other in ipairs(cells) do
local entity = other.entity
entities[other.position] = entity
linked[entity] = entities
-- log(string.format("entity=%s x=%s y=%s %s", entity.unit_number, entity.position.x, entity.position.y, dbgVector(other.position)))
end
end
end
return linked
end
local function inside_linked_entities(cell)
local entities = cell.entities
local others,neighbours = {},cell_nodes(entities)
-- test if entities are connected inside chunk
neighbour_cells(cell.chunk, cell, function (cell,position)
-- log(dbgVector(position))
-- log(dbgHeatNetworkCell(cell))
local entity = get2d(entities, position.x, position.y)
local found = nil
for _,cells in ipairs(others) do
if testpath(entity, cells[1].entity, neighbours) then -- inner
-- log"connected inside"
found = cells
break
end
end
if not found then
found = {}
table.insert(others, found)
end
table.insert(found, {cell=cell, entity=entity, position=position})
end)
-- log("Inside ".. dbgLinked(others))
return others
end
local function outside_linked_entities(cell, others)
-- test if entities are connected outside chunk
local n = #others
if n > 1 then
for i = 1,n do
local current = others[i]
if current then
local start = current[1].cell
for j,cells in pairs(others) do
if current ~= cells then
for _,goal in pairs(cells) do goal = goal.cell
if testpath(start, goal, connected_cells, cell) then -- outer
-- log"connected outside"
current = imerge(current, cells)
others[current == cells and i or j] = nil
break
end
end
end
end
end
end
end
-- log("Outside ".. dbgLinked(others))
return others
end
local function get_linked_entities(cell)
local others = inside_linked_entities(cell)
local inside = Linked(others)
others = outside_linked_entities(cell, others)
local outside = Linked(others)
return outside, inside
end
local function get_entity_neighbour_cells(entity,callback,...)
local z = entity.surface.index
local is_reactor = entity.type == 'reactor'
for i,p,o in heat_buffer_transitions(entity) do
local cell = get_heat_network_cell(o.x,o.y,z)
if cell then callback(cell,o,...) end
if is_reactor or i == 1 then
for _,reactor in pairs(get_heat_outlet(p.x,p.y,z,{})) do
local position = heat_buffer_transition_position(reactor, reactor.position,p)
local cell = get_heat_network_cell(position.x,position.y,z)
if cell then callback(cell, position, ...) end
end
if is_reactor then
local cell = get_heat_network_cell(p.x,p.y,z)
if cell then callback(cell, p, ...) end
end
end
end
end
return { -- exports
testpath = testpath,
getpath = getpath,
is_connected_outside = is_connected_outside,
chunk_nodes = chunk_nodes,
cell_nodes = cell_nodes,
neighbour_chunks = neighbour_chunks,
neighbour_cells = neighbour_cells,
connected_cells = connected_cells,
get_chunk_neighbour = get_chunk_neighbour,
get_linked_entities = get_linked_entities,
get_entity_neighbour_cells = get_entity_neighbour_cells,
clear_heat_network_for_surface = clear_heat_network_for_surface,
cleanup_heat_network = cleanup_heat_network,
rm_heat_network_cell = rm_heat_network_cell,
get_heat_network_cell = get_heat_network_cell,
set_heat_network_cell = set_heat_network_cell,
get_heat_outlet = get_heat_outlet,
set_heat_outlet = set_heat_outlet,
distance = heuristic,
}