Compare commits

...

13 Commits

Author SHA1 Message Date
teknomunk e34862d100 Change main spawning look to use adaptive delay based on last run with maximum period of 2.5 seconds, put some logging behind a flag, remove unused constants 2024-08-06 06:14:59 -05:00
teknomunk 6581757b88 Move raycast_line_of_sight function to mcl_mobs/functions.lua, rework group spawn point selection code, add line of sight check to group origin 2024-08-06 06:14:59 -05:00
teknomunk 2453406392 Change logging default and one log message 2024-08-06 06:14:57 -05:00
teknomunk 45ffe8ea27 Refactor spawn_group() so that each spawn point can only be used once and protection is respected 2024-08-06 06:10:36 -05:00
teknomunk 8cf0660f37 Fix passive spawning, disable some debug logging 2024-08-06 06:10:36 -05:00
teknomunk 6028cbaa88 Stop mobs that require ground from spawning in water, remove unused code and variables, some minor microoptimizations 2024-08-06 06:10:36 -05:00
teknomunk 4b3edcaed8 Drop biome group generation and replace with spawn state checks for much improved performance (but has regressions), change logging of failure to find spawn point, increase spawn attempts to 4 per second with no position retries and only a single mob or group per attempt 2024-08-06 06:10:36 -05:00
teknomunk ba1ed47747 Replace biome_check() with table.find() 2024-08-06 06:10:36 -05:00
teknomunk 6abf980d98 Implement generating biome groups for spawning (not actually used yet) 2024-08-06 06:10:36 -05:00
teknomunk d5edaaa688 Move table.* functions to separate file in mcl_utils, add table.find() and table.intersect() 2024-08-06 06:10:36 -05:00
teknomunk 9b958bed62 Move modpath to separate variable to make it easier to add new files without merge conflicts 2024-08-06 06:10:36 -05:00
teknomunk f985fcf5c5 Have the list of biomes automatically generated 2024-08-06 06:10:36 -05:00
teknomunk 52f8814876 Initial changes to spawn_check() and spawn_a_mob(), add profiling for mob spawning 2024-08-06 06:10:34 -05:00
6 changed files with 488 additions and 487 deletions

View File

