world_regrowth_pp/src/scripts/components/extra_regrowth.lua

447 lines
16 KiB
Lua

--------------------------------------------------------------------------
--[[ EventRegrowth class definition ]]
-- A modified and more feature-rich version of the original regrowthmanager.lua
-- It acts as a standalone regrowth manager and is independent of the 3 existing ones
-- It's unlikely affected by game updates as long as Klei doesn't break the API (they shouldn't)
-- quackerd
--------------------------------------------------------------------------
return Class(function(self, inst)
assert(inst.ismastersim, "extra_regrowth should not exist on client")
require "map/terrain"
require "ocean_util"
--------------------------------------------------------------------------
--[[ Constants ]]
--------------------------------------------------------------------------
local DEBUG = false
local DEBUG_TELE = false
local UPDATE_PERIOD = 9 -- 9 seconds per tick
local JITTER_RADIUS = 1 -- random point within this radius
local MAX_RADIUS = 1000 -- cap the max radius
local INC_RADIUS = 3 -- increase this amount after we fail
local BASE_RADIUS = 15 -- don't spawn near player's base
local EXCLUDE_RADIUS = 2 -- no other entities in this radius within the spawn point
local MIN_PLAYER_DISTANCE = 30 -- minimum distance of player to spawn entities
local EXCLUDE_TAGS =
{
"statue", -- marble stuff on the ground, ancient statues
"hive", --spiderden, wasphive, beehive
} -- these aren't considered structures
local EXCLUDE_PREFABS =
{
catcoonden = 1,
ancient_altar = 1,
ancient_altar_broken = 1,
houndmound = 1,
mermhouse = 1,
pigtorch = 1,
mermhead = 1,
pighead = 1,
pandoraschest = 1,
minotaurchest = 1,
pighouse = 1,
rabbithouse = 1,
chessjunk1 = 1,
chessjunk2 = 1,
chessjunk3 = 1,
wall_ruins = 1,
} -- these aren't considered structures
local REGROW_STATUS =
{
SUCCESS = 0,
STRUCT = 1,
CACHE = 2,
PLAYER = 3,
DENSITY = 4,
TILE = 5,
ROAD = 6,
OCEAN = 7
}
local CACHE_RETRY =
{
[REGROW_STATUS.PLAYER] = true,
} -- we don't increase the radius for these reasons
--------------------------------------------------------------------------
--[[ Member variables ]]
--------------------------------------------------------------------------
--Public
self.inst = inst
--Private
local ready = false
local regrowth_table = {}
local entity_list = {}
--------------------------------------------------------------------------
--[[ Private member functions ]]
--------------------------------------------------------------------------
local function TestStructures(x, y, z, radius)
local ents = TheSim:FindEntities(x,y,z, BASE_RADIUS, nil, EXCLUDE_TAGS, { "structure", "wall" })
for i, v in ipairs(ents) do
if EXCLUDE_PREFABS[v.prefab] == nil then
-- if we cannot find it from the exclude table, then it is a structure and we failed the test
return false
end
end
return true
end
local function CanPlaceAtPoint(x, y, z)
local tile = TheWorld.Map:GetTileAtPoint(x, y, z)
return tile ~= GROUND.IMPASSABLE and
tile ~= GROUND.INVALID and
not GROUND_FLOORING[tile]
end
local function TestPlayers(x, y, z, radius)
return not IsAnyPlayerInRange(x,y,z, MIN_PLAYER_DISTANCE, nil)
end
local function TestEntities(x, y, z, radius)
local ents = TheSim:FindEntities(x,y,z, EXCLUDE_RADIUS)
return not (#ents > 0)
end
local function TestRegrowth(x, y, z, prefab, tile)
local cur_tile = TheWorld.Map:GetTileAtPoint(x, y, z)
if not TestPlayers(x,y,z, MIN_PLAYER_DISTANCE) then
return REGROW_STATUS.PLAYER
end
if not TestStructures(x, y, z, BASE_RADIUS) then
-- No regrowth around players and their bases
return REGROW_STATUS.STRUCT
end
if not TestEntities(x,y,z, EXCLUDE_RADIUS) then
-- Too dense
return REGROW_STATUS.DENSITY
end
if (RoadManager ~= nil) and (RoadManager:IsOnRoad(x, 0, z)) then
return REGROW_STATUS.ROAD
end
-- hack to get away with it for now
if IsOceanTile(cur_tile) then
return REGROW_STATUS.OCEAN
end
if (CanPlaceAtPoint(x, y, z) and TheWorld.Map:CanPlacePrefabFilteredAtPoint(x, y, z, prefab)) or ((tile ~= nil) and (cur_tile == tile)) then
return REGROW_STATUS.SUCCESS
end
return REGROW_STATUS.TILE
end
local function GetPosStr(pos)
return "( " .. pos.x .. " , " .. pos.y .. " , ".. pos.z .. " )"
end
local function GetCoordStr(x,y,z)
return "( " .. x .. " , " .. y .. " , ".. z .. " )"
end
local function GetRStatusStr(status)
for k, v in pairs(REGROW_STATUS) do
if v == status then
return k
end
end
return nil
end
local function EntityDeathEventHandler(ent)
if entity_list[ent.prefab] == nil then
entity_list[ent.prefab] = {}
end
local position = ent:GetPosition()
entity_list[ent.prefab][#entity_list[ent.prefab]+1] =
{
position = position,
interval = regrowth_table[ent.prefab].interval,
remove=false,
retry = 0
}
ent:RemoveEventCallback("onremove", EntityDeathEventHandler, nil)
if DEBUG then
print("[ExtraRegrowth] " .. ent.prefab .. " was removed at " .. GetPosStr(position) .. " Tile: " .. TheWorld.Map:GetTileAtPoint(position.x, position.y, position.z))
end
end
local function GetRandomLocation(x, y, z, r)
local theta = math.random() * 2 * PI
local radius = math.random() * r
local x = x + radius * math.cos(theta)
local z = z - radius * math.sin(theta)
return x,y,z
end
local function TryRegrowth(prefab, product, position, rand_radius)
local x,y,z = GetRandomLocation(position.x,position.y,position.z, rand_radius)
local orig_tile = inst.Map:GetTileAtPoint(position.x, position.y, position.z)
local status = TestRegrowth(x,y,z, prefab, orig_tile)
if CACHE_RETRY[status] ~= nil then
if DEBUG then
print("[ExtraRegrowth] Cached a " .. product .. " at " .. GetCoordStr(x,y,z) .. " for " .. prefab .. " at " .. GetPosStr(position) .. " with radius " .. rand_radius .. " due to " .. GetRStatusStr(status))
end
return status
end
if status ~= REGROW_STATUS.SUCCESS then
if DEBUG then
print("[ExtraRegrowth] Failed to spawn a " .. product .. " at ".. GetCoordStr(x,y,z) .. " for " .. prefab .. " at " .. GetPosStr(position) .. " with radius ".. rand_radius .. " due to " .. GetRStatusStr(status))
end
return status
end
local instance = SpawnPrefab(product)
if instance ~= nil then
instance.Transform:SetPosition(x,y,z)
instance:ListenForEvent("onremove", EntityDeathEventHandler, nil)
if DEBUG then
print("[ExtraRegrowth] Spawned a " .. product .. " at " .. GetCoordStr(x,y,z) .. " for " .. prefab .. " at " .. GetPosStr(position) .. " with radius " .. rand_radius .. " tile: " .. TheWorld.Map:GetTileAtPoint(x,y,z))
end
if DEBUG_TELE then
c_teleport(x,0,z)
end
end
return status
end
local function HookEntities(prefab)
while next(Ents) == nil do
end
local count = 0
for k, v in pairs(Ents) do
if v.prefab == prefab then
v:RemoveEventCallback("onremove", EntityDeathEventHandler, nil)
v:ListenForEvent("onremove", EntityDeathEventHandler, nil)
count = count + 1
end
end
if DEBUG then
print("[ExtraRegrowth] Hooked " .. count .. " " .. prefab)
end
end
local function HookAllRegisteredPrefabs()
local count = 0
for guid,ent in pairs(Ents) do
if regrowth_table[ent.prefab] ~= nil then
ent:RemoveEventCallback("onremove", EntityDeathEventHandler, nil)
ent:ListenForEvent("onremove", EntityDeathEventHandler, nil)
count = count + 1
end
end
if DEBUG then
print("[ExtraRegrowth] Hooked " .. count .. " entities")
end
end
--------------------------------------------------------------------------
--[[ Public member functions ]]
--------------------------------------------------------------------------
function self:GetUpdatePeriod()
return UPDATE_PERIOD
end
function self:FinishModConfig()
HookAllRegisteredPrefabs()
ready = true
end
function self:RegisterRegrowth(prefab, product, interval)
if interval == nil then
if DEBUG then
print("[ExtraRegrowth] WARNING: interval for prefab " .. prefab .. " is null. Using default.")
end
interval = 480
end
if DEBUG then
interval = interval / 100
end
if regrowth_table[prefab] == nil then
-- avoid duplicate registration
regrowth_table[prefab] =
{
product = product,
interval = interval
}
end
if DEBUG then
print("[ExtraRegrowth] Registered product " .. product .. " for prefab " .. prefab .. " with interval " .. interval)
end
end
--------------------------------------------------------------------------
--[[ Initialization ]]
--------------------------------------------------------------------------
inst:DoTaskInTime(0, function() HookAllRegisteredPrefabs() end)
inst:DoPeriodicTask(UPDATE_PERIOD, function() self:LongUpdate() end)
inst:ListenForEvent("ms_cyclecomplete", function() HookAllRegisteredPrefabs() end) -- every ~ 1 day we rehook every entities
--------------------------------------------------------------------------
--[[ Update ]]
--------------------------------------------------------------------------
local function RegrowPrefabTask(prefab, data)
local rand_radius = JITTER_RADIUS + data.retry * INC_RADIUS
if rand_radius > MAX_RADIUS then
rand_radius = MAX_RADIUS
end
local success = TryRegrowth(prefab, regrowth_table[prefab].product, data.position, rand_radius)
if success == REGROW_STATUS.SUCCESS then
data.remove = true
end
if CACHE_RETRY[success] == nil then
-- only increase radius when not cached
data.retry = data.retry + 1
end
end
function self:LongUpdate()
if not ready then
-- do nothing if the table is not fully initialized
-- in case we accidentally drop some saved entities due to the respawn_table[prefab] == nil check
return
end
local count = 0
for prefab in pairs(entity_list) do
if entity_list[prefab] == nil or #entity_list[prefab] == 0 then
-- only do meaningful work
else
if regrowth_table[prefab] == nil then
-- if we don't have it registered, discard
entity_list[prefab] = nil
else
for i = #entity_list[prefab], 1, -1 do
if entity_list[prefab][i].remove then
-- handle expired objects first
if DEBUG then
print("[ExtraRegrowth] Removed prefab " .. prefab .. " at ".. GetPosStr(entity_list[prefab][i].position) .." from the entity list.")
end
table.remove(entity_list[prefab], i)
else
-- decrease the interval
if entity_list[prefab][i].interval > UPDATE_PERIOD then
entity_list[prefab][i].interval = entity_list[prefab][i].interval - UPDATE_PERIOD
else
-- else set to 0 and regen
entity_list[prefab][i].interval = 0
end
if DEBUG then
print("[ExtraRegrowth] Prefab " .. prefab .. " at " .. GetPosStr(entity_list[prefab][i].position) .. " has interval " .. entity_list[prefab][i].interval )
end
if entity_list[prefab][i].interval == 0 then
-- different threads
local data = entity_list[prefab][i]
RegrowPrefabTask(prefab, data)
end
end
end
end
end
end
end
--------------------------------------------------------------------------
--[[ Save/Load ]]
--------------------------------------------------------------------------
function self:OnSave()
local data = {
entities = {}
}
for prefab in pairs(entity_list) do
if entity_list[prefab] ~= nil then
-- could be nil (set in the event loop)
data.entities[prefab] = {}
for i = 1, #entity_list[prefab] do
data.entities[prefab][#data.entities[prefab] + 1] =
{
interval = entity_list[prefab][i].interval,
position =
{
x = entity_list[prefab][i].position.x,
y = entity_list[prefab][i].position.y,
z = entity_list[prefab][i].position.z
},
remove = entity_list[prefab][i].remove,
retry = entity_list[prefab][i].retry
}
end
if DEBUG then
print("[ExtraRegrowth] Saved " .. #data.entities[prefab] .. " entities for prefab " .. prefab)
end
end
end
return data
end
function self:OnLoad(data)
for prefab in pairs(data.entities) do
if entity_list[prefab] == nil then
entity_list[prefab] = {}
for i = 1, #data.entities[prefab] do
entity_list[prefab][#entity_list[prefab] + 1] =
{
interval = data.entities[prefab][i].interval,
position =
{
x = data.entities[prefab][i].position.x,
y = data.entities[prefab][i].position.y,
z = data.entities[prefab][i].position.z
},
remove = (data.entities[prefab][i].remove == nil) and false or data.entities[prefab][i].remove,
retry = (data.entities[prefab][i].retry == nil) and 0 or data.entities[prefab][i].retry
}
end
if DEBUG then
print("[ExtraRegrowth] Loaded " .. #entity_list[prefab] .. " entities for prefab " .. prefab)
end
end
end
end
--------------------------------------------------------------------------
--[[ End ]]
--------------------------------------------------------------------------
end)