--[[ DYNAMIC ZONE - Anomalies Spawn Module Original Author(s) VodoXleb Singustromo Edited by License Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) (https://creativecommons.org/licenses/by-nc-sa/4.0) Synopsis Main module responsible for spawning anomalies on transitions that are blocked. Anomalies are spawned randomly in a given radius around the teleport space restrictor. Additionally we also spawn anomalies on predefined coordinates due to missing level vertex id's on some map spots. Anomalies are only being spawned for the current level as we handle over 20 of those per blocked transition in order to not cap the a-life limit with heavily modded games. demonized' kd_tree is used for rudimentary distance checks We've also utilized some modified anomaly spawn code from DAO (marked below) --]] parent = _G["dynamic_zone"] if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end -------------------------- -- Dependencies -- -------------------------- local utils = dynamic_zone_utils local debug = dynamic_zone_debug local route_manager = dynamic_zone_routes ------------------------------------------ -- Global Variables & Constants -- ------------------------------------------ CONST_LOGGING_PREFIX = "Anomalies" local log = debug.log_register("info", CONST_LOGGING_PREFIX) local log_warn = debug.log_register("warning", CONST_LOGGING_PREFIX) local log_error = debug.log_register("error", CONST_LOGGING_PREFIX) CONST_MDATA_KEY_USED_TRANSITION = "last_taken_transition" CONST_STRING_THEME_ID = "st_dynzone_$NAME_theme" CONST_ANOMALY_FALLBACK_SIZE_MIN = 2 -- fallback values CONST_ANOMALY_FALLBACK_SIZE_MAX = 4 CONST_FIELD_SIZE_MIN = 30 -- static spawn at transition center CONST_FIELD_SIZE_MAX = 40 CONST_SAFEZONE_RADIUS = 10 CONST_GENERATE_NARROW_FACTOR = 0.6 CONST_ABORT_CREATION_DELAY_MS = 250 CONST_BASE_INI_DIR = "plugins\\dynamic_zone\\anomalies" ini_transition_params = ini_file_ex(CONST_BASE_INI_DIR .. "\\transition_settings.ltx") ini_static_spawns = ini_file_ex(CONST_BASE_INI_DIR .. "\\static_spawns.ltx") -------------------------------------- -- Anomaly Spawn Settings -- -------------------------------------- -- Contains spawn parameters for transitions - also the defaults spawn_parameters = { defaults = { -- Needs to contain every possible key (failsafe) rootpos = false, narrow = false, count = 19, max_height_offset = 0, spread_radius = 20, generate_max_tries = 64, min_anomaly_proximity = 1, static_spawn_ratio = 0.4 }, } anomaly_themes = { [1] = { name = "thermal", anomaly_field_types = { "zone_field_thermal_strong", "zone_field_thermal_average", }, anomaly_mines_types = { "zone_mine_thermal_strong", "zone_mine_thermal_average", "zone_mine_thermal_weak", }, }, [2] = { name = "acidic", anomaly_field_types = { "zone_field_acidic_strong", "zone_field_acidic_average", }, anomaly_mines_types = { "zone_mine_acidic_strong", "zone_mine_acidic_average", "zone_mine_acidic_weak", "zone_mine_chemical_strong", "zone_mine_chemical_average", "zone_mine_chemical_weak", }, }, [3] = { name = "electric", anomaly_field_types = { "zone_field_psychic_strong", "zone_field_psychic_average", }, anomaly_mines_types = { "zone_mine_electric_strong", "zone_mine_electric_average", "zone_mine_electric_weak", }, }, [4] = { name = "gravitational", anomaly_field_types = { "zone_field_radioactive_strong", "zone_field_radioactive_average", }, anomaly_mines_types = { "zone_mine_gravitational_strong", "zone_mine_gravitational_average", "zone_mine_gravitational_weak", }, }, } -- TODO: Use table from DAO/Arrival, if available -- defining anomaly radii for most in-game anomalies anomaly_radii = { zone_radioactive = {min = 4, max = 6}, zone_radioactive_weak = {min = 4, max = 6}, zone_radioactive_average = {min = 4, max = 6}, zone_radioactive_strong = {min = 4, max = 6}, zone_mine_acid = {min = 2, max = 3}, zone_mine_acidic_weak = {min = 2, max = 3}, zone_mine_acidic_average = {min = 2, max = 3}, zone_mine_acidic_strong = {min = 2, max = 3}, zone_mine_blast = {min = 2, max = 3}, zone_mine_umbra = {min = 2, max = 3}, zone_mine_electra = {min = 2, max = 3}, zone_mine_electric_weak = {min = 2, max = 3}, zone_mine_electric_average = {min = 2, max = 3}, zone_mine_electric_strong = {min = 2, max = 3}, zone_mine_flash = {min = 3, max = 3}, zone_mine_ghost = {min = 2, max = 3}, zone_mine_gold = {min = 2, max = 3}, zone_mine_thorn = {min = 2, max = 3}, zone_mine_seed = {min = 3, max = 3}, zone_mine_shatterpoint = {min = 6, max = 8}, zone_mine_gravitational_weak = {min = 2, max = 3}, zone_mine_gravitational_average = {min = 3, max = 5}, zone_mine_gravitational_strong = {min = 4, max = 6}, zone_mine_sloth = {min = 3, max = 4}, zone_mine_mefistotel = {min = 3, max = 4}, zone_mine_net = {min = 2, max = 3}, zone_mine_point = {min = 2, max = 3}, zone_mine_cdf = {min = 2, max = 3}, zone_mine_sphere = {min = 4, max = 5}, zone_mine_springboard = {min = 4, max = 6}, zone_mine_thermal_weak = {min = 1, max = 2}, zone_mine_thermal_average = {min = 1, max = 2}, zone_mine_thermal_strong = {min = 1, max = 2}, zone_mine_zharka = {min = 1, max = 2}, zone_mine_vapour = {min = 1, max = 2}, zone_mine_vortex = {min = 3, max = 5}, } ------------------------------ -- Global Getters -- ------------------------------ -- Getter for the coresponding string identifier determined by anomaly_themes[id].name -- @param theme_id (index from anomaly_theme table) -- @returns string (identifier used in xml) function get_theme_string(theme_id) if not utils.valid_type{ caller = "get_theme_string", "int", theme_id } then return end utils.assert(anomaly_themes[theme_id], "Undefined theme with ID #%s!", theme_id) local theme_name = anomaly_themes[theme_id] and anomaly_themes[theme_id].name local string_id = CONST_STRING_THEME_ID return (theme_name) and string_id:gsub("%$NAME", theme_name) end -- Ensures that we always have usable spawn parameters -- Uses defaults section as a fallback -- @param transition_name -- @returns table function get_spawn_parameters(transition_name) if not utils.valid_type{ caller = "get_spawn_parameters", "str", transition_name } then return end local defaults = spawn_parameters.defaults if (utils.is_table_empty(spawn_parameters[transition_name])) then log("No parameters defined for transition '%s', using defaults.", transition_name or "N/A") return defaults end return spawn_parameters[transition_name] end -- Returns the root position for anomaly spawns of the given transition -- @param transition_name -- @returns pos (copy) or nil function get_rootposition(transition_name) if not utils.valid_type{ caller = "get_rootposition", "str", transition_name } then return end local params = get_spawn_parameters(transition_name) local pos = params and params.pos return pos and vector():set(pos.x, pos.y, pos.z) end ----------------------------- -- Configuration -- ----------------------------- function collect_spawn_parameters(section) local settings, target = ini_transition_params:collect_section(section) spawn_parameters[section] = {} target = spawn_parameters[section] for key, value in pairs(settings) do if (key == "rootpos") then target[key] = utils.string_to_vector(value) or false goto continue end local value_bool = utils.string_to_bool(value) target[key] = (type(value_bool) == "bool") and value_bool or tonumber(value) or value :: continue :: end end -- Returns the appropriate radius, to ensure we default to fallback values, if necessary -- @param anomaly_type section of the anomaly -- @returns table || nil function get_anomaly_radii(anomaly_type) if (not anomaly_type) then return end -- Fields share the same radii in our use case if string.find(anomaly_type, "^zone_field_") then return {min = CONST_FIELD_SIZE_MIN, max = CONST_FIELD_SIZE_MAX} end return anomaly_radii[anomaly_type] or {min = CONST_ANOMALY_FALLBACK_SIZE_MIN, max = CONST_ANOMALY_FALLBACK_SIZE_MAX} end ---------------------------- -- Monkey patches -- ---------------------------- SRTeleportMsgBoxOk = ui_sr_teleport.msg_box_ui.OnMsgOk -- We gotta save the last used transition for Anomaly safe Zone function ui_sr_teleport.msg_box_ui.OnMsgOk(self) local transition_name, m_data, data = self.name local route = route_manager.get_route_by_transition(transition_name) if (utils.assert_failed(route) or not route.blocked) then goto teleport end m_data = alife_storage_manager.get_state() data = m_data and m_data[parent.data_key] data[CONST_MDATA_KEY_USED_TRANSITION] = transition_name log("Saved '%s' as last used transition", data[CONST_MDATA_KEY_USED_TRANSITION]) :: teleport :: return SRTeleportMsgBoxOk(self) end ------------------------- -- Callbacks -- ------------------------- function on_game_start() RegisterScriptCallback("dynzone_changed_block_state", dynzone_changed_block_state) RegisterScriptCallback("on_level_changing", release_all_anomalies) ini_transition_params:section_for_each(collect_spawn_parameters) end -- Called in main script by actor_on_first_update because it -- should be executed after routes have been updated -- Spawns anomalies on the current level for blocked transitions -- Only does so, if spawned_anomalies of the transition is empty -- TODO: encapsulate from main script (e.g. new Callback) function spawn_on_current_level(force) for route_id, route in route_manager.iterate_routes() do if not (route.blocked or force) then goto next_route end spawn_for_route_on_current_level(route_id) :: next_route :: end end -- Sets the Anomaly Theme, Spawns and Despawns anomalies accordingly -- We check the spawned_anomalies attribute to -- determine if that transition is on current level function dynzone_changed_block_state(previously_blocked, new_blocked) for route_id, _ in pairs(previously_blocked) do route_manager.set_route_property(route_id, "anomaly_theme", 0) for section, attributes in route_manager.iterate_transitions(route_id) do if (not utils.is_table_empty(attributes.spawned_anomalies)) then release_all_from_transition(section) end end end for route_id, _ in pairs(new_blocked) do set_route_theme(route_id) -- We need to set it beforehand for e.g. the news spawn_for_route_on_current_level(route_id) end end -- Called 'on_level_changing' function release_all_anomalies() if (utils.debug_level_loaded()) then return end log("Releasing anomalies on '%s'", utils.get_mapname()) for route_id, route in route_manager.iterate_routes() do for section, attributes in route_manager.iterate_transitions(route_id) do if (not utils.is_table_empty(attributes.spawned_anomalies)) then release_all_from_transition(section) end end end end ------------------------------ -- Main functions -- ------------------------------ -- Sets a specific or random theme for any given route -- @param gizmo transition-section || route-id -- @param theme_id Optional theme id (randomized, if invalid) function set_route_theme(gizmo, theme_id) local theme = (theme_id and anomaly_themes[theme_id]) and theme_id or math.random(1, #anomaly_themes) local route_id = (route_manager.route_exists(gizmo)) and gizmo or route_manager.get_route_by_transition(gizmo).id if not utils.valid_type{ caller = "set_route_theme", "int", route_id, "int", theme } then return end route_manager.set_route_property(route_id, "anomaly_theme", theme) if (route_manager.get_route_property(route_id, "anomaly_theme") ~= theme) then log_warn("Unable to set route theme for route #%s", route_id) end -- Just a sanity check end function spawn_for_route_on_current_level(route_id) if not utils.valid_type{ caller = "spawn_for_route_on_current_level", "int", route_id } then return end local actor_level = utils.get_mapname() if (utils.is_underground(actor_level)) then log("Loaded underground level %s, will not execute.", actor_level) return end local route = route_manager.get_route(route_id) local m_data, safe_zone = alife_storage_manager.get_state() local data = m_data and m_data[parent.data_key] local last_used_transition = data[CONST_MDATA_KEY_USED_TRANSITION] if (utils.table_has(route.members, last_used_transition)) then data[CONST_MDATA_KEY_USED_TRANSITION] = nil safe_zone = true end for section, attributes in route_manager.iterate_transitions(route_id) do if (not utils.is_table_empty(attributes.spawned_anomalies)) then log_warn("[%s] Already spawned anomalies for '%s'", route_id, section) goto continue end local se_obj = section and get_story_se_object(section) if utils.assert_failed(se_obj, "No object with the ID #%s exists!", id) then goto continue end -- We need to check, if transition is on same level local transition_level = utils.get_mapname(se_obj) if (actor_level ~= transition_level) then goto continue end populate_transition(section, safe_zone) -- Just a logging check local spawned = route_manager.get_transition_property(section, "spawned_anomalies") log("Spawned %s anomalies for %s\n# %s", size_table(spawned), section, table.concat(spawned, ", ")) :: continue :: end end -- Spawns anomalies on a transition (static from LTX and dynamically) -- Also reads transition specific spawn parameters from the INI function populate_transition(section, safe_zone_around_player) if not utils.valid_type{ caller = "populate_transition", "str", section } then return end local route_id = route_manager.get_route_id(section) local theme_id = route_id and route_manager.get_route_property(route_id, "anomaly_theme") local anomaly_theme = anomaly_themes[theme_id] if utils.assert_failed(anomaly_theme and type(anomaly_theme) == "table") then return end local parameters = get_spawn_parameters(section) if utils.assert_failed(parameters) then return end local rpos = parameters.rootpos log("Spawning %s dynamic anomalies for '%s'%s (Theme: %s)", parameters.count +1, section, rpos and string.format(" @(%.1f, %.1f, %.1f)", rpos.x, rpos.y, rpos.z) or "", game.translate_string(get_theme_string(theme_id))) local transition_field_types = anomaly_theme.anomaly_field_types local guarenteed_spawn_type = transition_field_types[math.random(1, #transition_field_types)] spawn_at_transition(section, guarenteed_spawn_type, true, nil, parameters) local position_data = kd_tree.buildTreeVectors() if (utils.assert_failed(position_data)) then return end local types = anomaly_theme.anomaly_mines_types local count = parameters.count while (count > 0) do local anomaly_type = types[math.random(1, #types)] utils.assert(anomaly_type) spawn_at_transition(section, anomaly_type, false, position_data, parameters) count = count -1 end -- We now spawn static anomalies which fill the gaps on the map that have no level id's populate_transition_with_static_spawns(section, anomaly_theme, position_data, parameters) if (not safe_zone_around_player) then return end local actor_pos, transition = db.actor:position(), route_manager.get_transition(section) log("Establishing Safe-Zone around player for transition '%s' @(%.1f, %.1f, %.1f)", section, actor_pos.x, actor_pos.y, actor_pos.z) --[[ TODO: Only use one tree and directly attach Obj-ID's to position data Gotta build a new tree, otherwise we won't have their object-id as reference Returns table with elements like this: { { x = a, y = b, z = c, data = d}, distance } --]] local pos_tree = kd_tree.buildTreeSeObjectIds(transition.spawned_anomalies) local anomalies_near = pos_tree:nearestAll(actor_pos) if utils.assert_failed(not utils.is_table_empty(anomalies_near)) then log_error("Position Tree returned no object references") return end for _, anomaly_info in pairs(anomalies_near) do local obj_id, distance = anomaly_info[1].data, anomaly_info[2] if (distance > CONST_SAFEZONE_RADIUS) then break end -- Sorted by distance local se_obj = alife_object(obj_id) if (utils.assert_failed(se_obj, "Got no server object with ID #%s", obj_id)) then goto continue end local anomaly_name = se_obj:section_name() if (anomaly_name == guarenteed_spawn_type) then goto continue end log("Releasing previously spawned Anomaly '%s' with ID #%s (Distance: %.2f)", anomaly_name, obj_id, distance) local id_idx = utils.index_of(transition.spawned_anomalies, obj_id) if utils.assert_failed(id_idx) then log_error("[#%s] Unable to find Obj-ID #%s in {%s}", route_id, obj_id, unpack(transition.spawned_anomalies, ", ")) goto continue end table.remove(transition.spawned_anomalies, id_idx) alife_release_id(obj_id) :: continue :: end end -- Randomly spawns anomalies from a predefined pool in the ini -- @param transition_name section of target transition -- @param anomaly_theme pointer to the theme (anomaly_themes[theme_id]) function populate_transition_with_static_spawns(transition_name, anomaly_theme, position_data, parameters) if not utils.valid_type{ caller = "populate_transition_with_static_spawns", "str", transition_name, "tbl", anomaly_theme, "tbl", parameters } then return end if (not ini_static_spawns:section_exist(transition_name)) then log("Transition '%s' has no predefined static spawns, skipping.", transition_name) return end local min_anomaly_proximity = parameters.min_anomaly_proximity local spawn_percent = parameters.static_spawn_ratio local transition_spawns = ini_static_spawns:collect_section(transition_name) local types = anomaly_theme.anomaly_mines_types _g.shuffle_table(transition_spawns) local shuffled_spawns = {} for _, v in pairs(transition_spawns) do -- reindex for next step shuffled_spawns[#shuffled_spawns +1] = v end local remaining_spawns = _g.round(#shuffled_spawns * spawn_percent) log("Trying to populate transition %s with %s static spawns", transition_name, remaining_spawns) local spawned = route_manager.get_transition_property(transition_name, "spawned_anomalies") for index in utils.random_numbered_sequence(1, #shuffled_spawns) do if (remaining_spawns < 1) then break end local position_raw = shuffled_spawns[index] local data = position_raw and utils.string_to_posdata(position_raw) if (not data) then goto continue end local anomaly_type, pos = types[math.random(1, #types)], data.pos if (not posdata_distance_check(position_data, pos, anomaly_type, min_anomaly_proximity)) then goto continue end local anomaly_id = drx_da_spawn_anomaly(anomaly_type, pos, data.lvid, data.gvid) if (anomaly_id) then spawned[#spawned +1] = anomaly_id remaining_spawns = remaining_spawns -1 end :: continue :: end end -- @param transition_name section of target transition -- @param anomaly_type section of anomaly -- @param spawn_at_center boolean -- @param position_data kd_tree; used when spawn_at_center = false (optional) -- @param parameters needed when spawn_at_center = false function spawn_at_transition(transition_name, anomaly_type, spawn_at_center, position_data, parameters) -- TODO: Create corresponding function in utils local id = transition_name and get_story_object_id(transition_name) local transition_obj = id and alife_object(id) if (not transition_obj) then log_error("Can't create dynamic anomaly for %s, the specified transition does not exist.", transition_name) return end local pos = (parameters) and parameters.rootpos or transition_obj.position if (not spawn_at_center) then pos = generate_valid_position(transition_obj, anomaly_type, parameters, position_data) if (not pos) then log_error("Failed to generate position. Aborting.") return end end local lvid = level.vertex_id(pos) local anomaly_id = drx_da_spawn_anomaly(anomaly_type, pos, lvid, transition_obj.m_game_vertex_id) if (not anomaly_id) then return end local spawned = route_manager.get_transition_property(transition_name, "spawned_anomalies") spawned[#spawned +1] = anomaly_id return true end -- Releases all spawned anomalies tied to any given transition function release_all_from_transition(transition_name) if not utils.valid_type{ caller = "release_all_from_transition", "str", transition_name } then return end local transition = route_manager.get_transition(transition_name) local spawned_anomalies = transition and transition.spawned_anomalies if (utils.assert_failed(spawned_anomalies)) then return end local released = {} for index, id in pairs(spawned_anomalies) do if (alife_release_id(id)) then released[#released +1] = id else log_error("Unable to release anomaly with ID #%s", id) end end transition.spawned_anomalies = {} -- make sure no old references remain log("Released the following %s IDs for %s\n# %s", size_table(released), transition_name, table.concat(released, ", ")) end function posdata_distance_check(position_data, pos, anomaly_type, minimum_proximity) if not utils.valid_type{ caller = "posdata_distance_check", "tbl", position_data, "usr", pos, "str", anomaly_type, "number", minimum_proximity } then return end if (not position_data) then return true elseif (not position_data.root) then position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z} return true end local nearest = position_data:nearest(pos) if not (nearest and nearest[1] and nearest[1][2]) then log("Can't check position data (%.2f, %.2f, %.2f)", pos.x, pos.y, pos.z) position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z} return true end local radii = get_anomaly_radii(anomaly_type) local anomaly_radius = _g.round((radii.min + radii.max) / 2) -- mean local spawned_distance = nearest[1][2] - (anomaly_radius *2) if (spawned_distance >= minimum_proximity) then position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z} return true end log("Position invalid. Too close. (Distance %s < %s) @(%.2f, %.2f, %.2f)", spawned_distance, minimum_proximity, pos.x, pos.y, pos.z) end -- ANOMALY SPAWN FUNCTIONS -- THE FOLLOWING LINES ARE TAKEN FROM drx_da_main.script -- FROM DYNAMIC ANOMALIES OVERHAUL (DAO) (UPDATE 25) -- CREDITS TO TheMrDemonized, DoctorX, Eugenium, Barry Bogs, Lucy, Grok, Jurkonov, CrimsonVirus -- -- THESE FUNCTIONS HAVE BEEN ADAPTED FOR USE IN THIS ADDON BY THE MOD AUTHORS -- Spawns one anomaly of any valid given type -- @param anomaly_type (anomaly section) -- @param pos, lvid, gvid (self-explanatory) -- @returns se_obj_id function drx_da_spawn_anomaly(anomaly_type, pos, lvid, gvid) if not (anomaly_type and ini_sys:section_exist(anomaly_type)) then log_error("Anomaly type '%s' does not exist! Aborting.", anomaly_type) return end local se_obj = alife():create(anomaly_type, pos, lvid, gvid) if (not se_obj) then log_error("[%s] Unable to spawn anomaly.", anomaly_type) return end local function abort_creation(se_obj_id, anomaly_type) utils.timed_call(CONST_ABORT_CREATION_DELAY_MS, function() local obj = alife_object(se_obj_id) if obj then log_error("[%s] Anomaly '%s' failed to spawn correctly, releasing.", se_obj_id, anomaly_type) alife_release(obj) end return true end) end local data = utils_stpk.get_anom_zone_data(se_obj) -- get anomaly properties if (not data) then abort_creation(se_obj.id, anomaly_type) return end data.shapes[1] = {} data.shapes[1].shtype = 0 data.shapes[1].offset = vector():set(0, 0, 0) -- Leave for compatibility with CoC 1.4.22, delete later data.shapes[1].center = vector():set(0, 0, 0) local radii = get_anomaly_radii(anomaly_type) data.shapes[1].radius = math.random(radii.min, radii.max) utils_stpk.set_anom_zone_data(data, se_obj) return se_obj.id end -- Generates a random valid position from a vertex in radius around a given transition -- @param se_obj game object of the transition space restrictor -- @param anomaly_type section of the anomaly to be spawned -- @param spawn_params table containing the spawn parameters -- @param position_data (optional) function generate_valid_position(se_obj, anomaly_type, spawn_params, position_data) if not utils.valid_type{ caller = "generate_valid_position", "usr", se_obj, "str", anomaly_type, "tbl", spawn_params } then return end local random = math.random local level_vertex_id = level.vertex_id local level_vertex_position = level.vertex_position local rootpos = spawn_params.rootpos or se_obj.position local num_tries = spawn_params.generate_max_tries local spread_radius = spawn_params.spread_radius local minimum_proximity = spawn_params.min_anomaly_proximity local max_offset = {x = spread_radius, y = spread_radius, z = spread_radius} if (spawn_params.narrow) then for k, v in pairs(max_offset) do max_offset[k] = math.floor(max_offset[k] * CONST_GENERATE_NARROW_FACTOR) end end local pos = vector():set(0, 0, 0) -- In the following, we randomly offset the position to -- get a valid level vertex id we can spawn an anomaly on while (num_tries > 0) do local offset_x = max_offset.x * random() local pos_x = (random() <= 0.5) and (rootpos.x +offset_x) or (rootpos.x -offset_x) local offset_y = max_offset.y * random() local pos_y = (random() <= 0.5) and (rootpos.y +offset_y) or (rootpos.y -offset_y) local offset_z = max_offset.z * random() local pos_z = (random() <= 0.5) and (rootpos.z +offset_z) or (rootpos.z -offset_z) -- Set anomaly position at vertex and check if valid: pos = vector():set(pos_x, pos_y, pos_z) local check_distance -- interpreter would complain bc of goto statements pos = utils.get_closest_vertex_pos(pos) if (not pos) then goto next_try end check_distance = pos:distance_to(rootpos) if (check_distance > spread_radius) then log_error("Distance to root position is too high (%s)", check_distance) goto next_try end if (posdata_distance_check(position_data, pos, anomaly_type, minimum_proximity)) then break end :: next_try :: num_tries = (num_tries - 1) end if (num_tries <= 0) then log_error("Unable to generate valid lvid position, aborting.") return end return pos end