mirror of
https://github.com/Oarcinae/FactorioScenarioMultiplayerSpawn.git
synced 2025-01-10 00:28:22 +02:00
384 lines
14 KiB
Lua
384 lines
14 KiB
Lua
-- regrowth_map.lua
|
|
-- Sep 2019
|
|
-- REVERTED BACK TO SOFT MOD
|
|
|
|
-- Code tracks all chunks generated and allows for deleting of inactive chunks.
|
|
--
|
|
-- Basic rules of regrowth:
|
|
-- 1. Area around player is safe for quite a large distance.
|
|
-- 2. Chunks with pollution won't be deleted.
|
|
-- 3. Chunks with any player buildings won't be deleted.
|
|
-- 4. Anything within radar range won't be deleted, but radar MUST be active.
|
|
-- -- This works by refreshing all chunk timers within radar range using
|
|
-- the on_sector_scanned event.
|
|
-- 5. Chunks timeout after 1 hour-ish, configurable
|
|
|
|
require("lib/oarc_utils")
|
|
require("config")
|
|
|
|
REGROWTH_TIMEOUT_TICKS = TICKS_PER_HOUR -- TICKS_PER_HOUR TICKS_PER_MINUTE
|
|
|
|
-- Init globals and set player join area to be off limits.
|
|
function RegrowthInit()
|
|
global.rg = {}
|
|
global.rg.player_refresh_index = nil
|
|
global.rg.force_removal_flag = -2000
|
|
global.rg.map = {}
|
|
global.rg.removal_list = {}
|
|
global.rg.chunk_iter = nil
|
|
global.rg.world_eater_iter = nil
|
|
global.rg.timeout_ticks = REGROWTH_TIMEOUT_TICKS
|
|
end
|
|
|
|
function TriggerCleanup()
|
|
global.rg.force_removal_flag = game.tick
|
|
end
|
|
|
|
function RegrowthForceRemoveChunksCmd(cmd_table)
|
|
if (game.players[cmd_table.player_index].admin) then
|
|
TriggerCleanup()
|
|
end
|
|
end
|
|
|
|
-- Get the next player index available
|
|
function GetNextPlayerIndex(player_index)
|
|
if (not global.rg.player_refresh_index or not game.players[global.rg.player_refresh_index]) then
|
|
global.rg.player_refresh_index = 1
|
|
else
|
|
global.rg.player_refresh_index = global.rg.player_refresh_index + 1
|
|
end
|
|
|
|
if (global.rg.player_refresh_index > #game.players) then
|
|
global.rg.player_refresh_index = 1
|
|
end
|
|
|
|
return global.rg.player_refresh_index
|
|
end
|
|
|
|
-- Adds new chunks to the global table to track them.
|
|
-- This should always be called first in the chunk generate sequence
|
|
-- (Compared to other RSO & Oarc related functions...)
|
|
function RegrowthChunkGenerate(event)
|
|
local c_pos = GetChunkPosFromTilePos(event.area.left_top)
|
|
|
|
-- Surface must be "added" first.
|
|
if (global.rg == nil) then return end
|
|
|
|
-- If this is the first chunk in that row:
|
|
if (global.rg.map[c_pos.x] == nil) then
|
|
global.rg.map[c_pos.x] = {}
|
|
end
|
|
|
|
-- Only update it if it isn't already set!
|
|
if (global.rg.map[c_pos.x][c_pos.y] == nil) then
|
|
global.rg.map[c_pos.x][c_pos.y] = game.tick
|
|
end
|
|
end
|
|
|
|
-- Mark an area for "immediate" forced removal
|
|
function RegrowthMarkAreaForRemoval(pos, chunk_radius)
|
|
local c_pos = GetChunkPosFromTilePos(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 (global.rg.map[x] ~= nil) then
|
|
global.rg.map[x][y] = nil
|
|
end
|
|
table.insert(global.rg.removal_list, {pos={x=x,y=y},force=true})
|
|
end
|
|
if (table_size(global.rg.map[x]) == 0) then
|
|
global.rg.map[x] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Downgrades permanent flag to semi-permanent.
|
|
function RegrowthMarkAreaNotPermanentOVERWRITE(pos, chunk_radius)
|
|
local c_pos = GetChunkPosFromTilePos(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 (global.rg.map[x] and global.rg.map[x][y] and (global.rg.map[x][y] == -2)) then
|
|
global.rg.map[x][y] = -1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Marks a chunk containing a position to be relatively permanent.
|
|
function MarkChunkSafe(c_pos, permanent)
|
|
if (global.rg.map[c_pos.x] == nil) then
|
|
global.rg.map[c_pos.x] = {}
|
|
end
|
|
|
|
if (permanent) then
|
|
global.rg.map[c_pos.x][c_pos.y] = -2
|
|
|
|
-- Make sure we don't overwrite...
|
|
elseif (global.rg.map[c_pos.x][c_pos.y] and (global.rg.map[c_pos.x][c_pos.y] ~= -2)) then
|
|
global.rg.map[c_pos.x][c_pos.y] = -1
|
|
end
|
|
end
|
|
|
|
-- Marks a safe area around a CHUNK position to be relatively permanent.
|
|
function RegrowthMarkAreaSafeGivenChunkPos(c_pos, chunk_radius, permanent)
|
|
if (global.rg == nil) then return end
|
|
|
|
for i=-chunk_radius,chunk_radius do
|
|
for j=-chunk_radius,chunk_radius do
|
|
MarkChunkSafe({x=c_pos.x+i,y=c_pos.y+j}, permanent)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Marks a safe area around a TILE position to be relatively permanent.
|
|
function RegrowthMarkAreaSafeGivenTilePos(pos, chunk_radius, permanent)
|
|
if (global.rg == nil) then return end
|
|
|
|
local c_pos = GetChunkPosFromTilePos(pos)
|
|
RegrowthMarkAreaSafeGivenChunkPos(c_pos, chunk_radius, permanent)
|
|
end
|
|
|
|
-- Refreshes timers on a chunk containing position
|
|
function RefreshChunkTimer(pos, bonus_time)
|
|
local c_pos = GetChunkPosFromTilePos(pos)
|
|
|
|
if (global.rg.map[c_pos.x] == nil) then
|
|
global.rg.map[c_pos.x] = {}
|
|
end
|
|
if (global.rg.map[c_pos.x][c_pos.y] >= 0) then
|
|
global.rg.map[c_pos.x][c_pos.y] = game.tick + bonus_time
|
|
end
|
|
end
|
|
|
|
-- Refreshes timers on all chunks around a certain area
|
|
function RefreshArea(pos, chunk_radius, bonus_time)
|
|
local c_pos = GetChunkPosFromTilePos(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 (global.rg.map[x] == nil) then
|
|
global.rg.map[x] = {}
|
|
end
|
|
if ((global.rg.map[x][y] == nil) or (global.rg.map[x][y] >= 0)) then
|
|
global.rg.map[x][y] = game.tick + bonus_time
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Refreshes timers on all chunks near an ACTIVE radar
|
|
function RegrowthSectorScan(event)
|
|
if (event.radar.surface.name ~= GAME_SURFACE_NAME) then return end
|
|
|
|
RefreshArea(event.radar.position, 14, 0)
|
|
end
|
|
|
|
-- Refresh all chunks near a single player. Cyles through all connected players.
|
|
function RefreshPlayerArea()
|
|
player_index = GetNextPlayerIndex()
|
|
if (player_index and game.connected_players[player_index]) then
|
|
local player = game.connected_players[player_index]
|
|
|
|
if (not player.character) then return end
|
|
if (player.character.surface.name ~= GAME_SURFACE_NAME) then return end
|
|
|
|
RefreshArea(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.
|
|
function RegrowthSingleStepArray()
|
|
|
|
-- Make sure we have a valid iterator!
|
|
if (not global.rg.chunk_iter or not global.rg.chunk_iter.valid) then
|
|
global.rg.chunk_iter = game.surfaces[GAME_SURFACE_NAME].get_chunks()
|
|
end
|
|
|
|
local next_chunk = global.rg.chunk_iter()
|
|
|
|
-- Check if we reached the end
|
|
if (not next_chunk) then
|
|
global.rg.chunk_iter = game.surfaces[GAME_SURFACE_NAME].get_chunks()
|
|
next_chunk = global.rg.chunk_iter()
|
|
end
|
|
|
|
-- Do we have it in our map?
|
|
if (not global.rg.map[next_chunk.x] or not global.rg.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 = global.rg.map[next_chunk.x][next_chunk.y]
|
|
if ((c_timer ~= nil) and (c_timer >= 0) and ((c_timer + global.rg.timeout_ticks) < game.tick)) then
|
|
|
|
-- Check chunk actually exists
|
|
if (game.surfaces[GAME_SURFACE_NAME].is_chunk_generated({x=next_chunk.x, y=next_chunk.y})) then
|
|
table.insert(global.rg.removal_list, {pos={x=next_chunk.x, y=next_chunk.y}, force=false})
|
|
global.rg.map[next_chunk.x][next_chunk.y] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Remove all chunks at same time to reduce impact to FPS/UPS
|
|
function OarcRegrowthRemoveAllChunks()
|
|
for key,c_remove in pairs(global.rg.removal_list) do
|
|
local c_pos = c_remove.pos
|
|
|
|
-- Confirm chunk is still expired
|
|
if (not global.rg.map[c_pos.x] or not global.rg.map[c_pos.x][c_pos.y]) then
|
|
|
|
-- If it is FORCE removal, then remove it regardless of pollution.
|
|
if (c_remove.force) then
|
|
game.surfaces[GAME_SURFACE_NAME].delete_chunk(c_pos)
|
|
|
|
-- If it is a normal timeout removal, don't do it if there is pollution in the chunk.
|
|
elseif (game.surfaces[GAME_SURFACE_NAME].get_pollution({c_pos.x*32,c_pos.y*32}) > 0) then
|
|
global.rg.map[c_pos.x][c_pos.y] = game.tick
|
|
|
|
-- Else delete the chunk
|
|
else
|
|
game.surfaces[GAME_SURFACE_NAME].delete_chunk(c_pos)
|
|
end
|
|
end
|
|
|
|
-- Remove entry
|
|
global.rg.removal_list[key] = nil
|
|
end
|
|
|
|
-- MUST GET A NEW CHUNK ITERATOR ON DELETE CHUNK!
|
|
global.rg.chunk_iter = nil
|
|
global.rg.world_eater_iter = nil
|
|
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.
|
|
function RegrowthOnTick()
|
|
|
|
-- Every half a second, refresh all chunks near a single player
|
|
-- Cyles through all players. Tick is offset by 2
|
|
if ((game.tick % (30)) == 2) then
|
|
RefreshPlayerArea()
|
|
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 i=1,20 do
|
|
RegrowthSingleStepArray()
|
|
end
|
|
|
|
if (not global.world_eater_disable) then
|
|
WorldEaterSingleStep()
|
|
end
|
|
|
|
-- Allow enable/disable of auto cleanup, can change during runtime.
|
|
local interval_ticks = global.rg.timeout_ticks
|
|
-- Send a broadcast warning before it happens.
|
|
if ((game.tick % interval_ticks) == interval_ticks-(60*30 + 1)) then
|
|
if (#global.rg.removal_list > 100) then
|
|
SendBroadcastMsg("Map cleanup in 30 seconds... Unused and old map chunks will be deleted!")
|
|
end
|
|
end
|
|
|
|
-- Delete all listed chunks across all active surfaces
|
|
if ((game.tick % interval_ticks) == interval_ticks-1) then
|
|
if (#global.rg.removal_list > 100) then
|
|
OarcRegrowthRemoveAllChunks()
|
|
SendBroadcastMsg("Map cleanup done, sorry for your loss.")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- This function removes any chunks flagged but on demand.
|
|
-- Controlled by the global.rg.force_removal_flag
|
|
-- This function may be used outside of the normal regrowth modse.
|
|
function RegrowthForceRemovalOnTick()
|
|
-- Catch force remove flag
|
|
if (game.tick == global.rg.force_removal_flag+60) then
|
|
SendBroadcastMsg("Map cleanup (forced) in 30 seconds... Unused and old map chunks will be deleted!")
|
|
end
|
|
|
|
if (game.tick == global.rg.force_removal_flag+(60*30 + 60)) then
|
|
OarcRegrowthRemoveAllChunks()
|
|
SendBroadcastMsg("Map cleanup done, sorry for your loss.")
|
|
end
|
|
end
|
|
|
|
function WorldEaterSingleStep()
|
|
|
|
-- Make sure we have a valid iterator!
|
|
if (not global.rg.world_eater_iter or not global.rg.world_eater_iter.valid) then
|
|
global.rg.world_eater_iter = game.surfaces[GAME_SURFACE_NAME].get_chunks()
|
|
end
|
|
|
|
local next_chunk = global.rg.world_eater_iter()
|
|
|
|
-- Check if we reached the end
|
|
if (not next_chunk) then
|
|
global.rg.world_eater_iter = game.surfaces[GAME_SURFACE_NAME].get_chunks()
|
|
next_chunk = global.rg.world_eater_iter()
|
|
end
|
|
|
|
-- Do we have it in our map?
|
|
if (not global.rg.map[next_chunk.x] or not global.rg.map[next_chunk.x][next_chunk.y]) then
|
|
return -- Chunk isn't in our map so we don't care?
|
|
end
|
|
|
|
-- Search for any abandoned radars and destroy them?
|
|
local entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{area=next_chunk.area,
|
|
force={global.ocore.abandoned_force},
|
|
name="radar"}
|
|
for k,v in pairs(entities) do
|
|
v.die(nil)
|
|
end
|
|
|
|
-- Search for any entities with _DESTROYED_ force and kill them.
|
|
entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{area=next_chunk.area,
|
|
force={global.ocore.destroyed_force}}
|
|
for k,v in pairs(entities) do
|
|
v.die(nil)
|
|
end
|
|
|
|
-- If the chunk isn't marked permament, then check if we can remove it
|
|
local c_timer = global.rg.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 entities = game.surfaces[GAME_SURFACE_NAME].find_entities_filtered{area=area, force={"enemy", "neutral"}, invert=true}
|
|
local total_count = #entities
|
|
local has_last_user_set = false
|
|
|
|
if (total_count > 0) then
|
|
for k,v in pairs(entities) do
|
|
if (v.last_user or (v.type == "character")) then
|
|
has_last_user_set = true
|
|
return -- 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 k,v in pairs(entities) do
|
|
v.die(nil)
|
|
end
|
|
-- SendBroadcastMsg(next_chunk.x .. "," .. next_chunk.y .. " WorldEaterSingleStep - ENTITIES FOUND")
|
|
global.rg.map[next_chunk.x][next_chunk.y] = game.tick -- Set the timer on it.
|
|
end
|
|
else
|
|
-- SendBroadcastMsg(next_chunk.x .. "," .. next_chunk.y .. " WorldEaterSingleStep - NO ENTITIES FOUND")
|
|
global.rg.map[next_chunk.x][next_chunk.y] = game.tick -- Set the timer on it.
|
|
end
|
|
end
|
|
end
|