1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2026-04-26 21:02:59 +02:00
Files
ComfyFactorio/utils/chunk_removal.lua
2026-03-29 18:32:13 +02:00

686 lines
21 KiB
Lua

local Global = require 'utils.global'
local Task = require 'utils.task'
local Token = require 'utils.token'
local Event = require 'utils.event'
local Alert = require 'utils.alert'
local Surface = require 'utils.surface'
local Commands = require 'utils.commands'
local this =
{
settings =
{
player_refresh_index = nil,
force_removal_flag = -2000,
chunk_iter = nil,
world_eater_iter = nil,
timeout_ticks = 216000,
enabled = false,
remover_disabled = false,
_debug = false,
task_remover = false,
pollution_enabled = false
},
list =
{
map = {},
removal_list = {}
}
}
Global.register(
this,
function (tbl)
this = tbl
end
)
local cleaner = '[color=blue]Cleaner:[/color] '
local floor = math.floor
local Public = {}
local clear_chunk_token =
Token.register(
function (event)
local chunk = event.chunk
if not chunk then
return
end
local surface = game.get_surface(event.surface_index)
if not surface or not surface.valid then
return
end
if chunk and #chunk > 2 then
for _, c in pairs(chunk) do
surface.delete_chunk(c)
end
else
surface.delete_chunk(chunk)
end
end
)
local function debug(str)
if this.settings._debug then
print(str)
end
end
local function notify_all_players(msg)
local color = { r = 0, g = 255, b = 171 }
local players = game.connected_players
for i = 1, #players do
local player = players[i]
Alert.alert_player(player, 10, msg, color)
end
end
local function chunk_from_tile_pos(pos)
return { x = floor(pos.x / 32), y = floor(pos.y / 32) }
end
local function get_next_player()
if (not this.settings.player_refresh_index or not game.players[this.settings.player_refresh_index]) then
this.settings.player_refresh_index = 1
else
this.settings.player_refresh_index = this.settings.player_refresh_index + 1
end
if (this.settings.player_refresh_index > #game.players) then
this.settings.player_refresh_index = 1
end
return this.settings.player_refresh_index
end
local function chunk_is_safe(c_pos, permanent)
if (this.list.map[c_pos.x] == nil) then
this.list.map[c_pos.x] = {}
end
if (permanent) then
-- Make sure we don't overwrite...
this.list.map[c_pos.x][c_pos.y] = -2
elseif (this.list.map[c_pos.x][c_pos.y] and (this.list.map[c_pos.x][c_pos.y] ~= -2)) then
this.list.map[c_pos.x][c_pos.y] = -1
end
end
local function area_is_safe(c_pos, chunk_radius, permanent)
for i = -chunk_radius, chunk_radius do
for j = -chunk_radius, chunk_radius do
chunk_is_safe({ x = c_pos.x + i, y = c_pos.y + j }, permanent)
end
end
end
-- Refreshes timers on all chunks around a certain area
local function refresh_area(pos, chunk_radius, bonus_time)
local c_pos = chunk_from_tile_pos(pos)
if not c_pos then
return
end
for i = -chunk_radius, chunk_radius do
local x = c_pos.x + i
for k = -chunk_radius, chunk_radius do
local y = c_pos.y + k
if (this.list.map[x] == nil) then
this.list.map[x] = {}
end
if ((this.list.map[x][y] == nil) or (this.list.map[x][y] >= 0)) then
this.list.map[x][y] = game.tick + bonus_time
end
end
end
end
-- Refresh all chunks near a single player. Cyles through all connected players.
local function refresh_player_area()
local player_index = get_next_player()
if (player_index and game.connected_players[player_index]) then
local player = game.connected_players[player_index]
if (not player.character) then
return
end
local this_surface_index = Surface.get_surface_index()
if (player.surface.index ~= this_surface_index) then
return
end
refresh_area(player.position, 4, 0)
end
end
-- Gets the next chunk the array map and checks to see if it has timed out.
-- Adds it to the removal list if it has.
local function get_next_chunk()
local this_surface_index = Surface.get_surface_index()
local surface = game.get_surface(this_surface_index)
if not surface or not surface.valid then
debug('get_next_chunk - Surface was not valid?')
return
end
-- Make sure we have a valid iterator!
if (not this.settings.chunk_iter or not this.settings.chunk_iter.valid) then
this.settings.chunk_iter = surface.get_chunks()
end
local next_chunk = this.settings.chunk_iter()
-- Check if we reached the end
if (not next_chunk) then
this.settings.chunk_iter = surface.get_chunks()
next_chunk = this.settings.chunk_iter()
end
-- Do we have it in our map?
if (not this.list.map[next_chunk.x] or not this.list.map[next_chunk.x][next_chunk.y]) then
return -- Chunk isn't in our map so we don't care?
end
-- If the chunk has timed out, add it to the removal list
local c_timer = this.list.map[next_chunk.x][next_chunk.y]
if ((c_timer ~= nil) and (c_timer >= 0) and ((c_timer + this.settings.timeout_ticks) < game.tick)) then
-- Check chunk actually exists
if (surface.is_chunk_generated({ x = next_chunk.x, y = next_chunk.y })) then
this.list.removal_list[#this.list.removal_list + 1] = { pos = { x = next_chunk.x, y = next_chunk.y }, force = false }
this.list.map[next_chunk.x][next_chunk.y] = nil
end
end
end
-- Remove all chunks at same time to reduce impact to FPS/UPS
local function try_remove_chunks()
local this_surface_index = Surface.get_surface_index()
local surface = game.get_surface(this_surface_index)
if not surface or not surface.valid then
debug('try_remove_chunks - Surface was not valid?')
return
end
local tick = 5
for key, c_remove in pairs(this.list.removal_list) do
tick = tick + 6
local c_pos = c_remove.pos
-- Confirm chunk is still expired
if (not this.list.map[c_pos.x] or not this.list.map[c_pos.x][c_pos.y]) then
-- If it is FORCE removal, then remove it regardless of pollution.
if (c_remove.force) then
-- If it is a normal timeout removal, don't do it if there is pollution in the chunk.
if not this.settings.task_remover then
surface.delete_chunk(c_pos)
else
Task.set_timeout_in_ticks(tick, clear_chunk_token, { chunk = c_pos, surface_index = surface.index })
end
elseif this.settings.pollution_enabled and (surface.get_pollution({ c_pos.x * 32, c_pos.y * 32 }) > 0) then
-- Else delete the chunk
this.list.map[c_pos.x][c_pos.y] = game.tick
else
if not this.settings.task_remover then
surface.delete_chunk(c_pos)
else
Task.set_timeout_in_ticks(tick, clear_chunk_token, { chunk = c_pos, surface_index = surface.index })
end
end
end
-- Remove entry
this.list.removal_list[key] = nil
end
-- MUST GET A NEW CHUNK ITERATOR ON DELETE CHUNK!
this.settings.chunk_iter = nil
this.settings.world_eater_iter = nil
end
local function remove_chunks()
local this_surface_index = Surface.get_surface_index()
local surface = game.get_surface(this_surface_index)
if not surface or not surface.valid then
debug('remove_chunks - Surface was not valid?')
return
end
-- Make sure we have a valid iterator!
if (not this.settings.world_eater_iter or not this.settings.world_eater_iter.valid) then
this.settings.world_eater_iter = surface.get_chunks()
end
local next_chunk = this.settings.world_eater_iter()
-- Check if we reached the end
if (not next_chunk) then
this.settings.world_eater_iter = surface.get_chunks()
next_chunk = this.settings.world_eater_iter()
end
-- Do we have it in our map?
if (not this.list.map[next_chunk.x] or not this.list.map[next_chunk.x][next_chunk.y]) then
return -- Chunk isn't in our map so we don't care?
end
-- If the chunk isn't marked permament, then check if we can remove it
local c_timer = this.list.map[next_chunk.x][next_chunk.y]
if (c_timer == -1) then
local area =
{
left_top = { next_chunk.area.left_top.x - 8, next_chunk.area.left_top.y - 8 },
right_bottom = { next_chunk.area.right_bottom.x + 8, next_chunk.area.right_bottom.y + 8 }
}
local ents = surface.find_entities_filtered { area = area, force = { 'enemy', 'neutral' }, invert = true }
local total_count = #ents
local has_last_user_set = false
local t_type =
{
['character'] = true,
['construction-robot'] = true,
['logistic-robot'] = true
}
if (total_count > 0) then
for _, v in pairs(ents) do
if (v.last_user or t_type[v.type]) then
has_last_user_set = true
break -- This means we're done checking this chunk.
end
end
-- If all entities found have no last user, then KILL all entities!
if not has_last_user_set then
for _, v in pairs(ents) do
if (v and v.valid) then
v.die(nil)
end
end
-- notify_all_players(cleaner .. next_chunk.x .. "," .. next_chunk.y .. " WorldEaterSingleStep - ENTITIES FOUND")
this.list.map[next_chunk.x][next_chunk.y] = game.tick -- Set the timer on it.
end
else
-- notify_all_players(cleaner .. next_chunk.x .. "," .. next_chunk.y .. " WorldEaterSingleStep - NO ENTITIES FOUND")
this.list.map[next_chunk.x][next_chunk.y] = game.tick -- Set the timer on it.
end
end
end
local function remove_chunks_forced()
local this_surface_index = Surface.get_surface_index()
local surface = game.get_surface(this_surface_index)
if not surface or not surface.valid then
debug('remove_chunks_forced - Surface was not valid?')
return
end
notify_all_players(cleaner .. 'Manual map cleanup in progress - forced!')
local chunks = surface.get_chunks()
local tick = 0
local chunks_to_remove = {}
for chunk in chunks do
local can_delete = true
-- Do we have it in our map?
if surface.is_chunk_generated(chunk) then
local area =
{
left_top = { chunk.area.left_top.x - 64, chunk.area.left_top.y - 64 },
right_bottom = { chunk.area.right_bottom.x + 64, chunk.area.right_bottom.y + 64 }
}
local ents = surface.find_entities_filtered { area = area, force = { 'neutral', 'enemy' }, invert = true }
local total_count = #ents
if (total_count > 0) then
for _, v in pairs(ents) do
if (v.last_user) then
can_delete = false
end
if v.type == 'character' then
can_delete = false
end
end
if can_delete then
chunks_to_remove[#chunks_to_remove + 1] = chunk
-- Task.set_timeout_in_ticks(tick, clear_chunk_token, { chunk = chunk, surface_index = surface.index })
-- surface.delete_chunk(chunk)
end
else
-- surface.delete_chunk(chunk)
chunks_to_remove[#chunks_to_remove + 1] = chunk
-- Task.set_timeout_in_ticks(tick, clear_chunk_token, { chunk = chunk, surface_index = surface.index })
end
end
if chunks_to_remove and #chunks_to_remove >= 10 then
tick = tick + 2
Task.set_timeout_in_ticks(tick, clear_chunk_token,
{ chunk = chunks_to_remove, surface_index = surface.index })
chunks_to_remove = {}
end
end
end
-- Marks a safe area around a TILE position to be relatively permanent.
local function mark_area_safe_tiles(pos, chunk_radius, permanent)
local c_pos = chunk_from_tile_pos(pos)
if not c_pos then
return
end
area_is_safe(c_pos, chunk_radius, permanent)
end
-- This is the main work function, it checks a single chunk in the list
-- per tick. It works according to the rules listed in the header of this
-- file.
local function on_tick()
local tick = game.tick
-- Every half a second, refresh all chunks near a single player
-- Cyles through all players. Tick is offset by 2
if ((tick % (30)) == 2) then
refresh_player_area()
end
-- Catch force remove flag
if (tick == this.settings.force_removal_flag + 60) then
notify_all_players(cleaner .. 'Manual map cleanup in progress!')
end
if (tick == this.settings.force_removal_flag + (60 * 30 + 60)) then
try_remove_chunks()
notify_all_players(cleaner .. 'Manual map cleanup done.')
return
end
-- Every tick, check a few points in the 2d array of the only active surface According to /measured-command this
-- shouldn't take more than 0.1ms on average
for _ = 1, 20 do
get_next_chunk()
end
if (not this.settings.remover_disabled) then
remove_chunks()
end
-- Allow enable/disable of auto cleanup, can change during runtime.
local interval_ticks = this.settings.timeout_ticks
-- Send a broadcast warning before it happens.
if ((tick % interval_ticks) == interval_ticks - (60 * 30 + 1)) then
if (#this.list.removal_list > 100) then
notify_all_players(cleaner .. 'Automated map cleanup in progress!')
end
end
-- Delete all listed chunks across all active surfaces
if ((tick % interval_ticks) == interval_ticks - 1) then
if (#this.list.removal_list > 100) then
try_remove_chunks()
notify_all_players(cleaner .. 'Automated map cleanup done.')
end
end
end
function Public.on_chunk_generated(event)
local c_pos = chunk_from_tile_pos(event.area.left_top)
-- Surface must be "added" first.
if (this.settings == nil) then
return
end
-- If this is the first chunk in that row:
if (this.list.map[c_pos.x] == nil) then
this.list.map[c_pos.x] = {}
end
-- Only update it if it isn't already set!
if (this.list.map[c_pos.x][c_pos.y] == nil) then
this.list.map[c_pos.x][c_pos.y] = game.tick
end
end
function Public.trigger()
this.settings.force_removal_flag = game.tick
end
-- Mark an area for "immediate" forced removal
function Public.mark_base_for_removal_forced(pos, chunk_radius)
local c_pos = chunk_from_tile_pos(pos)
for i = -chunk_radius, chunk_radius do
local x = c_pos.x + i
for k = -chunk_radius, chunk_radius do
local y = c_pos.y + k
if (this.list.map[x] ~= nil) then
this.list.map[x][y] = nil
end
this.list.removal_list[#this.list.removal_list + 1] = { pos = { x = x, y = y }, force = true }
end
if (table_size(this.list.map[x]) == 0) then
this.list.map[x] = nil
end
end
end
-- Downgrades permanent flag to semi-permanent.
function Public.mark_base_for_removal_non_forced(pos, chunk_radius)
local c_pos = chunk_from_tile_pos(pos)
for i = -chunk_radius, chunk_radius do
local x = c_pos.x + i
for k = -chunk_radius, chunk_radius do
local y = c_pos.y + k
if (this.list.map[x] and this.list.map[x][y] and (this.list.map[x][y] == -2)) then
this.list.map[x][y] = -1
end
end
end
end
-- Refreshes timers on a chunk containing position
function Public.refresh_chunk_timer(pos, bonus_time)
local c_pos = chunk_from_tile_pos(pos)
if (this.list.map[c_pos.x] == nil) then
this.list.map[c_pos.x] = {}
end
if (this.list.map[c_pos.x][c_pos.y] >= 0) then
this.list.map[c_pos.x][c_pos.y] = game.tick + bonus_time
end
end
-- Refreshes timers on all chunks near an ACTIVE radar
function Public.on_sector_scanned(event)
local this_surface_index = Surface.get_surface_index()
local radar = event.radar
local surface = radar.surface
if (surface.index ~= this_surface_index) then
return
end
refresh_area(radar.position, 14, 0)
end
function Public.toggle_chunk_removal(state)
this.settings.enabled = state or false
end
Event.add(
defines.events.on_built_entity,
function (event)
local entity = event.entity
if not entity or not entity.valid then
return
end
if this.settings.enabled then
local surface_index = Surface.get_surface_index()
if entity.surface.index ~= surface_index then
return
end
mark_area_safe_tiles(entity.position, 2, false)
end
end
)
Event.add(
defines.events.on_player_built_tile,
function (event)
local surface_index = event.surface_index
local tiles = event.tiles
if this.settings.enabled then
local this_surface_index = Surface.get_surface_index()
if surface_index ~= this_surface_index then
return
end
for _, tile in pairs(tiles) do
mark_area_safe_tiles(tile.position, 2, false)
end
end
end
)
Event.add(
defines.events.script_raised_built,
function (event)
local entity = event.entity
if not entity or not entity.valid then
return
end
if this.settings.enabled then
local this_surface_index = Surface.get_surface_index()
if entity.surface.index ~= this_surface_index then
return
end
mark_area_safe_tiles(entity.position, 2, false)
end
end
)
Event.add(
defines.events.on_tick,
function ()
if this.settings.enabled then
on_tick()
end
end
)
Event.add(
defines.events.on_robot_built_entity,
function (event)
local entity = event.entity
if this.settings.enabled then
local surface_index = Surface.get_surface_index()
if (entity.surface.index ~= surface_index) then
return
end
mark_area_safe_tiles(entity.position, 2, false)
end
end
)
Event.add(
defines.events.on_sector_scanned,
function (event)
if this.settings.enabled then
Public.on_sector_scanned(event)
end
end
)
Event.on_init(
function ()
local position = { x = 0, y = 0 }
mark_area_safe_tiles(position, 2, true)
end
)
Commands.new('chunk_removal_run', 'Chunk removal')
:require_role('remove_chunks')
:callback(function (player)
player.print('[Chunk removal] - Initiated force removal of chunks.')
remove_chunks_forced()
end
)
Commands.new('chunk_removal_debug', 'Chunk removal')
:require_role('remove_chunks')
:callback(function (player)
if this.settings._debug then
this.settings._debug = false
player.print('[Chunk removal] - Debug messages are now disabled.')
else
this.settings._debug = true
player.print('[Chunk removal] - Debug messages are now enabled.')
end
end
)
Commands.new('chunk_removal_toggle_task', 'Chunk removal')
:require_role('remove_chunks')
:callback(function (player)
if this.settings.task_remover then
this.settings.task_remover = false
player.print('[Chunk removal] - Tasks are now disabled when removing chunks.')
else
this.settings.task_remover = true
player.print('[Chunk removal] - Tasks are now enabled when removing chunks.')
end
end
)
Commands.new('chunk_removal_pollution_enabled', 'Chunk removal')
:require_role('remove_chunks')
:callback(function (player)
if this.settings.pollution_enabled then
this.settings.pollution_enabled = false
player.print('[Chunk removal] - Pollution is now disabled when removing chunks.')
else
this.settings.pollution_enabled = true
player.print('[Chunk removal] - Pollution is now enabled when removing chunks.')
end
end
)
Commands.new('chunk_removal_enabled', 'Chunk removal')
:require_role('remove_chunks')
:callback(function (player)
if this.settings.enabled then
this.settings.enabled = false
player.print('[Chunk removal] - is now disabled.')
else
this.settings.enabled = true
player.print('[Chunk removal] - is now enabled.')
end
end
)
Public.mark_area_safe_tiles = mark_area_safe_tiles
return Public