From 4f11d8b07e5c5b53144dfce9adf92b1c0356edf9 Mon Sep 17 00:00:00 2001 From: secXsQuared Date: Sat, 27 Jan 2018 04:23:18 -0500 Subject: [PATCH] Added interval for each entity. Optimized event regrowth algorithem and structure check. --- LICENSE | 21 ++ modinfo.lua | 4 +- modmain.lua | 86 +++++++- scripts/components/event_regrowth.lua | 249 +++++++++++++++++------- scripts/components/natural_regrowth.lua | 140 ++++++++----- 5 files changed, 364 insertions(+), 136 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b71b600 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 secXsQuared + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/modinfo.lua b/modinfo.lua index 81cf80c..a65c70c 100644 --- a/modinfo.lua +++ b/modinfo.lua @@ -1,7 +1,7 @@ -name = "World Regrowth Ex" +name = "Advanced World Regrowth" description = "Advanced world regrowth" author = "lolo" -version = "0.0.1" +version = "1.0.0" forumthread = "" diff --git a/modmain.lua b/modmain.lua index 155e080..8b210f8 100644 --- a/modmain.lua +++ b/modmain.lua @@ -1,11 +1,19 @@ if GLOBAL.STRINGS.NAMES.MIGRATION_PORTAL then - AddPrefabPostInit("world", function(inst) + -- we have caves + AddPrefabPostInit("forest", function(inst) + if inst.ismastersim then + inst:AddComponent("natural_regrowth") + inst:AddComponent("event_regrowth") + end + end) + AddPrefabPostInit("cave", function(inst) if inst.ismastersim then inst:AddComponent("natural_regrowth") inst:AddComponent("event_regrowth") end end) else + -- only overworld AddPrefabPostInit("world", function(inst) if inst.ismastersim then inst:AddComponent("natural_regrowth") @@ -14,18 +22,80 @@ else end) end +local natural = +{ + --plants + berrybush = 1440, + berrybush2 = 1440, + berrybush_juicy = 1440, + carrot_planted = 240, + evergreen = 30, + deciduoustree = 30, + marsh_tree = 480, + twiggytree = 480, + flower = 240, + flower_evil = 480, + grass = 240, + blue_mushroom = 240, + red_mushroom = 240, + green_mushroom = 240, + reeds = 480, + sapling = 240, + marsh_bush = 480, + cactus = 480, + rock1 = 240, + rock2 = 240, + rock_flintless = 240, + marbletree=1440, + rock_moon = 480, + stalagmite = 240, + stalagmite_tall = 240, +} + +local event = +{ + houndbone = 960, + pighead = 960, + marblepillar = 1440, + livingtree = 960, + mandrake = 960, + beehive = 480, + wasphive = 960, + houndmound = 1440, + pighouse = 960, + mermhouse = 960, + spiderden = 960, + molehill = 960, + catcoonden = 960, + tentacle = 480, + rabbithole = 480, + fireflies = 480, + knight = 23, + bishop = 9, + rook = 34, + knight_nightmare = 1440, + bishop_nightmare = 1440, + rook_nightmare = 1440, + monkeypods = 1440, + ruins_statue_mage = 960, + ruins_statue_mage_nogem = 960, + ruins_statue_head = 960, + ruins_statue_head_nogem = 960, + rabbithouse = 960 +} + AddComponentPostInit("natural_regrowth", function(component) - component:RegisterRegrowth("evergreen", "evergreen") + for prefab, time in pairs(natural) do + component:RegisterRegrowth(prefab, prefab, time) + end end) AddComponentPostInit("event_regrowth", function(component) - component:RegisterRegrowth("knight", "knight") - component:RegisterRegrowth("evergreen", "evergreen") - component:RegisterRegrowth("grass", "grass") - component:RegisterRegrowth("rock1", "rock1") - component:RegisterRegrowth("rock2", "rock2") + for prefab, time in pairs(event) do + component:RegisterRegrowth(prefab, prefab, time) + end component:FinishModConfig() -end) +end) --"forest" for the overworld diff --git a/scripts/components/event_regrowth.lua b/scripts/components/event_regrowth.lua index d7f3a3a..a86c0f5 100644 --- a/scripts/components/event_regrowth.lua +++ b/scripts/components/event_regrowth.lua @@ -1,25 +1,36 @@ -------------------------------------------------------------------------- --[[ EventRegrowth class definition ]] +-- A modified 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 change the API (they shouldn't) +-- by lolo Jan. 2018 -------------------------------------------------------------------------- return Class(function(self, inst) - - assert(TheWorld.ismastersim, "event_regrowth should not exist on client") + + assert(inst.ismastersim, "event_regrowth should not exist on client") require "map/terrain" -------------------------------------------------------------------------- --[[ Constants ]] -------------------------------------------------------------------------- - local DEBUG = true - local DEBUG_TELE = true - local RETRY_PER_PREFAB = 10 -- retry 5 times for each prefab - local UPDATE_PERIOD = 3 -- less likely to update on the same frame as others + local DEBUG_TELE = false + + local UPDATE_PERIOD = 11 local BASE_RADIUS = 20 - local EXCLUDE_RADIUS = 2 - local JITTER_RADIUS = 10 - local MIN_PLAYER_DISTANCE = 40 -- this is our "outer" sleep radius + local EXCLUDE_RADIUS = 3 + local JITTER_RADIUS = 6 + local TOTAL_RADIUS = 1000 + local MIN_PLAYER_DISTANCE = 40 + local THREADS_PER_BATCH = 5 -- since we retry a lot, we reduce the # of threads to guarantee performance + local THREADS_PER_BATCH_HOOK = 5 + local REGROW_STATUS = { + SUCCESS = 0, + FAILED = 1, + CACHE = 2, + } -------------------------------------------------------------------------- --[[ Member variables ]] @@ -43,83 +54,145 @@ return Class(function(self, inst) end local position = ent:GetPosition() - table.insert(entity_list[ent.prefab], position) + table.insert(entity_list[ent.prefab], {position = position, interval = regrowth_table[ent.prefab].interval}) ent:RemoveEventCallback("onremove", EntityDeathEventHandler, nil) if DEBUG then - print("[EventRegrowth] Entity of type ", ent.prefab, " was removed at ", position) + print("[EventRegrowth] ", ent.prefab, " was removed at ", position) end end local function TestForRegrow(x, y, z, tile) - if TheWorld.Map:GetTileAtPoint(x, y, z) ~= tile then + local ents = TheSim:FindEntities(x,y,z, BASE_RADIUS, nil, nil, { "structure", "wall" }) + if #ents > 0 then + -- No regrowth around players and their bases + return REGROW_STATUS.FAILED + end + + if inst.Map:GetTileAtPoint(x, y, z) ~= tile then -- keep things in their biome (more or less) - return false + return REGROW_STATUS.CACHE end local ents = TheSim:FindEntities(x,y,z, EXCLUDE_RADIUS) if #ents > 0 then -- Too dense - return false + return REGROW_STATUS.CACHE end if IsAnyPlayerInRange(x,y,z, MIN_PLAYER_DISTANCE, nil) then + return REGROW_STATUS.CACHE + end + + return REGROW_STATUS.SUCCESS + end + + -- duplicate of canregrow in natural regrowth + local function CanRegrow(x, y, z, prefab) + + if IsAnyPlayerInRange(x,y,z, MIN_PLAYER_DISTANCE, nil) then + return false + end + + local ents = TheSim:FindEntities(x,y,z, EXCLUDE_RADIUS) + if #ents > 0 then + -- Too dense return false end local ents = TheSim:FindEntities(x,y,z, BASE_RADIUS, nil, nil, { "structure", "wall" }) if #ents > 0 then - -- No regrowth around players and their bases + -- Don't spawn inside bases + return false + end + + if not (inst.Map:CanPlantAtPoint(x, y, z) and + inst.Map:CanPlacePrefabFilteredAtPoint(x, y, z, prefab)) + or (RoadManager ~= nil and RoadManager:IsOnRoad(x, 0, z)) then + -- Not ground we can grow on return false end return true end - local function TryRegrowth(prefab, product, position) - local x = position.x - local y = position.y - local z = position.z - - local orig_tile = TheWorld.Map:GetTileAtPoint(x,y,z) + local function GetRandomLocation(x, y, z, radius) local theta = math.random() * 2 * PI - local radius = math.random() * JITTER_RADIUS + local radius = math.random() * radius local x = x + radius * math.cos(theta) local z = z - radius * math.sin(theta) - - if TestForRegrow(x,y,z, orig_tile) then - local instance = SpawnPrefab(product) - if instance ~= nil then - instance.Transform:SetPosition(x,y,z) - instance:ListenForEvent("onremove", EntityDeathEventHandler, nil) - - if DEBUG then - print("[EventRegrowth] Spawned a ",product," for prefab ",prefab," at ", "(", x,0,z, ")") - end - - if DEBUG_TELE then - c_teleport(x,0,z) - end - end - return true - else - return false - end + return x,y,z end - local function HookAllEntities() + local function TryRegrowth(prefab, product, position) + local x,y,z = GetRandomLocation(position.x,position.y,position.z,JITTER_RADIUS) + local orig_tile = inst.Map:GetTileAtPoint(x,y,z) + local status = TestForRegrow(x,y,z, orig_tile) + + if status == REGROW_STATUS.CACHE then + if DEBUG then + print("[EventRegrowth] Cached a ",product," for prefab ",prefab," at ", x, ",", y,",",z) + end + return false + end + + if status == REGROW_STATUS.FAILED then + -- for the failed case, we want to try spawning at a random location + x,y,z = GetRandomLocation(position.x,position.y,position.z,TOTAL_RADIUS) + + if not CanRegrow(x,y,z, product) then + -- if cannot regrow, return CACHE status + if DEBUG then + print("[EventRegrowth] Failed to spawn a ",product," for prefab ",prefab," at ", x, ",", y,",",z) + end + return false + end + end + + local instance = SpawnPrefab(product) + if instance ~= nil then + instance.Transform:SetPosition(x,y,z) + instance:ListenForEvent("onremove", EntityDeathEventHandler, nil) + + if DEBUG then + print("[EventRegrowth] Spawned a ",product," for prefab ",prefab," at ", x, ",", y,",",z) + end + + if DEBUG_TELE then + c_teleport(x,0,z) + end + + end + + return true + end + + local function HookEntities(prefab) while next(Ents) == nil do end + local count = 0 for k, v in pairs(Ents) do - if regrowth_table[v.prefab] ~= nil then + if v.prefab == prefab then v:RemoveEventCallback("onremove", EntityDeathEventHandler, nil) v:ListenForEvent("onremove", EntityDeathEventHandler, nil) count = count + 1 end end if DEBUG then - print("[EventRegrowth] Hooked ", count, " entities.") + print("[EventRegrowth] Hooked ", count, " ",prefab) + end + end + + local function HookAllEntities(ents) + local count = 0 + local delay = 0 + for prefab in pairs(ents) do + inst:DoTaskInTime(delay, function() HookEntities(prefab) end) + count = count + 1 + if math.fmod(count, THREADS_PER_BATCH_HOOK) == 0 then + delay = delay + 1 + end end end @@ -130,11 +203,15 @@ return Class(function(self, inst) regrowth_table_populated_by_mod = true end - function self:RegisterRegrowth(prefab, product) + function self:RegisterRegrowth(prefab, product, interval) if regrowth_table[prefab] == nil then -- avoid duplicate registration - regrowth_table[prefab] = product - HookAllEntities() + regrowth_table[prefab] = + { + product = product, + interval = interval + } + HookEntities(prefab) end if DEBUG then @@ -147,12 +224,23 @@ return Class(function(self, inst) -------------------------------------------------------------------------- inst:DoPeriodicTask(UPDATE_PERIOD, function() self:LongUpdate(UPDATE_PERIOD) end) + inst:ListenForEvent("ms_cyclecomplete", function() HookAllEntities(regrowth_table) end) -- every ~ 1 day we rehook every entities + inst:DoTaskInTime(0, function() HookAllEntities(regrowth_table) end) - inst:DoPeriodicTask(99, HookAllEntities, 0) -------------------------------------------------------------------------- --[[ Update ]] -------------------------------------------------------------------------- - + local function RegrowPrefabTask(prefab) + for i = #entity_list[prefab],1,-1 do + local success = TryRegrowth(prefab, regrowth_table[prefab].product, entity_list[prefab][i].position) + + if success then + -- remove from the list if it's success or failed + table.remove(entity_list[prefab], i) + end + end + end + function self:LongUpdate(dt) if not regrowth_table_populated_by_mod then -- do nothing if the table is not fully initialized @@ -160,40 +248,39 @@ return Class(function(self, inst) return end + local count = 0 + local delay = 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 DEBUG then - print("[EventRegrowth] Regrowing ", prefab, "...") - end if regrowth_table[prefab] == nil then -- if we don't have it registered, discard entity_list[prefab] = nil - if DEBUG then - print("[EventRegrowth] Discarded") - end else - for i = #entity_list[prefab],1,-1 do + for i = 1, #entity_list[prefab] do + -- 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("[EventRegrowth] Spawning at location", entity_list[prefab][i]) + print("[EventRegrowth]", prefab, " at ", entity_list[prefab][i].position, " has interval ", entity_list[prefab][i].interval ) end - local attempts = 0 - while attempts < RETRY_PER_PREFAB do - local success = TryRegrowth(prefab, regrowth_table[prefab], entity_list[prefab][i]) - attempts = attempts + 1 - - if success then - print("[EventRegrowth] Succeeded after ", attempts, " attempts.") - -- we respawned this guy, remove from the list - table.remove(entity_list[prefab], i) - break + + if entity_list[prefab][i].interval == 0 then + -- different threads + inst:DoTaskInTime(delay, function() RegrowPrefabTask(prefab) end) + + -- try not to flood the server with threads + count = count + 1 + if math.fmod( count,THREADS_PER_BATCH ) == 0 then + delay = delay + 1 end end - - if DEBUG and attempts == RETRY_PER_PREFAB then - print("[EventRegrowth] Failed after ", attempts, " attempts.") - end end end end @@ -205,11 +292,27 @@ return Class(function(self, inst) -------------------------------------------------------------------------- function self:OnSave() - + local data = { + entities = {} + } + for prefab in pairs(entity_list) do + data.entities[prefab] = {} + for i = 1, #entity_list[prefab] do + table.insert(data.entities[prefab], {interval = entity_list[prefab][i].interval, position = entity_list[prefab][i].position}) + 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] = {} + end + for i = 1, #data.entities[prefab] do + table.insert(entity_list[prefab], {interval = data.entities[prefab][i].interval, position = data.entities[prefab][i].position}) + end + end end -------------------------------------------------------------------------- diff --git a/scripts/components/natural_regrowth.lua b/scripts/components/natural_regrowth.lua index 0fb59b5..75ab353 100644 --- a/scripts/components/natural_regrowth.lua +++ b/scripts/components/natural_regrowth.lua @@ -3,27 +3,25 @@ -- A modified version of the original desolationspawner.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 change the API (they shouldn't) --- Klei has copyright over existing code used in this file. --- by lolo Jan. 2018. +-- by lolo Jan. 2018 -------------------------------------------------------------------------- return Class(function(self, inst) - assert(TheWorld.ismastersim, "natrual_regrowth should not exist on client") + assert(inst.ismastersim, "natrual_regrowth should not exist on client") require "map/terrain" -------------------------------------------------------------------------- --[[ Constants ]] -------------------------------------------------------------------------- - local RETRY_PER_PREFAB = 10 -- retry 5 times for each prefab local DEBUG = false local DEBUG_TELE = false - local UPDATE_PERIOD = 31 -- less likely to update on the same frame as others + local UPDATE_PERIOD = 9 local BASE_RADIUS = 20 - local EXCLUDE_RADIUS = 2 - local MIN_PLAYER_DISTANCE = 40 -- this is our "outer" sleep radius - + local EXCLUDE_RADIUS = 3 + local MIN_PLAYER_DISTANCE = 40 + local THREADS_PER_BATCH = 5 -------------------------------------------------------------------------- --[[ Member variables ]] -------------------------------------------------------------------------- @@ -34,6 +32,7 @@ return Class(function(self, inst) --Private local regrowth_table = {} local area_data = {} + local intervals = {} -------------------------------------------------------------------------- --[[ Private member functions ]] @@ -57,8 +56,8 @@ return Class(function(self, inst) return false end - if not (TheWorld.Map:CanPlantAtPoint(x, y, z) and - TheWorld.Map:CanPlacePrefabFilteredAtPoint(x, y, z, prefab)) + if not (inst.Map:CanPlantAtPoint(x, y, z) and + inst.Map:CanPlacePrefabFilteredAtPoint(x, y, z, prefab)) or (RoadManager ~= nil and RoadManager:IsOnRoad(x, 0, z)) then -- Not ground we can grow on return false @@ -67,13 +66,13 @@ return Class(function(self, inst) end local function TryRegrowth(area, prefab, product) - if TheWorld.topology.nodes[area] == nil then + if inst.topology.nodes[area] == nil then return false end - local points_x, points_y = TheWorld.Map:GetRandomPointsForSite(TheWorld.topology.nodes[area].x, TheWorld.topology.nodes[area].y, TheWorld.topology.nodes[area].poly, 1) + local points_x, points_y = inst.Map:GetRandomPointsForSite(inst.topology.nodes[area].x, inst.topology.nodes[area].y, inst.topology.nodes[area].poly, 1) if #points_x < 1 or #points_y < 1 then - return + return false end local x = points_x[1] local z = points_y[1] @@ -95,20 +94,32 @@ return Class(function(self, inst) return true else - + if DEBUG then + print("[NaturalRegrowth] Failed to spawn a ",product," for prefab ",prefab," at ", "(", x,0,z, ")", " in ", area) + end return false end end - + + local function PrintDensities() + for area, densities in pairs(inst.generated.densities) do + for k,v in pairs(densities) do + print(area, k, v) + end + end + end + local function PopulateAreaData(prefab) - if TheWorld.generated == nil then + if inst.generated == nil then -- Still starting up, not ready yet. return end - for area, densities in pairs(TheWorld.generated.densities) do + -- PrintDensities() + + for area, densities in pairs(inst.generated.densities) do if densities[prefab] ~= nil then - for id, v in ipairs(TheWorld.topology.ids) do + for id, v in ipairs(inst.topology.ids) do if v == area then if area_data[prefab] == nil then area_data[prefab] = {} @@ -120,11 +131,15 @@ return Class(function(self, inst) end end end + + if DEBUG then + print("[NaturalRegrowth] Populated ", area_data[prefab] == nil and 0 or #area_data[prefab], " areas for ", prefab) + end end local function PopulateAllAreaData() -- This has to be run after 1 frame from startup - for prefab, _ in pairs(regrowth_table) do + for prefab in pairs(regrowth_table) do PopulateAreaData(prefab) end end @@ -133,11 +148,16 @@ return Class(function(self, inst) --[[ Public member functions ]] -------------------------------------------------------------------------- - function self:RegisterRegrowth(prefab, product) + function self:RegisterRegrowth(prefab, product, interval) if DEBUG then - print("Registered ", product, " for prefab " ,prefab ) + print("[NaturalRegrowth] Registered ", product, " for prefab " ,prefab ) end - regrowth_table[prefab] = product + regrowth_table[prefab] = {product = product, interval = interval} + + if intervals[prefab] == nil then + intervals[prefab] = interval + end + PopulateAreaData(prefab) end @@ -152,39 +172,44 @@ return Class(function(self, inst) -------------------------------------------------------------------------- --[[ Update ]] -------------------------------------------------------------------------- + + local function RegrowPrefabTask(areas, prefab) + local rand = math.random(1, #areas) + local success = TryRegrowth(areas[rand], prefab, regrowth_table[prefab].product) + if success then + -- success, reset the timer + intervals[prefab] = regrowth_table[prefab] == nil and nil or regrowth_table[prefab].interval + end + end function self:LongUpdate(dt) + local count = 0 + local delay = 0 for prefab in pairs(area_data) do - - if DEBUG then - print("[NaturalRegrowth] Regrowing ", prefab, "...") - end - local areas = area_data[prefab] if regrowth_table[prefab] == nil then - if DEBUG then - print("[NaturalRegrowth] Discarded") - end area_data[prefab] = nil + intervals[prefab] = nil else - local rand = math.random(1, #areas) - local attempts = 0 - - while attempts < RETRY_PER_PREFAB do - local success = TryRegrowth(areas[rand], prefab, regrowth_table[prefab]) - attempts = attempts + 1 - - if success then - if DEBUG then - print("[NaturalRegrowth] Succeeded after ", attempts, " attempts.") - end - break - end + if intervals[prefab] > UPDATE_PERIOD then + intervals[prefab] = intervals[prefab] - UPDATE_PERIOD + else + intervals[prefab] = 0 + end + + if DEBUG then + print("[NaturalRegrowth]", prefab, " has interval ", intervals[prefab]) end - if DEBUG and attempts == RETRY_PER_PREFAB then - print("[NaturalRegrowth] Failed after ", attempts, " attempts.") + if intervals[prefab] == 0 then + -- use multiple threads? In the future a threadpool maybe? + inst:DoTaskInTime(delay, function() RegrowPrefabTask(areas,prefab) end) + -- try not to flood the server with threads + count = count + 1 + if math.fmod( count,THREADS_PER_BATCH ) == 0 then + delay = delay + 1 + end end end end @@ -196,25 +221,34 @@ return Class(function(self, inst) function self:OnSave() local data = { - areas = {} + areas = {}, + intervals = {} } for prefab in pairs(area_data) do data.areas[prefab] = {} - for area in pairs(area_data[prefab]) do - table.insert(data.areas[prefab], area) + + for i = 1, #area_data[prefab] do + table.insert(data.areas[prefab], area_data[prefab][i]) end end + for prefab, interval in pairs(intervals) do + data.intervals[prefab] = interval + end return data end function self:OnLoad(data) for prefab in pairs(data.areas) do - for area in pairs(data.areas) do - if area_data[prefab] == nil then - area_data[prefab] = {} - end - table.insert(area_data[prefab], area) + if area_data[prefab] == nil then + area_data[prefab] = {} end + for i = 1, #data.areas[prefab] do + table.insert(area_data[prefab], data.areas[prefab][i]) + end + end + + for prefab, interval in pairs(data.intervals) do + intervals[prefab] = interval end end