@ -1,52 +1,9 @@
mcl_util = {}
dofile(minetest.get_modpath(minetest.get_current_modname()).."/roman_numerals.lua")
-- Updates all values in t using values from to*.
function table.update(t, ...)
for _, to in ipairs {...} do
for k, v in pairs(to) do
t[k] = v
end
end
return t
end
-- Updates nil values in t using values from to*.
function table.update_nil(t, ...)
for _, to in ipairs {...} do
for k, v in pairs(to) do
if t[k] == nil then
t[k] = v
end
end
end
return t
end
---Works the same as `pairs`, but order returned by keys
---
---Taken from https://www.lua.org/pil/19.3.html
---@generic T: table, K, V, C
---@param t T
---@param f? fun(a: C, b: C):boolean
---@return fun():K, V
function table.pairs_by_keys(t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0 -- iterator variable
local function iter() -- iterator function
i = i + 1
if a[i] == nil then
return nil
else
return a[i], t[a[i]]
end
end
return iter
end
local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
dofile(modpath.."/roman_numerals.lua")
dofile(modpath.."/table.lua")
local LOGGING_ON = minetest.settings:get_bool("mcl_logging_default", false)
local LOG_MODULE = "[MCL2]"

View File

@ -0,0 +1,67 @@
-- Updates all values in t using values from to*.
function table.update(t, ...)
for _, to in ipairs {...} do
for k, v in pairs(to) do
t[k] = v
end
end
return t
end
-- Updates nil values in t using values from to*.
function table.update_nil(t, ...)
for _, to in ipairs {...} do
for k, v in pairs(to) do
if t[k] == nil then
t[k] = v
end
end
end
return t
end
---Works the same as `pairs`, but order returned by keys
---
---Taken from https://www.lua.org/pil/19.3.html
---@generic T: table, K, V, C
---@param t T
---@param f? fun(a: C, b: C):boolean
---@return fun():K, V
function table.pairs_by_keys(t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0 -- iterator variable
local function iter() -- iterator function
i = i + 1
if a[i] == nil then
return nil
else
return a[i], t[a[i]]
end
end
return iter
end
function table.find(t, item)
for k,v in pairs(t) do
if v == item then return k end
end
return nil
end
function table.intersect(a, b)
local values_map = {}
for _,v in pairs(a) do values_map[v] = 1 end
for _,v in pairs(b) do values_map[v] = (values_map[v] or 0) + 1 end
-- Get all the values that are in both tables
local result = {}
for v,count in pairs(values_map) do
if count == 2 then table.insert(result, v) end
end
return result
end

View File

@ -0,0 +1,22 @@
function mcl_mobs.check_line_of_sight(origin, target)
local raycast = minetest.raycast(origin, target, false, true)
local los_blocked = false
for hitpoint in raycast do
if hitpoint.type == "node" then
--TODO type object could block vision, for example chests
local node = minetest.get_node(minetest.get_pointed_thing_position(hitpoint))
if node.name ~= "air" then
local nodef = minetest.registered_nodes[node.name]
if nodef and nodef.walkable then
los_blocked = true
break
end
end
end
end
return not los_blocked
end

View File

@ -6,6 +6,9 @@ local modname = minetest.get_current_modname()
local path = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)
mcl_mobs.fallback_node = minetest.registered_aliases["mapgen_dirt"] or "mcl_core:dirt"
dofile(path .. "/functions.lua")
--api and helpers
-- effects: sounds and particles mostly
dofile(path .. "/effects.lua")

View File

@ -20,6 +20,7 @@ local function atan(x)
return atann(x)
end
end
local raycast_line_of_sight = mcl_mobs.check_line_of_sight
local registered_fallback_node = minetest.registered_nodes[mcl_mobs.fallback_node]
@ -77,28 +78,6 @@ function mob_class:is_node_waterhazard(nodename)
end
local function raycast_line_of_sight (origin, target)
local raycast = minetest.raycast(origin, target, false, true)
local los_blocked = false
for hitpoint in raycast do
if hitpoint.type == "node" then
--TODO type object could block vision, for example chests
local node = minetest.get_node(minetest.get_pointed_thing_position(hitpoint))
if node.name ~= "air" then
local nodef = minetest.registered_nodes[node.name]
if nodef and nodef.walkable then
los_blocked = true
break
end
end
end
end
return not los_blocked
end
function mob_class:target_visible(origin)
if not origin then return end

View File

@ -8,6 +8,7 @@ local end_threshold = tonumber(minetest.settings:get("mcl_mobs_end_threshold"))
local overworld_threshold = tonumber(minetest.settings:get("mcl_mobs_overworld_threshold")) or 0
local overworld_sky_threshold = tonumber(minetest.settings:get("mcl_mobs_overworld_sky_threshold")) or 7
local overworld_passive_threshold = tonumber(minetest.settings:get("mcl_mobs_overworld_passive_threshold")) or 7
local debug_time_threshold = tonumber(minetest.settings:get("vl_debug_time_threshold")) or 1000
local get_node = minetest.get_node
local get_item_group = minetest.get_item_group
@ -33,6 +34,7 @@ local vector_floor = vector.floor
local table_copy = table.copy
local table_remove = table.remove
local pairs = pairs
local check_line_of_sight = mcl_mobs.check_line_of_sight
local logging = minetest.settings:get_bool("mcl_logging_mobs_spawn", false)
local function mcl_log (message, property)
@ -50,9 +52,8 @@ local dbg_spawn_counts = {}
local remove_far = true
local WAIT_FOR_SPAWN_ATTEMPT = 10
local FIND_SPAWN_POS_RETRIES = 16
local FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN = 8
local MAX_SPAWN_CYCLE_TIME = 2.5
local FIND_SPAWN_POS_RETRIES = 1
local MOB_SPAWN_ZONE_INNER = 24
local MOB_SPAWN_ZONE_INNER_SQ = MOB_SPAWN_ZONE_INNER^2 -- squared
@ -98,169 +99,9 @@ mcl_log("Percentage of hostile spawns are group: " .. hostile_group_percentage_s
--do mobs spawn?
local mobs_spawn = minetest.settings:get_bool("mobs_spawn", true) ~= false
local spawn_protected = minetest.settings:get_bool("mobs_spawn_protected") ~= false
local logging = minetest.settings:get_bool("mcl_logging_mobs_spawn",false)
-- THIS IS THE BIG LIST OF ALL BIOMES - used for programming/updating mobs
-- Also used for missing parameter
-- Please update the list when adding new biomes!
local list_of_all_biomes = {
-- underground:
"FlowerForest_underground",
"JungleEdge_underground",
"ColdTaiga_underground",
"IcePlains_underground",
"IcePlainsSpikes_underground",
"MegaTaiga_underground",
"Taiga_underground",
"ExtremeHills+_underground",
"JungleM_underground",
"ExtremeHillsM_underground",
"JungleEdgeM_underground",
"MangroveSwamp_underground",
-- ocean:
"RoofedForest_ocean",
"JungleEdgeM_ocean",
"BirchForestM_ocean",
"BirchForest_ocean",
"IcePlains_deep_ocean",
"Jungle_deep_ocean",
"Savanna_ocean",
"MesaPlateauF_ocean",
"ExtremeHillsM_deep_ocean",
"Savanna_deep_ocean",
"SunflowerPlains_ocean",
"Swampland_deep_ocean",
"Swampland_ocean",
"MegaSpruceTaiga_deep_ocean",
"ExtremeHillsM_ocean",
"JungleEdgeM_deep_ocean",
"SunflowerPlains_deep_ocean",
"BirchForest_deep_ocean",
"IcePlainsSpikes_ocean",
"Mesa_ocean",
"StoneBeach_ocean",
"Plains_deep_ocean",
"JungleEdge_deep_ocean",
"SavannaM_deep_ocean",
"Desert_deep_ocean",
"Mesa_deep_ocean",
"ColdTaiga_deep_ocean",
"Plains_ocean",
"MesaPlateauFM_ocean",
"Forest_deep_ocean",
"JungleM_deep_ocean",
"FlowerForest_deep_ocean",
"MushroomIsland_ocean",
"MegaTaiga_ocean",
"StoneBeach_deep_ocean",
"IcePlainsSpikes_deep_ocean",
"ColdTaiga_ocean",
"SavannaM_ocean",
"MesaPlateauF_deep_ocean",
"MesaBryce_deep_ocean",
"ExtremeHills+_deep_ocean",
"ExtremeHills_ocean",
"MushroomIsland_deep_ocean",
"Forest_ocean",
"MegaTaiga_deep_ocean",
"JungleEdge_ocean",
"MesaBryce_ocean",
"MegaSpruceTaiga_ocean",
"ExtremeHills+_ocean",
"Jungle_ocean",
"RoofedForest_deep_ocean",
"IcePlains_ocean",
"FlowerForest_ocean",
"ExtremeHills_deep_ocean",
"MesaPlateauFM_deep_ocean",
"Desert_ocean",
"Taiga_ocean",
"BirchForestM_deep_ocean",
"Taiga_deep_ocean",
"JungleM_ocean",
"MangroveSwamp_ocean",
"MangroveSwamp_deep_ocean",
-- water or beach?
"MesaPlateauFM_sandlevel",
"MesaPlateauF_sandlevel",
"MesaBryce_sandlevel",
"Mesa_sandlevel",
-- beach:
"FlowerForest_beach",
"Forest_beach",
"StoneBeach",
"ColdTaiga_beach_water",
"Taiga_beach",
"Savanna_beach",
"Plains_beach",
"ExtremeHills_beach",
"ColdTaiga_beach",
"Swampland_shore",
"MushroomIslandShore",
"JungleM_shore",
"Jungle_shore",
"BambooJungleM_shore",
"BambooJungle_shore",
"MangroveSwamp_shore",
-- dimension biome:
"Nether",
"BasaltDelta",
"CrimsonForest",
"WarpedForest",
"SoulsandValley",
"End",
-- Overworld regular:
"Mesa",
"FlowerForest",
"Swampland",
"Taiga",
"ExtremeHills",
"ExtremeHillsM",
"ExtremeHills+_snowtop",
"Jungle",
"Savanna",
"BirchForest",
"MegaSpruceTaiga",
"MegaTaiga",
"ExtremeHills+",
"Forest",
"Plains",
"Desert",
"ColdTaiga",
"MushroomIsland",
"IcePlainsSpikes",
"SunflowerPlains",
"IcePlains",
"RoofedForest",
"ExtremeHills+_snowtop",
"MesaPlateauFM_grasstop",
"JungleEdgeM",
"JungleM",
"BirchForestM",
"MesaPlateauF",
"MesaPlateauFM",
"MesaPlateauF_grasstop",
"MesaBryce",
"JungleEdge",
"SavannaM",
"MangroveSwamp",
"BambooJungle",
"BambooJungleEdge",
"BambooJungleEdgeM",
"BambooJungleM",
}
local list_of_all_biomes = {}
-- count how many mobs are in an area
local function count_mobs(pos,r,mob_type)
@ -561,11 +402,8 @@ function mcl_mobs:non_spawn_specific(mob_name,dimension,min_light,max_light)
end
function mcl_mobs:spawn_specific(name, dimension, type_of_spawning, biomes, min_light, max_light, interval, chance, aoc, min_height, max_height, day_toggle, on_spawn, check_position)
-- Do mobs spawn at all?
if not mobs_spawn then
return
end
if not mobs_spawn then return end
assert(min_height)
assert(max_height)
@ -588,19 +426,20 @@ function mcl_mobs:spawn_specific(name, dimension, type_of_spawning, biomes, min_
--load information into the spawn dictionary
local key = #spawn_dictionary + 1
spawn_dictionary[key] = {}
spawn_dictionary[key]["name"] = name
spawn_dictionary[key]["dimension"] = dimension
spawn_dictionary[key]["type_of_spawning"] = type_of_spawning
spawn_dictionary[key]["biomes"] = biomes
spawn_dictionary[key]["min_light"] = min_light
spawn_dictionary[key]["max_light"] = max_light
spawn_dictionary[key]["chance"] = chance
spawn_dictionary[key]["aoc"] = aoc
spawn_dictionary[key]["min_height"] = min_height
spawn_dictionary[key]["max_height"] = max_height
spawn_dictionary[key]["day_toggle"] = day_toggle
spawn_dictionary[key]["check_position"] = check_position
spawn_dictionary[key] = {
name = name,
dimension = dimension,
type_of_spawning = type_of_spawning,
biomes = biomes,
min_light = min_light,
max_light = max_light,
chance = chance,
aoc = aoc,
min_height = min_height,
max_height = max_height,
day_toggle = day_toggle,
check_position = check_position,
}
end
local function get_next_mob_spawn_pos(pos)
@ -678,17 +517,6 @@ local function get_next_mob_spawn_pos(pos)
return spawning_position_list[math_random(1, #spawning_position_list)]
end
--a simple helper function for mob_spawn
local function biome_check(biome_list, biome_goal)
for _, data in pairs(biome_list) do
if data == biome_goal then
return true
end
end
return false
end
local function is_farm_animal(n)
return n == "mobs_mc:pig" or n == "mobs_mc:cow" or n == "mobs_mc:sheep" or n == "mobs_mc:chicken" or n == "mobs_mc:horse" or n == "mobs_mc:donkey"
end
@ -750,85 +578,73 @@ local function get_biome_name(pos)
end
end
local function spawn_check(pos, spawn_def)
if not spawn_def or not pos then return end
local counts = {}
local function initial_spawn_check(state, node, spawn_def)
local function log_fail(reason)
local count = (counts[reason] or 0) + 1
counts[reason] = count
if logging then
minetest.log("Spawn check for "..tostring(spawn_def and spawn_def.name).." failed - "..reason.." ("..count..") "..dump({
state = state,
node = node,
}))
end
return false
end
if not spawn_def then return log_fail("missing spawn_def") end
local mob_def = minetest.registered_entities[spawn_def.name]
if mob_def.type == "monster" then
if not state.spawn_hostile then return log_fail("can't spawn hostile") end
else
if not state.spawn_passive then return log_fail("can't spawn passive") end
end
-- Make the dimention is correct
if spawn_def.dimension ~= state.dimension then return log_fail("incorrect dimension") end
if type(spawn_def.biomes) ~= "table" or not table.find(spawn_def.biomes, state.biome) then
return log_fail("Incorrect biome")
end
-- Ground mobs must spawn on solid nodes that are not leafes
if spawn_def.type_of_spawning == "ground" and not state.is_ground then
return log_fail("not ground node")
end
-- Water mobs must spawn in water
if spawn_def.type_of_spawning == "water" and not state.is_water then return log_fail("not water node") end
-- Farm animals must spawn on grass
if is_farm_animal(spawn_def.name) and not state.is_grass then return log_fail("not grass block") end
return true
end
local function spawn_check(pos, state, node, spawn_def)
local function log_fail(reason)
local count = (counts[reason] or 0) + 1
counts[reason] = count
mcl_log("Spawn check failed - "..reason.." ("..count..")")
return false
end
if not initial_spawn_check(state, node, spawn_def) then return false end
dbg_spawn_attempts = dbg_spawn_attempts + 1
local dimension = mcl_worlds.pos_to_dimension(pos)
-- Make sure the mob can spawn at this location
if pos.y < spawn_def.min_height or pos.y > spawn_def.max_height then return log_fail("incorrect height") end
-- Spawns require enough room for the mob
local mob_def = minetest.registered_entities[spawn_def.name]
local mob_type = mob_def.type
local gotten_node = get_node(pos).name
if not gotten_node then return end
if not has_room(mob_def,pos) then return log_fail("mob doesn't fit here") end
local biome_name = get_biome_name(pos)
if not biome_name then return end
-- Don't spawn if the spawn definition has a custom check and that fails
if spawn_def.check_position and not spawn_def.check_position(pos) then return log_fail("custom position check failed") end
local is_ground = minetest.get_item_group(gotten_node,"solid") ~= 0
if not is_ground then
pos.y = pos.y - 1
gotten_node = get_node(pos).name
is_ground = minetest.get_item_group(gotten_node,"solid") ~= 0
end
pos.y = pos.y + 1
local is_water = get_item_group(gotten_node, "water") ~= 0
local is_lava = get_item_group(gotten_node, "lava") ~= 0
local is_leaf = get_item_group(gotten_node, "leaves") ~= 0
local is_bedrock = gotten_node == "mcl_core:bedrock"
local is_grass = minetest.get_item_group(gotten_node,"grass_block") ~= 0
if pos.y >= spawn_def.min_height
and pos.y <= spawn_def.max_height
and spawn_def.dimension == dimension
and biome_check(spawn_def.biomes, biome_name) then
mcl_log("Spawn level 1 check - Passed")
if (is_ground or spawn_def.type_of_spawning ~= "ground")
and (spawn_def.type_of_spawning ~= "ground" or not is_leaf)
and (not is_farm_animal(spawn_def.name) or is_grass)
and (spawn_def.type_of_spawning ~= "water" or is_water)
and not is_bedrock
and has_room(mob_def,pos)
and (spawn_def.check_position and spawn_def.check_position(pos) or spawn_def.check_position == nil)
and ( not spawn_protected or not minetest.is_protected(pos, "") ) then
mcl_log("Spawn level 2 check - Passed")
local gotten_light = get_node_light(pos)
if modern_lighting then
local my_node = get_node(pos)
local sky_light = minetest.get_natural_light(pos)
local art_light = minetest.get_artificial_light(my_node.param1)
if mob_def.spawn_check then
return mob_def.spawn_check(pos, gotten_light, art_light, sky_light)
elseif mob_type == "monster" then
if dimension == "nether" then
if art_light <= nether_threshold then
return true
end
elseif dimension == "end" then
if art_light <= end_threshold then
return true
end
elseif dimension == "overworld" then
if art_light <= overworld_threshold and sky_light <= overworld_sky_threshold then
return true
end
end
else
-- passive threshold is apparently the same in all dimensions ...
if gotten_light > overworld_passive_threshold then
return true
end
end
else
if gotten_light >= spawn_def.min_light and gotten_light <= spawn_def.max_light then
return true
end
end
end
end
return false
return true
end
function mcl_mobs.spawn(pos,id)
@ -844,24 +660,133 @@ function mcl_mobs.spawn(pos,id)
return minetest.add_entity(pos, def.name)
end
local function build_state_for_position(pos, parent_state)
-- Get spawning parameters for this location
local biome_name = get_biome_name(pos)
if not biome_name then return end
local function spawn_group(p,mob,spawn_on,amount_to_spawn)
local nn= minetest.find_nodes_in_area_under_air(vector.offset(p,-5,-3,-5),vector.offset(p,5,3,5),spawn_on)
local o
table.shuffle(nn)
local dimension = mcl_worlds.pos_to_dimension(pos)
-- Get node and make sure it's loaded and a valid spawn point
local node = get_node(pos)
local node_name = node.name
-- Make sure we can spawn here
if not node or node_name == "ignore" or node_name == "mcl_core:bedrock" then return end
-- Check if it's ground
local is_water = get_item_group(node_name, "water") ~= 0
local is_ground = false
if not is_water then
is_ground = get_item_group(node_name,"solid") ~= 0
if not is_ground then
pos.y = pos.y - 1
node = get_node(pos)
node_name = node.name
is_ground = get_item_group(node_name,"solid") ~= 0
end
pos.y = pos.y + 1
end
-- Build spawn state data
local state = {
spawn_hostile = true,
spawn_passive = true,
}
if parent_state then state = table.copy(parent_state) end
state.biome = biome_name
state.dimension = dimension
state.is_ground = is_ground and get_item_group(node_name, "leaves") == 0
state.is_grass = get_item_group(node_name, "grass_block") ~= 0
state.is_water = is_water
-- Check light level
local gotten_light = get_node_light(pos)
-- Legacy lighting
if not modern_lighting then
if gotten_light < spawn_def.min_light or gotten_light > spawn_def.max_light then
state.light = gotten_light
end
else
-- Modern lighting
local light_node = get_node(pos)
local sky_light = minetest.get_natural_light(pos) or 0
local art_light = minetest.get_artificial_light(light_node.param1)
if dimension == "nether" then
if art_light > nether_threshold then
state.spawn_hostile = false
end
elseif dimension == "end" then
if art_light > end_threshold then
state.spawn_hostile = false
end
elseif dimension == "overworld" then
if art_light > overworld_threshold then
state.spawn_hostile = false
end
if sky_light > overworld_sky_threshold then
state.spawn_hostile = false
end
end
-- passive threshold is apparently the same in all dimensions ...
if gotten_light < overworld_passive_threshold then
state.spawn_passive = false
end
end
return state,node
end
local function spawn_group(p, mob, spawn_on, amount_to_spawn, parent_state)
-- Find possible spawn locations and shuffle the list
local nn = find_nodes_in_area_under_air(vector.offset(p,-5,-3,-5), vector.offset(p,5,3,5), spawn_on)
if not nn or #nn < 1 then
nn = {}
table.insert(nn,p)
end
table.shuffle(nn)
--minetest.log("Spawn point list: "..dump(nn))
for i = 1, amount_to_spawn do
local sp = vector.offset(nn[math.random(#nn)],0,1,0)
if spawn_check(nn[math.random(#nn)],mob) then
-- Use the first amount_to_spawn positions to spawn mobs. If a spawn position is protected,
-- it is removed from the list and not counted against the spawn amount. Only one mob will
-- spawn in a given spot.
local o
while amount_to_spawn > 0 and #nn > 0 do
-- Find the next valid group spawn point
local sp
while #nn > 0 and not sp do
-- Select the next spawn position
sp = vector.offset(nn[#nn],0,1,0)
nn[#nn] = nil
if spawn_protected and minetest.is_protected(sp, "") then
sp = nil
elseif not check_line_of_sight(p, sp) then
sp = nil
end
end
if not sp then return o end
-- Spawning prohibited in protected areas
local state, node = build_state_for_position(sp, parent_state)
if spawn_check(sp, state, node, mob) then
if mob.type_of_spawning == "water" then
sp = get_water_spawn(sp)
end
o = mcl_mobs.spawn(sp,mob.name)
if o then dbg_spawn_succ = dbg_spawn_succ + 1 end
--minetest.log("Using spawn point "..vector.to_string(sp))
o = mcl_mobs.spawn(sp,mob.name)
if o then
amount_to_spawn = amount_to_spawn - 1
dbg_spawn_succ = dbg_spawn_succ + 1
end
end
end
return o
@ -882,17 +807,13 @@ minetest.register_chatcommand("spawn_mob",{
table.insert(modifiers, ":"..capture)
end
local mod1 = string.find(param, ":")
local mobname = param
local mod1 = string.find(param, ":")
if mod1 then
mobname = string.sub(param, 1, mod1-1)
end
local mob = mcl_mobs.spawn(pos,mobname)
if mob then
for c=1, #modifiers do
modifs = modifiers[c]
@ -941,12 +862,7 @@ minetest.register_chatcommand("spawn_mob",{
})
if mobs_spawn then
-- Get pos to spawn, x and z are randomised, y is range
local function mob_cap_space (pos, mob_type, mob_counts_close, mob_counts_wide, cap_space_hostile, cap_space_non_hostile)
-- Some mob examples
--type = "monster", spawn_class = "hostile",
--type = "animal", spawn_class = "passive",
@ -1007,156 +923,178 @@ if mobs_spawn then
local spawning_position = get_next_mob_spawn_pos(pos)
if spawning_position then return spawning_position end
max_loops = max_loops - 1
end
return nil
end
local cumulative_chance = nil
local mob_library_worker_table = nil
local function initialize_spawn_data()
if not mob_library_worker_table then
mob_library_worker_table = table_copy(spawn_dictionary)
local function select_random_mob_def(spawn_table)
if #spawn_table == 0 then return nil end
local cumulative_chance = 0
for i = 1,#spawn_table do
cumulative_chance = cumulative_chance + spawn_table[i].chance
end
if not cumulative_chance then
cumulative_chance = 0
for k, v in pairs(mob_library_worker_table) do
cumulative_chance = cumulative_chance + v.chance
local mob_chance_offset = math_random(1, 1e6) / 1e6 * cumulative_chance
--minetest.log("action", "mob_chance_offset = "..tostring(mob_chance_offset).."/"..tostring(cumulative_chance))
for i = 1,#spawn_table do
local mob_def = spawn_table[i]
local mob_chance = mob_def.chance
if mob_chance_offset <= mob_chance then
--minetest.log(mob_def.name.." "..mob_chance)
return mob_def
end
mob_chance_offset = mob_chance_offset - mob_chance
end
assert(not "failed")
end
local function spawn_a_mob(pos, cap_space_hostile, cap_space_non_hostile)
local spawn_lists = {}
local function get_spawn_list(pos, cap_space_hostile, cap_space_non_hostile)
local spawn_hostile = false
local spawn_passive = false
-- Check capacity
local mob_counts_close, mob_counts_wide, total_mobs = count_mobs_all("spawn_class", pos)
local cap_space_hostile = mob_cap_space(pos, "hostile", mob_counts_close, mob_counts_wide, cap_space_hostile, cap_space_non_hostile )
if cap_space_hostile > 0 then
spawn_hostile = true
end
local cap_space_passive = mob_cap_space(pos, "passive", mob_counts_close, mob_counts_wide, cap_space_hostile, cap_space_non_hostile )
if cap_space_passive > 0 then
if math.random(100) < peaceful_percentage_spawned then
spawn_passive = true
end
end
-- Merge light level chcekss with cap checks
local state, node = build_state_for_position(pos)
if not state then return end
state.spawn_hostile = spawn_hostile and state.spawn_hostile
state.spawn_passive = spawn_passive and state.spawn_passive
-- Make sure it is possible to spawn a mob here
if not state.spawn_hostile and not state.spawn_passive then return end
-- Check the cache to see if we have already built a spawn list for this state
local state_hash = compute_hash(state) -- from mcl_enchanting
local spawn_list = spawn_lists[state_hash]
state.cap_space_hostile = cap_space_hostile
state.cap_space_passive = cap_space_passive
if spawn_list then
return spawn_list, state, node
end
-- Build a spawn list for this state
spawn_list = {}
local spawn_names = {}
for _,def in pairs(spawn_dictionary) do
if initial_spawn_check(state, node, def) then
table.insert(spawn_list, def)
table.insert(spawn_names, def.name)
end
end
if logging then
minetest.log(dump({
pos = pos,
node = node,
state = state,
state_hash = state_hash,
spawn_names = spawn_names,
}))
end
spawn_lists[state_hash] = spawn_list
return spawn_list, state, node
end
-- Spawns one mob or one group of mobs
local fail_count = 0
local function spawn_a_mob(pos, cap_space_hostile, cap_space_non_hostile)
local spawning_position = find_spawning_position(pos, FIND_SPAWN_POS_RETRIES)
if not spawning_position then
minetest.log("action", "[Mobs spawn] Cannot find a valid spawn position after retries: " .. FIND_SPAWN_POS_RETRIES)
fail_count = fail_count + 1
if logging and fail_count > 16 then
minetest.log("action", "[Mobs spawn] Cannot find a valid spawn position in last 16 attemtps")
end
return
end
fail_count = 0
-- Spawning prohibited in protected areas
if spawn_protected and minetest.is_protected(spawning_position, "") then return end
-- Select a mob
local spawn_list, state, node = get_spawn_list(spawning_position, cap_space_hostile, cap_space_non_hostile)
if not spawn_list then return end
local mob_def = select_random_mob_def(spawn_list)
if not mob_def or not mob_def.name then return end
local mob_def_ent = minetest.registered_entities[mob_def.name]
if not mob_def_ent then return end
local cap_space_available = state.cap_space_passive
if mob_def_ent.type == "monster" then
cap_space_available = state.cap_space_hostile
end
-- Make sure we would be spawning a mob
if not spawn_check(spawning_position, state, node, mob_def) then
mcl_log("Spawn check failed")
return
end
local mob_counts_close, mob_counts_wide, total_mobs = count_mobs_all("spawn_class", spawning_position)
--output_mob_stats(mob_counts_close, total_mobs)
--output_mob_stats(mob_counts_wide)
--grab mob that fits into the spawning location
--use random weighted choice with replacement to grab a mob, don't exclude any possibilities
--shuffle table once every loop to provide equal inclusion probability to all mobs
--repeat grabbing a mob to maintain existing spawn rates
local spawn_loop_counter = #mob_library_worker_table
while spawn_loop_counter > 0 do
table.shuffle(mob_library_worker_table)
local mob_chance_offset = math_random(1, cumulative_chance)
local mob_index = 1
local mob_chance = mob_library_worker_table[mob_index].chance
local step_chance = mob_chance
while step_chance < mob_chance_offset do
mob_index = mob_index + 1
if mob_index <= #mob_library_worker_table then
mob_chance = mob_library_worker_table[mob_index].chance
step_chance = step_chance + mob_chance
else
break
end
-- Water mob special case
if mob_def.type_of_spawning == "water" then
spawning_position = get_water_spawn(spawning_position)
if not spawning_position then
minetest.log("warning","[mcl_mobs] no water spawn for mob "..mob_def.name.." found at "..minetest.pos_to_string(vector.round(pos)))
return
end
--minetest.log(mob_def.name.." "..step_chance.. " "..mob_chance)
local mob_def = mob_library_worker_table[mob_index]
if mob_def and mob_def.name and minetest.registered_entities[mob_def.name] then
local mob_def_ent = minetest.registered_entities[mob_def.name]
local mob_spawn_class = mob_def_ent.spawn_class
local cap_space_available = mob_cap_space (spawning_position, mob_spawn_class, mob_counts_close, mob_counts_wide, cap_space_hostile, cap_space_non_hostile)
if cap_space_available > 0 then
--mcl_log("Cap space available")
-- Spawn caps for animals and water creatures fill up rapidly. Need to throttle this somewhat
-- for performance and for early game challenge. We don't want to reduce hostiles though.
local spawn_hostile = (mob_spawn_class == "hostile")
local spawn_passive = (mob_spawn_class ~= "hostile") and math.random(100) < peaceful_percentage_spawned
--mcl_log("Spawn_passive: " .. tostring(spawn_passive))
--mcl_log("Spawn_hostile: " .. tostring(spawn_hostile))
if (spawn_hostile or spawn_passive) and spawn_check(spawning_position,mob_def) then
if mob_def.type_of_spawning == "water" then
spawning_position = get_water_spawn(spawning_position)
if not spawning_position then
minetest.log("warning","[mcl_mobs] no water spawn for mob "..mob_def.name.." found at "..minetest.pos_to_string(vector.round(pos)))
return
end
end
if mob_def_ent.can_spawn and not mob_def_ent.can_spawn(spawning_position) then
minetest.log("warning","[mcl_mobs] mob "..mob_def.name.." refused to spawn at "..minetest.pos_to_string(vector.round(spawning_position)))
return
end
--everything is correct, spawn mob
local spawn_in_group = mob_def_ent.spawn_in_group or 4
local spawn_group_hostile = (mob_spawn_class == "hostile") and (math.random(100) < hostile_group_percentage_spawned)
local spawn_group_passive = (mob_spawn_class ~= "hostile") and (math.random(100) < peaceful_group_percentage_spawned)
mcl_log("spawn_group_hostile: " .. tostring(spawn_group_hostile))
mcl_log("spawn_group_passive: " .. tostring(spawn_group_passive))
local spawned
if spawn_in_group and (spawn_group_hostile or spawn_group_passive) then
local group_min = mob_def_ent.spawn_in_group_min or 1
if not group_min then group_min = 1 end
local amount_to_spawn = math.random(group_min, spawn_in_group)
mcl_log("Spawning quantity: " .. amount_to_spawn)
amount_to_spawn = math_min(amount_to_spawn, cap_space_available)
mcl_log("throttled spawning quantity: " .. amount_to_spawn)
if logging then
minetest.log("action", "[mcl_mobs] A group of " ..amount_to_spawn .. " " .. mob_def.name .. " mob spawns on " ..minetest.get_node(vector.offset(spawning_position,0,-1,0)).name .." at " .. minetest.pos_to_string(spawning_position, 1))
end
spawned = spawn_group(spawning_position,mob_def,{minetest.get_node(vector.offset(spawning_position,0,-1,0)).name}, amount_to_spawn)
else
if logging then
minetest.log("action", "[mcl_mobs] Mob " .. mob_def.name .. " spawns on " ..minetest.get_node(vector.offset(spawning_position,0,-1,0)).name .." at ".. minetest.pos_to_string(spawning_position, 1))
end
spawned = mcl_mobs.spawn(spawning_position, mob_def.name)
end
if spawned then
--mcl_log("We have spawned")
mob_counts_close, mob_counts_wide, total_mobs = count_mobs_all("spawn_class", pos)
local new_spawning_position = find_spawning_position(pos, FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN)
if new_spawning_position then
mcl_log("Setting new spawning position")
spawning_position = new_spawning_position
else
mcl_log("Cannot set new spawning position")
end
end
else
--mcl_log("Spawn check failed")
end
else
--mcl_log("Cap space full")
end
end
spawn_loop_counter = spawn_loop_counter - 1
end
if mob_def_ent.can_spawn and not mob_def_ent.can_spawn(spawning_position) then
minetest.log("warning","[mcl_mobs] mob "..mob_def.name.." refused to spawn at "..minetest.pos_to_string(vector.round(spawning_position)))
return
end
--everything is correct, spawn mob
local spawn_in_group = mob_def_ent.spawn_in_group or 4
local spawned
if spawn_in_group then
local group_min = mob_def_ent.spawn_in_group_min or 1
if not group_min then group_min = 1 end
local amount_to_spawn = math.random(group_min, spawn_in_group)
mcl_log("Spawning quantity: " .. amount_to_spawn)
amount_to_spawn = math.min(amount_to_spawn, cap_space_available)
mcl_log("throttled spawning quantity: " .. amount_to_spawn)
if amount_to_spawn > 1 then
if logging then
minetest.log("action", "[mcl_mobs] A group of " ..amount_to_spawn .. " " .. mob_def.name ..
"mob spawns on " ..minetest.get_node(vector.offset(spawning_position,0,-1,0)).name ..
" at " .. minetest.pos_to_string(spawning_position, 1)
)
end
return spawn_group(spawning_position,mob_def,{minetest.get_node(vector.offset(spawning_position,0,-1,0)).name}, amount_to_spawn, state)
end
end
if logging then
minetest.log("action", "[mcl_mobs] Mob " .. mob_def.name .. " spawns on " ..
minetest.get_node(vector.offset(spawning_position,0,-1,0)).name .." at "..
minetest.pos_to_string(spawning_position, 1)
)
end
return mcl_mobs.spawn(spawning_position, mob_def.name)
end
--MAIN LOOP
local timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer < WAIT_FOR_SPAWN_ATTEMPT then return end
initialize_spawn_data()
timer = 0
local function attempt_spawn()
local players = get_connected_players()
local total_mobs, total_non_hostile, total_hostile = count_mobs_total_cap()
@ -1178,6 +1116,35 @@ if mobs_spawn then
spawn_a_mob(pos, cap_space_hostile, cap_space_non_hostile)
end
end
end
local function fixed_timeslice(timer, dtime, timeslice_us, handler)
timer = timer - dtime
if timer > 0 then return timer, 0 end
-- Time the function
local start_time_us = minetest.get_us_time()
handler()
local stop_time_us = minetest.get_us_time()
-- Measure how long this took and calculate the time until the next call
local took = stop_time_us - start_time_us
timer = took / timeslice_us
return timer, took
end
--MAIN LOOP
local timer = 0
minetest.register_globalstep(function(dtime)
local next_spawn, took = fixed_timeslice(timer, dtime, 1000, attempt_spawn)
timer = next_spawn
if timer > MAX_SPAWN_CYCLE_TIME then timer = MAX_SPAWN_CYCLE_TIME end
if took > debug_time_threshold then
minetest.log("action","[mcl_mobs] took "..took.." us")
minetest.log("Next spawn attempt in "..tostring(timer))
end
end)
end
@ -1196,7 +1163,6 @@ function mob_class:despawn_allowed()
despawn_allowed(self)
end
assert(despawn_allowed({can_despawn=false}) == false, "despawn_allowed - can_despawn false failed")
assert(despawn_allowed({can_despawn=true}) == true, "despawn_allowed - can_despawn true failed")
@ -1247,3 +1213,10 @@ minetest.register_chatcommand("mobstats",{
output_mob_stats(mob_counts_wide, total_mobs, true)
end
})
minetest.register_on_mods_loaded(function()
for _,def in pairs(minetest.registered_biomes) do
table.insert(list_of_all_biomes, def.name)
end
end)