642 lines
22 KiB
Plaintext
642 lines
22 KiB
Plaintext
|
--[[
|
||
|
DYNAMIC ZONE
|
||
|
|
||
|
Original Author(s)
|
||
|
Singustromo <singustromo at disroot.org>
|
||
|
|
||
|
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)
|
||
|
|
||
|
This script contains general purpose utility functions and logic used
|
||
|
to decouple the vanilla anomaly logic from the addon logic.
|
||
|
|
||
|
We've taken some inspiration from NLTP_ASHES' Western Goods.
|
||
|
--]]
|
||
|
|
||
|
parent = _G["dynamic_zone"]
|
||
|
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
|
||
|
|
||
|
CONST_GAMETICK_DURATION_MS = 16
|
||
|
CONST_DEFAULT_PROXIMITY_DISTANCE = 2^16 -1
|
||
|
CONST_MAX_VALID_LVID = 2^32 -2
|
||
|
|
||
|
-------------------------
|
||
|
-- Dependencies
|
||
|
-------------------------
|
||
|
|
||
|
local opt = dynamic_zone_mcm
|
||
|
local debug = dynamic_zone_debug
|
||
|
|
||
|
CONST_LOGGING_PREFIX = "Utilities"
|
||
|
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
|
||
|
local log_error = debug.log_register("error", CONST_LOGGING_PREFIX)
|
||
|
|
||
|
----------------------------
|
||
|
-- Input Validation --
|
||
|
----------------------------
|
||
|
|
||
|
-- Inspired by Western Goods
|
||
|
-- Checks, if all necessary parameters are non-nil and have the correct type
|
||
|
-- Use it like this: valid_type{ caller = "function_name", type, value, ... }
|
||
|
-- The boolean type explicitely accepts only true|false as values
|
||
|
-- @param tbl
|
||
|
-- @returns boolean
|
||
|
function valid_type(tbl)
|
||
|
if (not opt.get("validate_parameter_types")) then return true end
|
||
|
|
||
|
local type_alias = {
|
||
|
int = "number", str = "string", tbl = "table",
|
||
|
fn = "function" , bool = "boolean", usr = "userdata",
|
||
|
}
|
||
|
|
||
|
local caller = tbl.caller and string.format("[%s] ", tbl.caller) or ""
|
||
|
local param_index, encountered_error = 1, false
|
||
|
for i=1, #tbl, 2 do
|
||
|
local typ, value, value_type = tbl[i], tbl[i +1]
|
||
|
typ = type_alias[typ] or typ
|
||
|
|
||
|
if not (typ == "boolean" or value) then
|
||
|
log_error("%sParameter no. %s is nil%s", caller, param_index, callstack(nil, true))
|
||
|
encountered_error = true
|
||
|
goto continue
|
||
|
end
|
||
|
|
||
|
-- also accounts for nil values (it's own type)
|
||
|
value_type = type(value)
|
||
|
if value_type ~= typ then
|
||
|
log_error("%sType mismatch for parameter no. %s (%s != %s)%s",
|
||
|
caller, param_index, value_type, typ, callstack(nil, true))
|
||
|
encountered_error = true
|
||
|
end
|
||
|
|
||
|
:: continue ::
|
||
|
param_index = param_index +1
|
||
|
end
|
||
|
|
||
|
return (not encountered_error)
|
||
|
end
|
||
|
|
||
|
-- Alternative to native assert that does not crash the game nor is intended to
|
||
|
-- alter the control flow directly. It merely logs the message and a
|
||
|
-- callstack via the default methods used by this addon
|
||
|
-- @param condition (boolean)
|
||
|
-- @param message (optional; format)
|
||
|
-- @param vararg (format elements)
|
||
|
-- @returns true when assertion failed
|
||
|
function assert(condition, message, ...)
|
||
|
condition = (type(condition) == nil) or condition -- nil-checking
|
||
|
if (not opt.get("validate_parameter_types")) then return (not condition) end
|
||
|
|
||
|
if (not condition) then
|
||
|
local args = {...}
|
||
|
message = (message and type(message) == "string") and message or "failed!"
|
||
|
|
||
|
log_error("%s%s", string.format("Assertion: " .. message,
|
||
|
unpack(args)), callstack(nil, true))
|
||
|
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Just an alias for usage in if-statements
|
||
|
assert_failed = assert
|
||
|
|
||
|
---------------------------
|
||
|
-- Type Conversion --
|
||
|
---------------------------
|
||
|
|
||
|
-- Only returns true, if string is "true" or of type bool
|
||
|
function string_to_bool(str) return (str == "true" or s == true) end
|
||
|
|
||
|
-- Converts a comma delimited string into a 3 dimensional vector
|
||
|
-- @returns vector on success
|
||
|
function string_to_vector(str)
|
||
|
if not valid_type{ caller = "string_to_vector", "str", str } then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local tbl = str_explode(str, ",") or {}
|
||
|
if assert_failed(#tbl == 3, "Invalid posdata (not 3 elements)") then return end
|
||
|
|
||
|
for index, value in pairs(tbl) do
|
||
|
tbl[index] = tonumber(value)
|
||
|
if assert_failed(tbl[index], "Element is not a number") then return end
|
||
|
end
|
||
|
|
||
|
return vector():set(tbl[1], tbl[2], tbl[3])
|
||
|
end
|
||
|
|
||
|
-- Converts a comma delimited string in the form of
|
||
|
-- `x, y, z, lvid, gvid` into positional data
|
||
|
-- @returns position data as table (pos, lvid, gvid)
|
||
|
function string_to_posdata(str)
|
||
|
if not valid_type{ caller = "string_to_posdata", "str", str } then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local tbl = str_explode(str, ",")
|
||
|
if assert_failed(#tbl == 5, "Invalid posdata (not 5 elements)") then return end
|
||
|
|
||
|
-- Convert all substrings to numbers
|
||
|
for index, value in pairs(tbl) do
|
||
|
tbl[index] = tonumber(value)
|
||
|
if assert_failed(tbl[index], "Element is not a number") then return end
|
||
|
end
|
||
|
|
||
|
local lvid, gvid = tbl[4], tbl[5]
|
||
|
if assert_failed(lvid < CONST_MAX_VALID_LVID, "Invalid level vertex id") then return end
|
||
|
|
||
|
return {
|
||
|
pos = vector():set(tbl[1], tbl[2], tbl[3]),
|
||
|
lvid = lvid,
|
||
|
gvid = gvid,
|
||
|
}
|
||
|
end
|
||
|
|
||
|
-- ARGB32 -> a byte for each channel
|
||
|
-- @param pixelvalue e.g. return value from GetARGB(a,r,g,b)
|
||
|
-- @returns table containing following keys: r, g, b, a
|
||
|
function argb_convert_from_pixelvalue(pixelvalue)
|
||
|
if not valid_type{ caller = "argb_convert_from_pixelvalue", "int", pixelvalue } then return end
|
||
|
|
||
|
local bitmask = 2^8 -1 -- 0xFF
|
||
|
local alpha = bit.band(bit.rshift(pixelvalue, 24), bitmask)
|
||
|
local red = bit.band(bit.rshift(pixelvalue, 16), bitmask)
|
||
|
local green = bit.band(bit.rshift(pixelvalue, 8), bitmask)
|
||
|
local blue = bit.band(pixelvalue, bitmask)
|
||
|
|
||
|
if assert_failed((red and green and blue and alpha), "Invalid pixelvalue (argb)!") then return end
|
||
|
return { r = red, g = green, b = blue, a = alpha }
|
||
|
end
|
||
|
|
||
|
-- Changes the weight of the alpha pixelvalue
|
||
|
-- @param alpha 8-bit integer
|
||
|
-- @returns pixelvalue (ARGB32)
|
||
|
function argb_change_alpha(pixelvalue, alpha)
|
||
|
if not valid_type{ caller = "argb_change_alpha",
|
||
|
"int", pixelvalue, "int", alpha } then return end
|
||
|
|
||
|
local alphavalue = bit.lshift(bit.band(alpha, 2^8 -1), 24) -- make sure it's 1 byte
|
||
|
local no_alpha = bit.band(pixelvalue, 2^24 -1)
|
||
|
|
||
|
if assert_failed((no_alpha and alphavalue), "Invalid pixelvalue (argb)!") then return end
|
||
|
return bit.bor(no_alpha, alphavalue)
|
||
|
end
|
||
|
|
||
|
---------------------------
|
||
|
-- Table Functions --
|
||
|
---------------------------
|
||
|
|
||
|
-- output of next() is nil when table is empty
|
||
|
function is_table_empty(tbl)
|
||
|
return not (tbl and next(tbl))
|
||
|
end
|
||
|
|
||
|
function index_of(tbl, value)
|
||
|
if not valid_type{ caller = "index_of", "tbl", tbl } then return end
|
||
|
|
||
|
for k, v in pairs(tbl) do
|
||
|
if (v == value) then return k end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Checks, if table includes the value; Also checks subtables
|
||
|
-- @returns true if value is in table
|
||
|
function table_has(tbl, value)
|
||
|
if not valid_type{ caller = "table_has", "tbl", tbl } then return end
|
||
|
|
||
|
for _, v in pairs(tbl) do
|
||
|
if v == value then return true end
|
||
|
|
||
|
if type(v) == 'table' and table_has(v, value) then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Checks, if all elements of tbl_in are contained in tbl
|
||
|
-- @returns boolean
|
||
|
function table_contains(tbl, tbl_in)
|
||
|
for _, v in pairs(tbl_in) do
|
||
|
if (not table_has(tbl, v)) then return end
|
||
|
end
|
||
|
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
-- Creates subtable with key, if needed, needs the type of the value
|
||
|
-- @returns success state of insertion
|
||
|
function table_safe_insert(tbl, key, value, value_type)
|
||
|
if not valid_type{ caller = "table_safe_insert",
|
||
|
"tbl", tbl, "str", key, value_type, value } then return end
|
||
|
|
||
|
if not tbl[key] then
|
||
|
tbl[key] = { value }
|
||
|
else
|
||
|
table.insert(tbl[key], value)
|
||
|
end
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
function table_copy(tbl)
|
||
|
if not valid_type{ caller = "table_copy", "tbl", tbl } then return end
|
||
|
|
||
|
local copy = {}
|
||
|
for k,v in pairs(tbl) do
|
||
|
copy[k] = (type(v) == "table") and table_copy(v) or v
|
||
|
end
|
||
|
|
||
|
return copy
|
||
|
end
|
||
|
|
||
|
-- recursively swaps values to keys into a one-dimensional dictionary
|
||
|
function values_to_keys(tbl, result)
|
||
|
if not valid_type{ caller = "values_to_keys",
|
||
|
"tbl", tbl, "tbl", result } then return end
|
||
|
|
||
|
for k, v in pairs(tbl) do
|
||
|
if type(v) == 'table' then
|
||
|
values_to_keys(v, result)
|
||
|
else
|
||
|
result[v] = true
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- lists all keys in tbl into an indexed table
|
||
|
function index_keys(tbl, result)
|
||
|
if not valid_type{ caller = "index_keys",
|
||
|
"tbl", tbl, "tbl", result } then return end
|
||
|
|
||
|
for key, v in pairs(tbl) do
|
||
|
result[#result +1] = key
|
||
|
end
|
||
|
end
|
||
|
|
||
|
------------------------
|
||
|
-- Closures --
|
||
|
------------------------
|
||
|
|
||
|
function random_numbered_sequence(from, to)
|
||
|
if not valid_type{ caller = "random_numbered_sequence",
|
||
|
"int", from, "int", to } then return end
|
||
|
|
||
|
local tbl = {}
|
||
|
for i = from, to do
|
||
|
tbl[#tbl + 1] = i
|
||
|
end
|
||
|
|
||
|
for i = #tbl, 2, -1 do
|
||
|
local j = math.random(i)
|
||
|
tbl[i], tbl[j] = tbl[j], tbl[i]
|
||
|
end
|
||
|
|
||
|
local index = 0
|
||
|
return function()
|
||
|
if index > #tbl then return end
|
||
|
index = index + 1
|
||
|
return tbl[index]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
------------------------------------
|
||
|
-- Timed Function Execution --
|
||
|
------------------------------------
|
||
|
|
||
|
-- Taken from Western Goods
|
||
|
-- executes functor once on next tick
|
||
|
-- @author: demonized
|
||
|
function next_tick(functor, ...)
|
||
|
if not valid_type{ caller = "next_tick", "fn", functor } then return end
|
||
|
|
||
|
local args = {...}
|
||
|
AddUniqueCall(function()
|
||
|
functor(unpack(args))
|
||
|
return true
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
-- Wrapper to throttle function execution with time delay
|
||
|
-- Modified derivative from modded exes
|
||
|
-- @delay number (milliseconds)
|
||
|
-- @delay_first_call boolean
|
||
|
-- @func functor
|
||
|
-- @vararg functor-parameters
|
||
|
-- @returns functor
|
||
|
function throttle(delay, delay_first_call, functor, ...)
|
||
|
if not valid_type{ caller = "throttle",
|
||
|
"int", delay, "bool", delay_first_call, "fn", functor } then return end
|
||
|
|
||
|
local args = {...}
|
||
|
if not (delay and delay > (CONST_GAMETICK_DURATION_MS or 16)) then
|
||
|
return function()
|
||
|
return functor(unpack(args))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local TimeGlobal = time_global
|
||
|
local tg_threshold = (delay_first_call)
|
||
|
and TimeGlobal() + delay or 0
|
||
|
|
||
|
return function()
|
||
|
local tg = TimeGlobal()
|
||
|
if (tg_threshold +1) > tg then return end
|
||
|
tg_threshold = tg + delay
|
||
|
|
||
|
return functor(unpack(args))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Repeatedly calls functor
|
||
|
-- Time should be declared in milliseconds
|
||
|
function timed_call(delay, functor, ...)
|
||
|
if not valid_type{ caller = "timed_call", "int", delay, "fn", functor } then return end
|
||
|
local args = {...}
|
||
|
|
||
|
-- We also delay first execution
|
||
|
local throttled_func = throttle(delay, true, functor, unpack(args))
|
||
|
if (not throttled_func) then return end
|
||
|
|
||
|
-- Calls functor every game tick (approx. 16-17 ms)
|
||
|
AddUniqueCall(throttled_func)
|
||
|
|
||
|
if (opt.get("validate_parameter_types")) then
|
||
|
log("timed_call | Added Unique Call for %s with delay of %sms%s",
|
||
|
functor, delay, callstack(nil, true))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
---------------------------
|
||
|
-- Map Markers --
|
||
|
---------------------------
|
||
|
|
||
|
function map_spot_exists(id, spot)
|
||
|
if not valid_type{ caller = "map_spot_exists",
|
||
|
"int", id, "str", spot } then return end
|
||
|
|
||
|
return (level.map_has_object_spot(id,spot) == 1)
|
||
|
end
|
||
|
|
||
|
function map_spot_remove(id, spot)
|
||
|
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
level.map_remove_object_spot(id, spot)
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
function map_spot_add(id, spot, hint)
|
||
|
if not valid_type{ caller = "map_spot_add", "str", hint } then return end
|
||
|
|
||
|
if assert_failed(not map_spot_exists(id, spot), "Spot (%s, %s) already exists!", id, spot) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
level.map_add_object_spot_ser(id, spot, hint)
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
function map_spot_change_hint(id, spot, hint)
|
||
|
if not valid_type{ caller = "map_spot_change_hint",
|
||
|
"int", id, "str", spot, "str", hint } then return end
|
||
|
|
||
|
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
level.map_change_spot_hint(id, spot, hint)
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
-- Read defined attributes from map_spots.xml for a given map spot
|
||
|
-- Uses a cache to speed up subsequent calls
|
||
|
-- @param spot (spot name; e.g. level_changer_right)
|
||
|
-- @returns table || nil (table contains texture and color (pixelvalue), if read)
|
||
|
local _cache_map_spot_texture_info = {}
|
||
|
function map_spot_get_texture_info(spot, color_as_argb, default_alpha)
|
||
|
if not valid_type{ caller = "get_mapspot_texture", "str", spot } then return end
|
||
|
|
||
|
local cached_result = _cache_map_spot_texture_info[spot]
|
||
|
if (cached_result) then return (cached_result) end
|
||
|
|
||
|
default_alpha = default_alpha or 255
|
||
|
spot = spot .. "_spot" -- that node got the actual info we need
|
||
|
local attr_tex = "texture"
|
||
|
|
||
|
local xml = _cache_map_spot_texture_info.parser
|
||
|
if (not xml) then
|
||
|
xml = CScriptXmlInit()
|
||
|
xml:ParseFile("map_spots.xml")
|
||
|
_cache_map_spot_texture_info.parser = xml
|
||
|
end
|
||
|
|
||
|
xml:NavigateToRoot() -- to <map_spots>
|
||
|
if assert_failed(xml:NodeExist(spot, 0), "Node %s does not exist", spot) then return end
|
||
|
xml:NavigateToNode(spot, 0)
|
||
|
|
||
|
local result = {}
|
||
|
result.texture = xml:ReadValue(attr_tex, 0)
|
||
|
if assert_failed(xml:NodeExist(attr_tex, 0), "Node %s/%s does not exist", spot, attr_tex) then return end
|
||
|
|
||
|
local color = {}
|
||
|
color.r = tonumber(xml:ReadAttribute(attr_tex, 0, "r"))
|
||
|
color.g = tonumber(xml:ReadAttribute(attr_tex, 0, "g"))
|
||
|
color.b = tonumber(xml:ReadAttribute(attr_tex, 0, "b"))
|
||
|
|
||
|
if (not is_table_empty(color)) then
|
||
|
color.a = tonumber(xml:ReadAttribute(attr_tex, 0, "a")) or default_alpha
|
||
|
|
||
|
result.color = color
|
||
|
if (not color_as_argb) then
|
||
|
result.color = GetARGB(color.a, color.r, color.g, color.b)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if (not is_table_empty(result)) then
|
||
|
_cache_map_spot_texture_info[spot] = result
|
||
|
return result
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- REQUIRES MODDED-EXES
|
||
|
-- Changes the map spot texture of any given object (given that it already has a map spot)
|
||
|
-- @param id object-id
|
||
|
-- @param spot current texture
|
||
|
-- @param texture new texture id
|
||
|
-- @returns true on success
|
||
|
function map_spot_change_texture(id, spot, texture)
|
||
|
if not valid_type{ caller = "map_spot_change_spot", "str", texture } then return end
|
||
|
|
||
|
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local spot_static = level.map_get_object_minimap_spot_static(id, spot)
|
||
|
local mini_static = level.map_get_object_spot_static(id, spot)
|
||
|
if assert_failed(spot_static and mini_static) then return end
|
||
|
|
||
|
spot_static:InitTexture(texture)
|
||
|
mini_static:InitTexture(texture)
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
-- REQUIRES MODDED-EXES
|
||
|
-- @param color argb32 pixelvalue (e.g. GetARGB(a,r,g,b))
|
||
|
-- @returns true on success
|
||
|
function map_spot_change_color(id, spot, color)
|
||
|
if not valid_type{ caller = "map_spot_change_color", "int", color } then return end
|
||
|
|
||
|
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local main_mapspot = level.map_get_object_spot_static(id, spot)
|
||
|
local mini_mapspot = level.map_get_object_minimap_spot_static(id, spot)
|
||
|
main_mapspot:SetTextureColor(color)
|
||
|
mini_mapspot:SetTextureColor(color)
|
||
|
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
-- REQUIRES MODDED-EXES
|
||
|
-- @returns pixelvalue (integer)
|
||
|
function map_spot_get_color(id, spot, get_mini)
|
||
|
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local main_mapspot = level.map_get_object_spot_static(id, spot)
|
||
|
local mini_mapspot = (get_mini) and level.map_get_object_minimap_spot_static(id, spot)
|
||
|
|
||
|
return (mini_mapspot and mini_mapspot:GetTextureColor())
|
||
|
or (main_mapspot and main_mapspot:GetTextureColor())
|
||
|
end
|
||
|
|
||
|
-----------------------------------
|
||
|
-- Game related Checks --
|
||
|
-----------------------------------
|
||
|
|
||
|
function has_translation(string)
|
||
|
if not valid_type{ caller = "has_translation", "str", string } then return end
|
||
|
return (game.translate_string(string) ~= string)
|
||
|
end
|
||
|
|
||
|
------------------------------
|
||
|
-- Object related --
|
||
|
------------------------------
|
||
|
|
||
|
-- @param obj (optional)
|
||
|
-- @returns level name
|
||
|
function get_mapname(obj)
|
||
|
if (not obj) then
|
||
|
return level.name()
|
||
|
elseif not valid_type{ caller = "get_mapname", "userdata", obj } then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local gvid
|
||
|
if (obj.online and type(obj.id) == "function") then
|
||
|
gvid = obj:game_vertex_id()
|
||
|
elseif (obj.id) then
|
||
|
gvid = obj.m_game_vertex_id
|
||
|
end
|
||
|
|
||
|
return (alife():level_name(game_graph():vertex(gvid):level_id()))
|
||
|
end
|
||
|
|
||
|
-- Only returns correct position for online objects as
|
||
|
-- we only check for the proximity to the player
|
||
|
-- @returns distance (to player)
|
||
|
function get_proximity_by_id(id, default)
|
||
|
if not valid_type{ caller = "get_proximity_by_id", "int", id } then return end
|
||
|
|
||
|
default = (default and type(default) == "number") or CONST_DEFAULT_PROXIMITY_DISTANCE
|
||
|
|
||
|
local se_obj = id and alife_object(id)
|
||
|
if not (se_obj and se_obj.online) then return default end
|
||
|
|
||
|
local pos = se_obj.position
|
||
|
return (pos and pos:distance_to(db.actor:position()) or default)
|
||
|
end
|
||
|
|
||
|
function get_point_proximity_by_id(id, point, default)
|
||
|
if not valid_type{ caller = "get_point_proximity_by_id", "int", id, "usr", point } then return end
|
||
|
default = default or CONST_DEFAULT_PROXIMITY_DISTANCE
|
||
|
|
||
|
local se_obj = id and alife_object(id)
|
||
|
if not (se_obj and se_obj.online) then return default end
|
||
|
|
||
|
local pos = se_obj.position
|
||
|
return (pos and pos:distance_to(point) or default)
|
||
|
end
|
||
|
|
||
|
-----------------------------
|
||
|
-- Level related --
|
||
|
-----------------------------
|
||
|
|
||
|
-- This works with and without modded exes
|
||
|
-- @returns pos from the target in the center of the viewport
|
||
|
function get_target_pos()
|
||
|
return (level.get_target_pos) and level.get_target_pos() -- REQUIRES MODDED-EXES
|
||
|
or device().cam_pos:add(device().cam_dir:mul(level.get_target_dist()))
|
||
|
end
|
||
|
|
||
|
-- Returns the position of a level vertex in close proximity (determined in-engine)
|
||
|
-- @param pos vector
|
||
|
-- @returns vector (copy)
|
||
|
function get_closest_vertex_pos(pos)
|
||
|
if not valid_type{ caller = "get_closest_vertex_pos", "usr", pos } then return end
|
||
|
local lvid = pos and pos.x and level.vertex_id(pos)
|
||
|
|
||
|
-- signed & last are reserved
|
||
|
if not (lvid and (lvid < CONST_MAX_VALID_LVID)) then return end
|
||
|
local pos = level.vertex_position(lvid)
|
||
|
|
||
|
return vector():set(pos.x, pos.y, pos.z)
|
||
|
end
|
||
|
|
||
|
-- Taken from Catspaw's utilities
|
||
|
function levelname_from_gvid(gvid)
|
||
|
if not valid_type{ caller = "levelname_from_gvid", "int", gvid } then return end
|
||
|
|
||
|
local gv = game_graph():vertex(gvid)
|
||
|
return alife():level_name(gv:level_id())
|
||
|
end
|
||
|
|
||
|
-- Requires that the weather manager is running (just before actor_on_first_update)
|
||
|
-- Additional levels need to be registered there to have working weather
|
||
|
-- @param level_name
|
||
|
-- @returns boolean
|
||
|
function is_underground(level)
|
||
|
if not valid_type{ caller = "is_underground", "str", level } then return end
|
||
|
|
||
|
return (not level_weathers.valid_levels[level])
|
||
|
end
|
||
|
|
||
|
function debug_level_loaded()
|
||
|
return (get_mapname() == "fake_start")
|
||
|
end
|
||
|
|
||
|
-- Uses txr_routes to determine connected maps
|
||
|
-- @param map
|
||
|
-- @returns table
|
||
|
function get_directly_connected_maps(map)
|
||
|
local get_txr_section = txr_routes.get_section
|
||
|
local get_txr_mapname = txr_routes.get_map
|
||
|
local routes = txr_routes.routes
|
||
|
|
||
|
local txr_section = map and get_txr_mapname(map)
|
||
|
if not (txr_section and routes[txr_section]) then return end
|
||
|
|
||
|
local connected = {}
|
||
|
for map, _ in pairs(routes[txr_section]) do
|
||
|
connected[#connected +1] = get_txr_section(map)
|
||
|
end
|
||
|
|
||
|
return connected
|
||
|
end
|