--[[ ----------------------------------------------------------------------------------------------
 File       : lua_helper.lua
 Description: различные вспомогательные функции и обертки
 Copyright  : 2017 © Prosectors Project
 Author     : Karlan
--]] ----------------------------------------------------------------------------------------------

--//-----------------------------------------------------------------
module '_G'

--//-----------------------------------------------------------------------------------------------
--// Variables
--//-----------------------------------------------------------------------------------------------
sini = system_ini()
gini = game_ini()

GAME_SCRIPTS = getFS():update_path("$game_scripts$", "")
GAME_LUA     = getFS():update_path("$game_lua$", "")

GAME_RELATIONS_SECT     = "game_relations"

INVALID_ID              = 0xffff
MAXIMUM_ID              = INVALID_ID-1
INVALID_STORY_ID        = raw_u32()
INVALID_SPAWN_STORY_ID  = raw_u32()

INVALID_LEVEL_VERTEX_ID = raw_u32()
INVALID_GAME_VERTEX_ID  = raw_u16()
INVALID_LEVEL_ID        = raw_u8()
INVALID_LOCATION_ID     = raw_u8()

INVALID_NAME            = 'INVALID_NAME'
INVALID_COST            = -1

BI_NONE                 = raw_u16()

KNIFE_SLOT          = 0
PISTOL_SLOT         = 1
RIFLE_SLOT          = 2
GRENADE_SLOT        = 3
APPARATUS_SLOT      = 4
BOLT_SLOT           = 5
OUTFIT_SLOT         = 6
PDA_SLOT            = 7
DETECTOR_SLOT       = 8
TORCH_SLOT          = 9
ARTEFACT_SLOT       = 10
HELMET_SLOT         = 11
SLOT_QUICK_ACCESS_0 = 12
SLOT_QUICK_ACCESS_1 = 13
SLOT_QUICK_ACCESS_2 = 14
SLOT_QUICK_ACCESS_3 = 15
BACKPACK_SLOT       = 16
DEVICE_SLOT         = 17
USE_SLOT            = 18
SLOTS_TOTAL         = 19
NO_SLOT             = 0xffffffff
NO_ACTIVE_SLOT      = raw_u32()
--//-------------------------
FRIENDS     = sini:r_s32(GAME_RELATIONS_SECT, "goodwill_friend")
NEUTRALS    = sini:r_s32(GAME_RELATIONS_SECT, "goodwill_neutal")
ENEMIES     = sini:r_s32(GAME_RELATIONS_SECT, "attitude_neutal_threshold")	--goodwill_enemy
--//-------------------------
HIT_TYPE_BY_ID = {
    [hit.burn]          = 'burn',
    [hit.shock]         = 'shock',
    [hit.strike]        = 'strike',
    [hit.wound]         = 'wound',
    [hit.radiation]     = 'radiation',
    [hit.telepatic]     = 'telepatic',
    [hit.chemical_burn] = 'chemical_burn',
    [hit.explosion]     = 'explosion',
    [hit.fire_wound]    = 'fire_wound',
    [hit.wound_2]       = 'wound_2',
    [hit.dummy]         = 'dummy',
}
--//-------------------------
se_object_flags = {
    ['flUseSwitches'] = global_flags.flUseSwitches,
    ['flSwitchOnline'] = global_flags.flSwitchOnline,
    ['flSwitchOffline'] = global_flags.flSwitchOffline,
    ['flInteractive'] = global_flags.flInteractive,
    ['flVisibleForAI'] = global_flags.flVisibleForAI,
    ['flUsefulForAI'] = global_flags.flUsefulForAI,
    ['flOfflineNoMove'] = global_flags.flOfflineNoMove,
    ['flUsedAI_Locations'] = global_flags.flUsedAI_Locations,
    ['flGroupBehaviour'] = global_flags.flGroupBehaviour,
    ['flCanSave'] = global_flags.flCanSave,
    ['flVisibleForMap'] = global_flags.flVisibleForMap,
    ['flUseSmartTerrains'] = global_flags.flUseSmartTerrains,
    ['flCheckForSeparator'] = global_flags.flCheckForSeparator,
    ['flForceSwitchOffline'] = global_flags.flForceSwitchOffline,
}
--//-------------------------
game_vertex_table = { --// Karlan: не самое лучшее место для этой таблицы, куда-бы ее деть?
    ['l04_darkvalley'] = {813, 1110},
    ['l11u_pripyat'] = {3643, 3692},
    ['l17u_catacomb'] = {4267, 4305},
    ['l14_marsh'] = {3844, 4084},
    ['l08_yantar'] = {1441, 1532},
    ['l18_foggy_thicket'] = {4306, 4362},
    ['l12u_sarcofag'] = {2412, 2476},
    ['l07_military'] = {1549, 1868},
    ['l11_pripyat'] = {2126, 2282},
    ['l03_agroprom'] = {418, 704},
    ['l17_hospital'] = {4220, 4266},
    ['l16_limansk'] = {4178, 4219},
    ['l10_radar'] = {1869, 2125},
    ['l15_red_forest'] = {4085, 4177},
    ['l18u_logovo'] = {4363, 4445},
    ['l08u_brainlab'] = {1533, 1548},
    ['l10u_bunker'] = {2671, 2801},
    ['l02_garbage'] = {253, 417},
    ['l05_bar'] = {1170, 1309},
    ['l10u_labx14'] = {3616, 3642},
    ['l03u_agr_underground'] = {705, 812},
    ['l09_deadcity'] = {3104, 3615},
    ['l04u_labx18'] = {1111, 1169},
    ['l06_rostok'] = {1310, 1440},
    ['l13_generators'] = {3693, 3772},
    ['l12u_control_monolith'] = {2477, 2526},
    ['l13u_warlab'] = {3773, 3843},
    ['k01_darkscape'] = {2802, 3103},
    ['l12_stancia_2'] = {2527, 2670},
    ['l01_escape'] = {0, 252},
    ['l12_stancia'] = {2283, 2411},
}
injurious_levels = {
    -- ['l11u_pripyat'] = true,
    -- ['l17u_catacomb'] = true,
    -- ['l12u_sarcofag'] = true,
    -- ['l18u_logovo'] = true,
    ['l08u_brainlab'] = {x=nil,y=25,z=nil},
    ['l10u_bunker'] = {x=nil,y=-15,z=nil},
    ['l10u_labx14'] = true,
    -- ['l03u_agr_underground'] = true,
    ['l04u_labx18'] = {x=nil,y=6.75,z=nil},
    -- ['l13u_warlab'] = true,
}
indoor_levels = {
    ['l11u_pripyat'] = true,
    ['l17u_catacomb'] = true,
    ['l12u_sarcofag'] = true,
    ['l12u_control_monolith'] = true,
    ['l18u_logovo'] = true,
    ['l08u_brainlab'] = true,
    ['l10u_bunker'] = true,
    ['l10u_labx14'] = true,
    ['l03u_agr_underground'] = true,
    ['l04u_labx18'] = true,
    ['l13u_warlab'] = true,
}
--//-------------------------
--// Karlan->Team: первые 10 тысяч используются в движке и игре примерно как статичные, вторые десять тысяч я зарезервировал под генерируемые движковые эффекторы, следовательно начинаем с третьего десятка
--// Karlan: добавлять только loop эффекты, либо эффекты которые сами по себе длительные по времени и их возможно придется отключать вручную, остальные отслеживать не нужно!
--// Karlan: для ассоциативности, и чтобы не создавать два массива предлагается окончанием задавать тип эффекта (_pp, _cam)
prosectors_effects_ids = {}
prosectors_effects_ids.start_effector_id    = 20001
local eff_id = prosectors_effects_ids.start_effector_id
prosectors_effects_ids.weather_cfg_pp       = eff_id + 1
prosectors_effects_ids.dream_black_pp       = eff_id + 2
prosectors_effects_ids.detector_scheme_pp   = eff_id + 3
prosectors_effects_ids.antigas_wear         = eff_id + 4
prosectors_effects_ids.suit_wear            = eff_id + 5
prosectors_effects_ids.surge_third_cam      = eff_id + 6
prosectors_effects_ids.surge_fourth_cam     = eff_id + 7
prosectors_effects_ids.surge_fourth_pp1     = eff_id + 8
prosectors_effects_ids.surge_fourth_pp2     = eff_id + 9
prosectors_effects_ids.antigas_toxic        = eff_id + 10
prosectors_effects_ids.bleeding             = eff_id + 11
prosectors_effects_ids.storyline_blowout    = eff_id + 12
prosectors_effects_ids.x18_gluk             = eff_id + 13
prosectors_effects_ids._pointer             = eff_id + 1000	--// должен быть последним
setmetatable(prosectors_effects_ids,
{__call = function (tbl,num,name)	--// резервирует указанное количество ид, и, если указано имя, заносит в таблицу
	assert(name == nil or tbl[name] == nil)
	local id = tbl._pointer
	tbl._pointer = id + (num or 1)
	assert(tbl._pointer < 100000)	--// кто-то слишком увлекся
	if is_string(name) then
		tbl[name] = id
	end
	return id
end}
)
--//-------------------------
--// Karlan->Team: идея следующая, в файлах может быть несколько функций, соответсвенно если одна - просто имя модуля, если несколько, то "имя модуля .. index", думаю нормально резервировать 5 тиков под модуль
prosectors_freeze_start = {}
prosectors_freeze_start.m_campfire = 500
prosectors_freeze_start.manager_spot1 = 505
prosectors_freeze_start.manager_spot2 = 506
prosectors_freeze_start.m_surge = 2500
prosectors_freeze_start.m_arts = 3000
--//-------------------------
local _DEBUG_       = true
local _DEBUG_GSC_   = false
local module_name   = 'lua_helper'
--//-----------------------------------------------------------------
local log       = function(msg, ...) return _DEBUG_ and log1(string.exformat(module_name..': '..tostring(msg), ...)) end
local printf    = function(msg, ...) return _DEBUG_ and _DEBUG_GSC_ and log1(string.exformat(module_name..':GSC: '..tostring(msg), ...)) end
--//-----------------------------------------------------------------------------------------------
--// Global functions
--//-----------------------------------------------------------------------------------------------
function collect_info(fmt, ...) --// для релизной версии, ставить в опасных местах (или затычках), пусть собирает ошибки в отдельный файл, потом юзер кинет
    warning(fmt, ...)
    local f,d = io.open(getFS():update_path("$logs$", 'collect_info.log'), 'a'), '['..os.date()..'] '
    f:write('\n' .. d..string.rep('-', 75) .. '\n' .. d .. string.exformat(fmt, ...))
    f:close()
end

function wfile(file_name, fmt, ...)
    local f,d = io.open(getFS():update_path("$logs$", file_name), 'a'), '['..os.date()..'] '
    f:write('\n' .. d .. string.exformat(fmt, ...))
    f:close()
end

function run_personal_test()
    local function f()
        local _, err = dofile(GAME_LUA .. "\\personal_test.lua")
        if err then
            get_console():execute("err="..tostring(string.gsub(err, " ", "_")))
        end
    end
    pcall(f)
end
---------------------------------------------------------------------------------------------
--// временно
function create(...)
    local args = table.safe_pack(...)
    local sec
    local npc = actor
    local cnt = 1
    local parent = true
    local res = nil
    for i = 1, args.n do
        local v = args[i]
        if     is_userdata( v ) then npc = v
        elseif is_string( v )   then sec = v
        elseif is_number( v )   then cnt = v
        elseif is_boolean( v )  then parent = v
        end
    end
    if not sec then
        return log("spawn section doesn't exist in arguments of the call")
    elseif not sini:section_exist(sec) then
        return log("section [\"%s\"] doesn't exist in system.ltx", sec)
    end
    for i=1, cnt do
        if parent then
        res = sim:create(sec, npc:id())
        else
        res = sim:create(sec, npc:position(), npc:level_vertex_id(), npc:game_vertex_id())
        end
    end
    
    return res
end
--// а эта тележка чисто для совместимости
function create_ammo(section, position, lvi, gvi, pid, num, func)
	if not is_number(pid) then pid = INVALID_ID end	-- no parent
	local num_in_box = sini:r_u32(section, "box_size")
	local sobj = nil
	while num > 0 do
		local spawn_num = math.min(num,num_in_box)
		num = num - spawn_num
		sobj = sim:create_ammo(section, position, lvi, gvi, pid, spawn_num)
		if func then func(sobj) end
	end
end
---------------------------------------------------------------------------------------------

--//---------------------------------------------
--// Table extensions (todo: унести в другое место)
--//---------------------------------------------
local mt = {}
mt.__call = function(parent, child) 
                return setmetatable(child, {__index = parent, __call = mt.__call}) 
            end
cfg_table = setmetatable({}, mt) --// todo: replace by table.cfg
function cfg_table:get(key) 
    return rawget(self, key) 
end
function cfg_table:set(key, value) 
    return rawset(self, key, value) 
end
cfg_pairs = function (section)
    local t = {}
    local function fill(tab)
        if type(tab) ~= 'table' then return end
        local parent = getmetatable(tab)
        if parent then
            fill(parent.__index)
        end
        for k,v in pairs(tab) do
            if type(v) ~= 'function' then 
                t[k] = v
            end
        end
    end
    fill(section)
    return  function ()
                k,v = next(t,k)
                return k,v
            end
end
--//-----------------------------------------------------------------
--// Ini-file functions (todo: сделать подобие C функций для удобной замены и работы)
--// Description: 
--//-----------------------------------------------------------------
--// Все эти cfg_get_* функции - одна проблема, но субпроблема здесь еще в неверном порядке передачи аргументов, немного исправил
--//---------------------------------------------
function cfg_get(char_ini, method, section, field)
	if is_string(section) then
		if char_ini:value_exist(section, field) then
			return char_ini[method](char_ini, section, field)
		end
	elseif is_table(section) then
		for _,s in ipairs(section) do
			if --[[s ~= "nil" and]] char_ini:value_exist(s, field) then
				return char_ini[method](char_ini, s, field)
			end
		end
	end
	return nil
end
--//---------------------------------------------
--// Получение строки
--//---------------------------------------------
function cfg_get_string(char_ini, section, field, object, mandatory, gulag_name, default_val)
    if (mandatory == nil or not gulag_name) then
        if default_val == nil then
            default_val = object --// Karlan: так куда лучше
        else
            error("section '%s': wrong arguments order in call to cfg_get_string", section) 
        end
    end
    printf("DEBUG: cfg_get_string: section='%s', field='%s'", section, field)
    local res = cfg_get(char_ini, 'r_string', section, field)
    if res ~= nil then
        if gulag_name and gulag_name ~= "" then
            return gulag_name .. "_" .. res
        end
        return res
    end
    if not mandatory then return default_val end
    error([[cfg_get_string: object is [%s]: attempt to read a non-existant string field ["%s"] in section ["%s"]:]], object, field, section)
end
--//---------------------------------------------
--// Получение булева значения
--//---------------------------------------------
function cfg_get_bool(char_ini, section, field, object, mandatory, default_val)
    if mandatory == nil then
        if default_val == nil then
            default_val = object --// Karlan: так куда лучше
        else
            error("section '%s': wrong arguments order in call to cfg_get_bool", section) 
        end
    end
    printf("DEBUG: cfg_get_bool: section='%s', field='%s'", section, field)
    local res = cfg_get(char_ini, 'r_bool', section, field)
    if res ~= nil then
        return res
    end
    if not mandatory then
        if default_val then
            if default_val ~= false and default_val ~= true then
                error("object '%s': section '%s': field '%s': default value is ".."not boolean", object:name(), section, field)
            end
            return default_val
        end
        return false
    end
    error([[cfg_get_bool: object is [%s]: attempt to read a non-existant boolean field ["%s"] in section ["%s"]:]], object, field, section)
end
--//---------------------------------------------
--// Получение числа с плавающей запятой
--//---------------------------------------------
function cfg_get_number(char_ini, section, field, object, mandatory, default_val)
    if mandatory == nil then
        if default_val == nil then
            default_val = object --// Karlan: так куда лучше
        else
            error("section '%s': wrong arguments order in call to cfg_get_number", section) 
        end
    end
    printf("DEBUG: cfg_get_number: section='%s', field='%s'", section, field)
    local res = cfg_get(char_ini, 'r_float', section, field)
    if res ~= nil then
        return res
    end
    if not mandatory then return default_val end
    warning([[cfg_get_number: object is [%s]: attempt to read a non-existant number field ["%s"] in section ["%s"]:]], object, field, section)
end
--//---------------------------------------------
--// Получение любого параметра заданным методом
--//---------------------------------------------
function READ_IF_EXISTS(ltx, method, section, field, default_value)
    -- log('cfg = %s, method = %s, sect = %s, field = %s, defval = %s', ltx, method, section, field, default_value)
    -- log('line_exist %s', ltx:line_exist(section, field))
    if ltx:value_exist(section, field) then
        return ltx[method](ltx, section, field)
    end
    return default_value
end
function READ_IF_EXISTS_SYS(method, section, field, default_value)
    return READ_IF_EXISTS(sini, method, section, field, default_value)
end
--//---------------------------------------------
--// Rulix: мои функи тут, неохота переделывать
--//---------------------------------------------
function str_explode(div,str)
	local t = {}
	for sect in str:gmatch('[^%s*%'..div..']+') do
		if sect and sect ~= "" then
			table.insert(t, sect)
		end
	end
	return t
end
function read_from_ini(ini,sec,lin,def,typ)
	if not (sec and lin) then
		abort("read_from_ini: ini [%s] sec [%s] lin [%s] def [%s] typ [%s]",ini~=nil,sec,lin,def,typ)
	end
	if not ini then
		ini = sini
	end
	if ini:line_exist(sec,lin) then
		if typ == 1 then
			return ini:r_string(sec,lin) or def
		elseif typ == nil then
			return ini:r_float(sec,lin) or def
		elseif typ == 0 then
			local r = ini:r_bool(sec,lin)
			if r == nil then
				return def
			end
			return r
		elseif typ == 2 then
			return ini:r_string_wq(sec,lin) or def
		elseif typ == 3 then
			return ini:r_s32(sec,lin) or def
		elseif typ == 4 then
			return ini:r_vector(sec,lin) or def
		end
	end
	return def
end
function collect_sections(ini,sections,vfunc,dval,convert)
	if vfunc == true then
		vfunc = tonumber
	end
	local r,p,c = {},{},{}
	for k,v in ipairs(sections) do
		if ini:section_exist(v) then
			local n = ini:line_count(v)
			if n > 0 then
				for i = 0,n-1 do
					local res,id,val = ini:r_line(v,i,"","")
					if r[id] == nil then
						r[id] = dval or vfunc and vfunc(val) or val
						if convert ~= nil then
							table.insert(c,convert == false and r[id] or id)
						end
					end
				end
			end
			p[k] = n
		else
			p[k] = 0
		end
	end
	if convert ~= nil then
		return c,r
	end
	return r,p
end
function parse_list(ini,sec,val,convert,vfunc)
	local str = cfg_get(ini,'r_string',sec,val)
	if str == nil then
		return {}
	end
	local tmp = str_explode(",",str)
	if vfunc then
		if vfunc == true then
			vfunc = tonumber
		end
		for i,v in ipairs(tmp) do
			local res = vfunc(v)
			if res ~= nil then
				tmp[i] = res
			end
		end
	end
	if convert then
		local swap,func = convert == -1,type(convert) == "function"
		local t = {}
		for i,v in ipairs(tmp) do
			t[v] = swap and i or func and convert(v,i) or convert
		end
		return t,tmp
	end
	return tmp
end
--//---------------------------------------------
--// Присел ли (актор)
--//---------------------------------------------
function is_crouched(obj)
	if obj:is_actor() then
		return bit_and(obj:get_state(),global_flags.mcCrouch) ~= 0
	end
end
--//---------------------------------------------
--// Получение реального имени
--//---------------------------------------------
function get_string_name(obj, short)
	assert((is_userdata(obj) or is_string(obj)), [[bad argument #1 to get_string_name (string or userdata excepted, got %s)]], type(obj))
	local section = is_string(obj) and obj or obj:section()
	if short and sini:value_exist(section,'inv_name_short') then
		return game.translate_string(sini:r_string(section, 'inv_name_short'))
	end
	return game.translate_string(cfg_get_string(sini, section, 'inv_name', INVALID_NAME))
end
--//---------------------------------------------
--// Получение реальной цены
--//---------------------------------------------
function get_cost(obj)
    assert((is_userdata(obj) or is_string(obj)), [[bad argument #1 to get_cost (string or userdata excepted, got %s)]], type(obj))
    if is_userdata(obj) then
        return obj:cost()
    end
    return READ_IF_EXISTS(sini, 'r_u32', obj, 'cost', INVALID_COST)
end
--//---------------------------------------------
--// Просчет цены с учетом инфляции
--//---------------------------------------------
function calculate_inflation_price(price, maximum, rup)
    assert(is_number(price), [[bad argument #1 to 'calculate_inflation_price' (number excepted, got %s)]], type(price))
    assert(manager_trade, [['manager_trade' module is unavailable in 'calculate_inflation_price' (table excepted, got '%s')]], type(manager_trade))
    local ci = manager_trade.get_inflation()
    local fv = (price/100)*ci + price
    local pv = fv * 1/(1+ci/100)^(-1/365)
    local gain = math.ceil(pv-price)
    local result = price+gain
    if is_boolean(rup) then
        result = manager_trade.calculate_round_value(result, rup)
    end
    if is_number(maximum) then result = math.min(result,maximum) end
    return result
end
--//---------------------------------------------
--// Items functional
--//---------------------------------------------
function has_kv_objects(tab, obj)
    assert(is_table(tab), [[bad argument #1 to 'has_kv_objects' (table excepted, got '%s')]], type(tab))
    obj = obj or actor
    for _,v in ipairs(tab) do
        if not obj:has_object_func(v.section,ammo_sections[v.section] and math.ceil(v.prob/sini:r_u32(v.section, "box_size")) or v.prob,v.cond,inv_is_item_accessible) then
            return false
        end
    end
    return true
end

function get_item_protection(obj, wc)
    assert((is_userdata(obj) or is_string(obj)), [[bad argument #1 to get_item_protection (string or userdata excepted, got %s)]], type(obj))
    if is_userdata(obj) then
        wc = wc or false
        return obj:get_summary_protection(wc)
    end
    local res = 0
    for _,v in pairs(HIT_TYPE_BY_ID) do
        res = res + (READ_IF_EXISTS_SYS('r_float', obj, v..'_protection', 0) * (wc or 1))
    end
    return res
end
--//-----------------------------------------------------------------
--// Vectors functions
--// Description: операции с векторами
--//-----------------------------------------------------------------
rad2deg = function(r) return r * 180.0 / math.pi end --// перевод угла из радианов в градусы
deg2rad = function(d) return d * math.pi / 180.0 end --// перевод угла из градусов в радианы
--//-------------------------
vector.compare          = function(v1, v2) return v1.x == v2.x and v1.y == v2.y and v1.z == v2.z end             --// сравнивает два вектора
vector.copy             = function(value, p2, p3) --// создает и возвращает копию вектора
    if tonumber(value) then return vector():set(value, p2, p3) end
    if type(value) == 'userdata' then return vector():set(value) end
    return type(value) == 'table' and (tonumber(value.x) and vector():set(value.x, value.y, value.z) or vector():set(value[1], value[2], value[3]))  
end
vector.in_frustrum      = function(obj1, obj2, patrol_path, path_point) --// проверяет находится ли второй объект в зоне видимости первого объекта
    if patrol_path ~= "" and path_point then obj2 = patrol_path:point(path_point) end
    local dir1 = obj1 == actor and device().cam_dir or obj1:direction()
    local dir2 = obj1:position():sub(is_vector(obj2) and obj2 or obj2:position())
    return vector.yaw_deg3d(dir1, dir2) < 35
end
vector.in_actor_frustrum = function(obj)
    return vector.in_frustrum(actor, obj)
end
vector.angle_rad        = function(v1, v2) --// угол между векторами в радианах
    local b1 = v1:normalize()
    local b2 = v2:normalize()
    local dotp = b1:dotproduct(b2)
    return math.acos(math.abs(dotp))
end
vector.angle_deg        = function(v1, v2) --// угол между векторами в градусах
    return rad2deg(vector.angle_rad(v1, v2))
end
vector.angle_left       = function(v1, v2)
    local res = vector()
    res:crossproduct(v1, v2)
    return res.y <= 0
end
vector.angle_left_xz    = function(v1, v2)
    v1.y = 0; v2.y = 0;
    return vector.angle_left(v1, v2)
end
vector.yaw_rad          = function(v1, v2) --// угол между векторами в радианах
    return math.acos(((v1.x*v2.x) + (v1.z*v2.z))/(math.sqrt(v1.x*v1.x + v1.z*v1.z)*math.sqrt(v2.x*v2.x + v2.z*v2.z)))
end
vector.yaw_deg          = function(v1, v2) --// угол между векторами в градусах
    return rad2deg(vector.yaw_rad(v1, v2))
end
vector.yaw_deg3d        = function(v1, v2) --// угол между векторами в градусах в тройном измерении
    return rad2deg(math.acos((v1.x*v2.x + v1.y*v2.y + v1.z*v2.z)/(math.sqrt(v1.x*v1.x + v1.y*v1.y + v1.z*v1.z )*math.sqrt(v2.x*v2.x + v2.y*v2.y + v2.z*v2.z))))
end
vector.rotate_y         = function(v, angle) --// вращает вектор вокруг оси y против часовой стрелки
    angle = deg2rad(angle)
    local c = math.cos(angle)
    local s = math.sin(angle)
    return vector():set(v.x * c - v.z * s, v.y, v.x * s + v.z * c)
end
vector.zero             = function() return vector():set(0, 0, 0) end
--//-------------------------
--// возвращает расстояние (или квадрат расстояния) между двумя гейм-объектами или векторами
function distance_between(obj1, obj2, sqrt)
    obj1 = is_vector(obj1) and obj1 or obj1:position()
    obj2 = is_vector(obj2) and obj2 or obj2:position()
    return sqrt and obj1:distance_to_sqr(obj2) or obj1:distance_to(obj2)
end

--//-----------------------------------------------------------------
--// Vertex&Paths functions
--// Description: 
--//-----------------------------------------------------------------
function valid_level_vertex_id(lvid)
	return lvid < INVALID_LEVEL_VERTEX_ID and level.valid_vertex_id(lvid)
end
--//---------------------------------------------
--// получение ближайшего гейм-вертекса к вектору на заданном уровне
--//---------------------------------------------
function get_nearest_game_vertex(lname, position)
    assert(is_table(game_vertex_table[lname]), 'Invalid level name')
    assert(is_vector(position), 'Invalid #2 argument')
    local gv_min, gv_max = game_vertex_table[lname][1], game_vertex_table[lname][2]
    local gg = game_graph()
    local dist = math.huge
    local res_gv = nil
    for i=gv_min, gv_max do
        local new_dist = distance_between(position, gg:vertex(i):level_point())
        if dist > new_dist then
            dist = new_dist
            res_gv = i
        end
    end
    return res_gv
end
--//---------------------------------------------
--// возвращает расстояние между двумя точками графа с учетом разности уровней
--//---------------------------------------------
function graph_distance(vid1, vid2)
    local p1 = game_graph():vertex(vid1):game_point()
    local p2 = game_graph():vertex(vid2):game_point()
    --printf("GRAPH DISTANCE [%s][%s][%s] : [%s][%s][%s]", p1.x, p1.y, p1.z, p2.x, p2.y, p2.z)
    return game_graph():vertex(vid1):game_point():distance_to(game_graph():vertex(vid2):game_point())
end
--//---------------------------------------------
--// возвращает расстояние между двумя объектами с учетом разности уровней
--//---------------------------------------------
function sim_dist_to(obj1 , obj2)
    return graph_distance(obj1.m_game_vertex_id, obj2.m_game_vertex_id)
end

--//-----------------------------------------------------------------
--// Game/Server objects functions
--// Description: функции для работы с игровыми объектами
--//-----------------------------------------------------------------
--//---------------------------------------------
--// объект из инвентаря владельца по айди
--//---------------------------------------------
function get_inventory_item_by_id(id, obj)
    local result = nil
    obj = obj or actor
    obj:iterate_inventory(function(_, item)
        if item:id() == id then
            result = item
            return true
        end
        return false
    end)
    -- log('called get_inventory_item_by_id: input = [%s], [%s]; result iterate = %s', id, obj, result)
    return result
end
--//---------------------------------------------
--// есть-ли аномалии вокруг заданного объекта/вектора в заданном диапазоне
--//---------------------------------------------
function around_anomaly(meters, obj_compare, section_compare, pattern, trim)
    local obj_compare, need_section_compare, objects, objc_id = obj_compare or actor, is_string(section_compare)
    assert(is_number(meters), [[bad argument #1 to 'around_anomaly' (number excepted, got %s)]], type(meters))
    assert(is_userdata(obj_compare), [[bad argument #2 to 'around_anomaly' (userdata excepted, got %s)]], type(obj_compare))
	if is_vector(obj_compare) then
		objects = level.get_anomalies(meters,obj_compare)
	else
		objects = level.get_anomalies(meters, is_game_object(obj_compare) and obj_compare:position() or obj_compare.position)
		objc_id = is_game_object(obj_compare) and obj_compare:id() or obj_compare.id
	end
	for i,obj in ipairs(objects) do
		if not objc_id or objc_id ~= obj:id() then
            if need_section_compare then
                local section = obj:section()
                section_compare = trim and section_compare:gsub(section_compare:match('_%w*$'), '') or section_compare
                if pattern and section:find(section_compare) or (section == section_compare) then
                    return true
                end
			else
				return true
            end
		end
	end
    return false
end
--//---------------------------------------------
--//---------------------------------------------
local function around_storage_objects(tstype, meters, obj_compare, alive_only, dead_only, return_objects)
	assert(is_number(meters), [[bad argument #1 to 'around_storage_objects' (number excepted, got %s)]], type(meters))
	local obj_compare, meters, objc_id, pos, res = obj_compare or actor, meters*meters, false
	assert(is_userdata(obj_compare), [[bad argument #2 to 'around_storage_objects' (userdata excepted, got %s)]], type(obj_compare))
	if is_vector(obj_compare) then
		pos = obj_compare
	else
		pos = is_game_object(obj_compare) and obj_compare:position() or obj_compare.position
		objc_id = is_game_object(obj_compare) and obj_compare:id() or obj_compare.id
	end
	for id,t in pairs(db.storage) do
		if t.stype == tstype then
			local obj = level.object_by_id(id)
			if obj and (not objc_id or objc_id ~= obj:id()) and not (alive_only and not obj:alive() or dead_only and obj:alive()) then
				if obj:position():distance_to_sqr(pos) < meters then
                    if not return_objects then
                        return true
                    end
                    if not is_table(res) then res = {} end
                    table.insert(res, obj)
				end
			end
		end
	end
	return res
end
--//---------------------------------------------
--// есть-ли сталкеры вокруг заданного объекта/вектора в заданном диапазоне
--//---------------------------------------------
function around_stalkers(meters, obj_compare, alive_only, dead_only, return_objects)
	return around_storage_objects(modules.stype_stalker, meters, obj_compare, alive_only, dead_only, return_objects)
end
--//---------------------------------------------
--// есть-ли монстры вокруг заданного объекта в заданном диапазоне (по умолчанию: switch_distance)
--//---------------------------------------------
function around_monsters(meters, obj_compare, alive_only, dead_only, return_objects)
	return around_storage_objects(modules.stype_mobile, meters, obj_compare, alive_only, dead_only, return_objects)
end
--//---------------------------------------------
--// получаем имя модели
--//---------------------------------------------
function get_model_name(obj, with_tail)
    assert(is_userdata(obj) or is_string(obj), [[is_userdata(obj) or is_string(obj)]])
    local model_name = string.split(is_string(obj) and obj or (is_game_object(obj) and obj:get_visual_name() or obj:get_cse_visual():get_visual()), '\\')
    model_name = model_name[#model_name]
    return model_name..(with_tail and ".ogf" or "")
end
--//---------------------------------------------
--// получаем объект, который находится в онлайне, nil - если в оффлайне или его нет в природе
--//---------------------------------------------
function get_online_object(obj_data) 
    assert((is_string(obj_data) or is_number(obj_data)), [[bad argument #1 to 'get_object_online' (string or number excepted, got %s)]], type(obj_data))
    return is_number(obj_data) and level.object_by_id(obj_data) or level.object_by_name(obj_data)
end
--//---------------------------------------------
--// получаем имя по айди
--//---------------------------------------------
function get_name_by_id(id)
    local sobj = sim:object(id)
    if sobj then
        return sobj:name()
    end
    return INVALID_NAME
end
--//---------------------------------------------
--// проверяем тип
--//---------------------------------------------
function is_game_object(obj)
    assert(is_userdata(obj), [[bad argument #1 to is_game_object (userdata excepted, got %s)]], type(obj))
    if is_function(obj.is_online) and not obj:is_online() then --// Karlan: пытаемся применить к удаленному/не готовому клиентскому
if PROSECTORS_DEBUG then
        warning('!ERROR: object is destroyed, [%s]', obj:get_destroy()) --// логировать нельзя, так как все методы идут через получение несуществующего объекта
end --// под дебагом будем бомбить движок, чтобы он давал хоть какое-то логирование, ошибка вполне явно устанавливается по стеку и тривиально устраняется
        return false --// тут не понятно ушел он в оффлайн или удален, впрочем в данном контексте это не так и важно
-- end
    end
    return obj:is_game_object()
end
--//---------------------------------------------
--// объект в инвентаре актора
--//---------------------------------------------
function parent_is_actor(obj)
	local parent = obj:parent()
	return parent and parent:id() == actor_id
end
--//---------------------------------------------
--// получаем порядковый номер класса объекта из таблицы clsid
--//---------------------------------------------
function get_clsid(obj)
    assert(is_userdata(obj), [[bad argument #1 to get_clsid (userdata excepted, got %s)]], type(obj))
    return obj and obj:clsid()
end 
--//---------------------------------------------
--// получаем story id 
--//---------------------------------------------
function get_sid(obj)
    return is_game_object(obj) and obj:story_id() or obj.m_story_id
end 
--//---------------------------------------------
--// имеет ли объект story_id
--//---------------------------------------------
function is_story_object(obj)
    return get_sid(obj) ~= INVALID_STORY_ID
end
--//---------------------------------------------
--// имя является уникальным (не содержит ID объекта)
--//---------------------------------------------
function is_uniq_name(name,id)	-- по идее id передавать не обязательно
	if is_userdata(name) then
		id = get_id(name)
		name = name:name()
	end
	assert(is_string(name))
	local subid = tonumber(string.sub(name,-5,-1))
	if id then return subid ~= id
	else return subid == nil end
end
--//---------------------------------------------
--// получаем айди по стори айди, применимо к серверным объектам
--//---------------------------------------------
function id_by_sid(sid)
    sid = tonumber(sid) or sid
    if is_string(sid) then sid = story_ids[sid] end
    local se_obj = sim and sim:story_object(sid)
    return se_obj and se_obj.id
end
function sid_by_id(id) 
    local se_obj = sim and sim:object(id)
    return se_obj and se_obj.m_story_id
end
--//---------------------------------------------
--// получаем гейм-объект по стори айди
--//---------------------------------------------
function level.object_by_sid(sid)
    local id = id_by_sid(sid)
    return is_number(id) and level.object_by_id(id) or nil
end
--//---------------------------------------------
--// получение id объекта.  obj == userdasta or number(sid) or string(name)
--//---------------------------------------------
function get_id(obj, no_assert)
    if is_number(obj) then
        return id_by_sid(obj)
    elseif is_userdata(obj) then -- сам объект
        return is_function(obj.id) and obj:id() or obj.id
    elseif is_string(obj) then -- имя объекта
        local o = sim:object(obj)
        if o then
            return o.id
        end
    end
    if PROSECTORS_DEBUG and not no_assert then
        -- abort('id~=INVALID_ID, object id[%s] is invalid!')
        -- warning('id~=INVALID_ID, object id[%s] is invalid!')
    end
    return INVALID_ID
end
--//---------------------------------------------
--// установить custom_data для объекта
--//---------------------------------------------
function set_custom_data(obj, cd)
    assert(is_string(cd), [[bad argument #2 to 'set_custom_data' (string excepted, got %s)]], type(cd))
    if is_game_object(obj) then
        obj:set_spawn_ini(cd)
    else
        obj:set_custom_data(cd)
    end
end
--//---------------------------------------------
--// нанести хит объекту
--//---------------------------------------------
function send_hit(obj, type, power, draftsman, bone, dir, impulse, ignore_armor, event)
	local h = hit()
	h.type = type
	h.power = power
	h.draftsman = draftsman or obj
	if is_string(bone) then h.bone = bone end
	if is_vector(dir) then h.direction = dir end
	if is_number(impulse) then h.impulse = impulse end
	if is_boolean(ignore_armor) then h.ignore_armor = ignore_armor end
	obj:hit(h,not event)
end
--//---------------------------------------------
--// установить позицию для объекта
--//---------------------------------------------
function set_position(obj, p1, p2, p3)
    assert((is_table(p1) or is_userdata(p1) or (is_number(p1) and is_number(p2) and is_number(p3))), [[wrong arguments to 'set_position' <[%s]:[%s]:[%s]>]], p1,p2,p3)
    local v = vector.copy(p1, p2, p3)
    assert(v, 'bad vector <%s>', v)
    if is_game_object(obj) then
        obj:set_position(v)
    else
        obj.position = v
    end
end
--//---------------------------------------------
--// установить направление взгляда для объекта
--//---------------------------------------------
function set_direction(obj, p1, p2, p3)
    assert((is_table(p1) or is_userdata(p1) or (is_number(p1) and is_number(p2) and is_number(p3))), [[wrong arguments to 'set_position' <[%s]:[%s]:[%s]>]], p1,p2,p3)
    local v = vector.copy(p1, p2, p3)
    assert(v, 'bad vector <%s>', v)
    if is_game_object(obj) then
        obj:set_direction(v)
    else
        obj.direction = v
    end
end
--//---------------------------------------------
--// находится-ли объект в зоне (либо хотя бы один объект из таблицы)
--//---------------------------------------------
function obj_in_zone(obj, zone, tbl)
    if is_table(tbl) then
        for _, v in pairs(tbl) do --// скорее всего лист, todo: прооверить
            local npc = v == 'actor' and actor or level.object_by_sid(v)
            if npc and zone and zone:inside(npc:center()) then
                return true
            end
        end
        return false
    end
    return obj and zone and zone:inside(obj:center())
end
--//---------------------------------------------
--// находится-ли объект вне зоны
--//---------------------------------------------
function obj_out_zone(obj, zone) 
    return not obj_in_zone(obj, zone)
end
--//---------------------------------------------
--// Community functional
--//---------------------------------------------
function equal_obj_community(comm, obj, class_id)
    obj = is_userdata(obj) and obj or actor
    return comm == get_obj_community(obj, class_id)
end

function get_obj_community(obj, class_id)
    local clsid = class_id or get_clsid(obj)
    return is_game_object(obj) and get_character_community(obj, clsid) or get_alife_character_community(obj, clsid)
end

function get_character_community(obj, clsid)
    if IsHuman(obj, clsid) then
        if obj.character_community then
            return obj:character_community()
        end
        warning("get_character_community:obj=[%s],clsid=[%s]", obj.name and obj:name(), get_clsid(obj))
    end
    return get_mob_community(obj, clsid)
end

function get_alife_character_community(sobj, clsid)
    if IsHuman(sobj, clsid) then
        if sobj.community then
            return sobj:community()
        elseif sobj.id == actor_id then
            return get_character_community(actor, clsid)
        else
            warning("get_alife_character_community:obj=[%s],clsid=[%s]", sobj.name and sobj:name(), get_clsid(sobj))
        end
    end
    return get_mob_community(sobj, clsid)
end

function get_mob_community(obj, class_id)
    local clsid = class_id or get_clsid(obj)
    local community = clsid and monster_classes[clsid]
    if community then
        return community
--  elseif clsid == clsid.psy_dog_phantom_s then
--      return "phantom"
    end
    warning("get_mob_community:obj=[%s],class_id=[%s]", obj.name and obj:name(), clsid)
    return "monster"
end
--//---------------------------------------------
--// Trade functional
--//---------------------------------------------
function block_trade(obj, block)
    assert(is_userdata(obj), [[bad argument #1 to trade_verify (userdata excepted, got %s)]], type(obj))
    assert(IsHuman(obj), [[bad argument #1 to trade_verify (IsHuman excepted, got %s)]], get_clsid(obj))
    obj:show_trade_btn(not block)
    if block then
        obj:disable_trade()
    else
        obj:enable_trade()
    end
end

function blocked_trade(obj)
    assert(is_userdata(obj), [[bad argument #1 to trade_verify (userdata excepted, got %s)]], type(obj))
    assert(IsHuman(obj), [[bad argument #1 to trade_verify (IsHuman excepted, got %s)]], get_clsid(obj))
    return not (obj:is_trade_enabled() and obj:is_shown_trade_btn())
end
--//---------------------------------------------
--// Talk functional
--//---------------------------------------------
--// здесь юзердаты существуют абсолютно всегда, в противном случае будет сбой в движке и вылет по движку
function get_actor_talk(first_speaker, second_speaker)
    if first_speaker:id() == actor_id then
        return first_speaker
    end
    return second_speaker
end
function get_npc_talk(first_speaker, second_speaker)
    if first_speaker:id() == actor_id then
        return second_speaker
    end
    return first_speaker
end
function get_actor_npc_talk(first_speaker, second_speaker)
    if first_speaker:id() == actor_id then
        return first_speaker, second_speaker
    end
    return second_speaker, first_speaker
end
--//---------------------------------------------
--// Map Spot functional
--//---------------------------------------------
--// эти функции пока написал без защиты от дебилов, так что если вы полетели на этих функциях, делайте выводы ;) TODO
function has_map_spot(id, _type)
    assert(level.present(), 'level is not loaded')
    local count = level.map_has_object_spot(id, _type)
    if count ~= 0 then
        return true, count --// Karlan: сделаем функцию булевой, но и не будем жадничать, отдадим вторым (наименее релевантным) параметром количество спотов на объекте
    end
    return false
end
function has_map_spot_by_sid(sid, _type) --// поддержка стиля, по сути ненужны
    local id = get_id(sid)
    return has_map_spot(id, _type)
end
function get_map_spot_hint(id, _type)
    assert(level.present(), 'level is not loaded')
    assert(has_map_spot(id, _type), 'id does not any type')
    return level.map_get_spot_hint(id, _type) 
end
function set_map_spot_hint(id, _type, hint)
    assert(level.present(), 'level is not loaded')
    assert(has_map_spot(id, _type), 'id does not any type')
    level.map_change_spot_hint(id, _type, hint or "no_hint") 
end
function highlight_map_spot_hint(id, _type, ...)
    assert(level.present(), 'level is not loaded')
    local color_type = type(arg[1])
    local str = is_string(arg[1]) and arg[1] or '255,255,255,255'
    if color_type == 'number' then
        local r,g,b,a = unpack(arg)
        str = tostring(r) .. ',' .. tostring(g) .. ',' .. tostring(b) .. ',' .. tostring(a)
    elseif color_type == 'table' then
        if is_array(arg[1]) then
            local r,g,b,a = unpack(arg[1])
            str = tostring(r) .. ',' .. tostring(g) .. ',' .. tostring(b) .. ',' .. tostring(a)
        else
            str = tostring(arg[1].r) .. ',' .. tostring(arg[1].g) .. ',' .. tostring(arg[1].b) .. ',' .. tostring(arg[1].a)
        end
    end
    level.map_highlight_spot(id, _type, str)
end
function remove_map_spot(id, _type, not_all)
    assert(level.present(), 'level is not loaded')
    local has, cnt = has_map_spot(id, _type)
    if not has then return end --// Karlan: если меток никаких не стоит, то и чистить нечего
    if not not_all then --// Karlan: по умолчанию удаляем все
        for i=0, cnt do
            level.map_remove_object_spot(id, _type)
        end
        return
    end
    level.map_remove_object_spot(id, _type)
end
function remove_map_spot_by_sid(sid, _type, not_all) --// поддержка стиля, по сути ненужны
    local id = get_id(sid)
    if id == INVALID_ID then return error_warn('remove_map_spot_by_sid STOP: bad sid [%s]', sid) end
    remove_map_spot(id, _type, not_all)
end
--// позиционная метка всегда сохраняется(!), могу сделать в движке опционально если надо
function add_map_spot(id, _type, hint, ser, not_del, not_all, pos, ln)
    assert(level.present(), 'level is not loaded')
    if not not_del then --// по умолчанию почистим объект от предыдущих спотов (флаг оставлен для создания псевдо-комплексного спота)
        remove_map_spot(id, _type, not_all)
    end
    if pos and ln then
        return level.map_add_position_spot_ser(pos, ln, _type, hint or "no_hint") --// тут необходимо возвратить айди метки, чтобы ее не потерять
    else
        level['map_add_object_spot'..(ser and '_ser' or '')](id, _type, hint or "no_hint")
    end
	-- assert(not has_map_spot(0, _type), '!ERROR: Actor has spot [%s]:[%s]', _type, hint)
    if id~=0 and has_map_spot(0, _type) then collect_info('ACTOR HAS SPOT [%s]:[%s]', _type, hint) remove_map_spot(0, _type) end
end
function add_map_spot_by_sid(sid, _type, hint, ser, not_del, not_all)
    local id = get_id(sid) --// Karlan->ALL: на самом деле это позволяет ставить метку не только по сиду, но и по имени и собственно на сам объект (не важно серверный или клиентский)
    if id == INVALID_ID then return error_warn('add_map_spot_by_sid STOP: bad sid [%s]', sid) end
    add_map_spot(id, _type, hint, ser, not_del, not_all)
end
--//---------------------------------------------
--// Relation functional
--//---------------------------------------------
--// Variables
local const_factions_relation = 
{
    ['enemy'] = -5000,
    ['neutral'] = 0,
    ['friend'] = 5000,
}
local game_relations_by_num = 
{
    [game_object.enemy] = 'enemy',
    [game_object.friend] = 'friend',
    [game_object.neutral] = 'neutral',
}
--// Factions
function get_factions_community_relation(faction, faction_to)
    assert(is_string(faction), [[bad argument #1 to get_factions_community_relation (string excepted, got %s)]], type(faction))
    assert(is_string(faction_to), [[bad argument #2 to get_factions_community_relation (string excepted, got %s)]], type(faction_to))
    return relation_registry.community_relation(faction, faction_to)
end

function set_factions_community_relation(faction, faction_to, new_community)
    assert(is_string(faction), [[bad argument #1 to set_factions_community_relation (string excepted, got %s)]], type(faction))
    assert(is_string(faction_to), [[bad argument #2 to set_factions_community_relation (string excepted, got %s)]], type(faction_to))
    assert((is_number(new_community) or is_string(new_community)), [[bad argument #3 to set_factions_community_relation (string or number excepted, got %s)]], type(new_community))
    if tonumber(new_community) then
        relation_registry.set_community_relation(faction, faction_to, new_community)
    else
        assert(const_factions_relation[new_community], [[const_factions_relation doesn't exist [%s] field]], new_community)
        relation_registry.set_community_relation(faction, faction_to, const_factions_relation[new_community])
    end
end

function get_factions_community_goodwill(faction_name, obj_id)
    assert(is_string(faction_name), [[bad argument #1 to get_factions_community_goodwill (string excepted, got %s)]], type(faction_name))
    assert(is_number(obj_id), [[bad argument #2 to get_factions_community_goodwill (number excepted, got %s)]], type(obj_id))
    return relation_registry.community_goodwill(faction_name, obj_id)
end

function set_factions_community_goodwill(faction_name, obj_id, new_goodwill)
    assert(is_string(faction_name), [[bad argument #1 to set_factions_community_goodwill (string excepted, got %s)]], type(faction_name))
    assert(is_number(obj_id), [[bad argument #2 to set_factions_community_goodwill (number excepted, got %s)]], type(obj_id))
    assert(is_number(delta), [[bad argument #3 to set_factions_community_goodwill (number excepted, got %s)]], type(new_goodwill))
    relation_registry.set_community_goodwill(faction_name, obj_id, new_goodwill)
end

function change_factions_community_goodwill(faction_name, obj_id, delta)
    assert(is_string(faction_name), [[bad argument #1 to change_factions_community_goodwill (string excepted, got %s)]], type(faction_name))
    assert(is_number(obj_id), [[bad argument #2 to change_factions_community_goodwill (number excepted, got %s)]], type(obj_id))
    assert(is_number(delta), [[bad argument #3 to change_factions_community_goodwill (number excepted, got %s)]], type(delta))
    relation_registry.change_community_goodwill(faction_name, obj_id, delta)
end

--// получает сумму из персонального отношения группировки первого объекта ко второму + общие отношения группировок 
function get_general_goodwill_between_objects(obj_id, obj_id_to)
    assert(is_number(obj_id), [[bad argument #1 to get_general_goodwill_between_objects (number excepted, got %s)]], type(obj_id))
    assert(is_number(obj_id_to), [[bad argument #2 to get_general_goodwill_between_objects (number excepted, got %s)]], type(obj_id_to)) 
    return relation_registry.get_general_goodwill_between(obj_id, obj_id_to)
end

is_factions_friends = function(faction, faction_to) return get_factions_community_relation(faction, faction_to) >= FRIENDS end
is_factions_enemies = function(faction, faction_to) return get_factions_community_relation(faction, faction_to) <= ENEMIES end
is_factions_neutral = function(faction, faction_to) return not (is_factions_enemies() and is_factions_friends()) end
--// NPCs
function is_friend(npc1, npc2, both)
    assert(is_userdata(npc1), [[bad argument #1 to is_friend (userdata excepted, got %s)]], type(npc1))
    assert(is_userdata(npc2), [[bad argument #2 to is_friend (userdata excepted, got %s)]], type(npc2))
    local res = npc1:relation(npc2) == game_object.friend
    if both and res then
        res = npc2:relation(npc1) == game_object.friend
    end
    return res
end

function get_npc_relation(npc1, npc2)
    assert(is_userdata(npc1), [[bad argument #1 to get_npc_relation (userdata excepted, got %s)]], type(npc1))
    assert(is_userdata(npc2), [[bad argument #2 to get_npc_relation (userdata excepted, got %s)]], type(npc2))
    local relation = npc1:relation(npc2)
    return relation, game_relations_by_num[relation]
end

function set_npc_relation(npc1, npc2, new_relation)
    assert(is_userdata(npc1), [[bad argument #1 to set_npc_relation (userdata excepted, got %s)]], type(npc1))
    assert(is_userdata(npc2), [[bad argument #2 to set_npc_relation (userdata excepted, got %s)]], type(npc2))
    assert((is_string(new_relation) or is_number(new_relation)), [[bad argument #3 to set_npc_relation (string or number excepted, got %s)]], type(new_relation))
    local n, s = get_npc_relation(npc1, npc2)
    if is_number(new_relation) and n == new_relation then return end
    if is_string(new_relation) and s == new_relation then return end
    local invert = table.invert(game_relations_by_num)
    new_relation = (is_string(new_relation) and invert[new_relation] or error([[invert doesn't exist [%s] field]], new_relation)) or (is_number(new_relation) and new_relation)
    npc1:set_relation(new_relation, npc2)
end

function get_npc_goodwill(npc1, npc2)
    assert(is_userdata(npc1), [[bad argument #1 to get_npc_goodwill (userdata excepted, got %s)]], type(npc1))
    assert(is_userdata(npc2), [[bad argument #2 to get_npc_goodwill (userdata excepted, got %s)]], type(npc2))
    -- return npc1:goodwill(npc2)
    local npc1_id, npc2_id = get_id(npc1), get_id(npc2)
    return relation_registry.get_goodwill(npc1_id, npc2_id)
end

function get_weighted_goodwill(npc1, npc2)
    return (get_npc_goodwill(npc1, npc2)+get_npc_goodwill(npc2, npc1))/2
end

function get_two_goodwill(npc1, npc2)
    return math.abs(get_npc_goodwill(npc1, npc2)-get_npc_goodwill(npc2, npc1))
end

function set_npc_goodwill(npc1, npc2, new_goodwill)
    assert(is_userdata(npc1), [[bad argument #1 to set_npc_goodwill (userdata excepted, got %s)]], type(npc1))
    assert(is_userdata(npc2), [[bad argument #2 to set_npc_goodwill (userdata excepted, got %s)]], type(npc2))
    assert(is_number(new_goodwill), [[bad argument #3 to set_npc_goodwill (number excepted, got %s)]], type(new_goodwill))
    npc1:set_goodwill(new_goodwill, npc2)
end
--// Karlan: от предыдущего отличается тем, что отсутствует ограничение, и из передаваемого значения сразу вычитаются (прибавляются) отношения между группировками, иными словами вычитается значение, которое можно получить функцикцией get_general_goodwill_between_objects
function force_set_npc_goodwill(npc1, npc2, new_goodwill)
    assert(is_userdata(npc1), [[bad argument #1 to force_set_npc_goodwill (userdata excepted, got %s)]], type(npc1))
    assert(is_userdata(npc2), [[bad argument #2 to force_set_npc_goodwill (userdata excepted, got %s)]], type(npc2))
    assert(is_number(new_goodwill), [[bad argument #3 to force_set_npc_goodwill (number excepted, got %s)]], type(new_goodwill))
    npc1:force_set_goodwill(new_goodwill, npc2)
end

function change_npc_goodwill(npc1, npc2, delta, min_value, talk_msg)
    assert(is_userdata(npc1), [[bad argument #1 to change_npc_goodwill (userdata excepted, got %s)]], type(npc1))
    assert(is_userdata(npc2), [[bad argument #2 to change_npc_goodwill (userdata excepted, got %s)]], type(npc2))
    assert(is_number(delta), [[bad argument #3 to change_npc_goodwill (number excepted, got %s)]], type(delta))
	if is_number(min_value) and delta < 0 then
		local current = npc1:goodwill(npc2)
		if current > min_value then
			delta = math.max(delta, min_value-current)
		else
			return
		end
	end
    if (min_value == true) or talk_msg then
        local task_texture, task_rect = get_texture_info("ui_inGame2_PD_Diplomat")
        actor:give_talk_message(string.format(game.translate_string("change_npc_goodwill_".. (delta < 0 and "minus" or "plus")), colors.gray, delta < 0 and colors.red or colors.green, delta), task_texture, task_rect, "iconed_answer_item", false)
    end
    npc1:change_goodwill(delta, npc2)
    
end
--// Karlan: получить симпатию можно вот так: npc:sympathy()
function set_npc_sympathy(npc, new_sympathy)
    assert(is_userdata(npc), [[bad argument #1 to set_npc_sympathy (userdata excepted, got %s)]], type(npc))
    assert(is_number(new_sympathy), [[bad argument #2 to set_npc_sympathy (number excepted, got %s)]], type(new_sympathy))
    npc:set_sympathy(math.clamp(new_sympathy, 0, 1))
end

function get_npc_general_goodwill(npc1, npc2)
    assert(is_userdata(npc1), [[bad argument #1 to get_npc_general_goodwill (userdata excepted, got %s)]], type(npc1))
    assert(is_userdata(npc2), [[bad argument #2 to get_npc_general_goodwill (userdata excepted, got %s)]], type(npc2))
    return npc1:general_goodwill(npc2)
end

function get_npc_community_goodwill(npc, faction)
    assert(is_userdata(npc), [[bad argument #1 to get_npc_community_goodwill (userdata excepted, got %s)]], type(npc))
    assert(is_string(faction), [[bad argument #2 to get_npc_community_goodwill (string excepted, got %s)]], type(faction))
    return npc:community_goodwill(faction)
end

function set_npc_community_goodwill(npc, faction, new_goodwill)
    assert(is_userdata(npc), [[bad argument #1 to set_npc_community_goodwill (userdata excepted, got %s)]], type(npc))
    assert(is_string(faction), [[bad argument #2 to set_npc_community_goodwill (string excepted, got %s)]], type(faction))
    assert(is_number(new_goodwill), [[bad argument #3 to set_npc_community_goodwill (number excepted, got %s)]], type(new_goodwill))
    npc:set_community_goodwill(faction, new_goodwill)
end
--// TODO: сделать для гулагов
function gulag_is_enemy(gulag, npc)
    assert((is_string(gulag) or is_number(gulag)), [[bad argument #1 to gulag_is_enemy (string or number excepted, got %s)]], type(gulag))
    local obj = npc or actor
    assert(is_userdata(obj), [[bad argument #2 to gulag_is_enemy (userdata excepted, got %s)]], type(obj))
    local gulag = m_gulag.get_gulag(gulag)
    --// Karlan->Karlan: возможно стоит напистаь общую функцию валидности гулага, чтобы не запрашивать методы для недоступных в данный момент гулагов
    return gulag and gulag.npc_is_enemy_to_anybody and gulag:npc_is_enemy_to_anybody(obj)
end
--//---------------------------------------------
--// Inventory flags functional
--//---------------------------------------------
--// установить флаг
function set_flag(obj, flag)
    obj:set_inventory_item_flags(flags16():assign(obj:get_inventory_item_flags()):set(flag, true))
end
--// проверка "установлен ли флаг у объекта"
function get_flag(obj, flag)
    return flags16():assign(obj:get_inventory_item_flags()):is_any(flag)
end
--// снять флаг
function remove_flag(obj, flag)
    obj:set_inventory_item_flags(flags16():assign(obj:get_inventory_item_flags()):set(flag, false))
end
--//---------------------------------------------
--// Inventory functional
--//---------------------------------------------
function is_dropable_slot(slot)
    local slots = {
    [KNIFE_SLOT]      = true,
    [PISTOL_SLOT]     = true,
    [RIFLE_SLOT]      = true,
    [GRENADE_SLOT]    = true,
    [APPARATUS_SLOT]  = true,
    }
    return slots[slot]
end
function drop_active_item()
    local active_item = actor:active_item()
    if active_item and is_dropable_slot(actor:active_slot()) then
        actor:drop_item(active_item,6)
    end
	local sec = actor:get_actor():secondary_item()
	if sec then actor:drop_item(sec,6) end
end
--// проверить нужной-ли секции итем лежит в слоте, если слот пуст или секция не совпадает возвращает false
function equal_slot_item_section(slot, section)
    local obj = actor and actor:item_in_slot(slot)
    if obj then
        return obj:section() == section, obj  --// Karlan: объект может понадобится и далее, для получения типа или еще чего, чтобы никого не нагружать отдадим его прямо тут :)
    end
    return false
end
--// сравнить айди предмета в нужном слоте
function equal_slot_item_id(slot, id)
    local obj = actor and actor:item_in_slot(slot)
    if obj then
        return obj:id() == id, obj
    end
    return false
end
--// 
function equal_active_item_section(section)
    local obj = actor and actor:active_item()
    if obj then
        return obj:section() == section, obj  --// Karlan: объект может понадобится и далее, для получения типа или еще чего, чтобы никого не нагружать отдадим его прямо тут :)
    end
    return false
end
--//
function equal_active_item_condition(condition)
    local obj = actor and actor:active_item()
    if obj then
        return obj:condition() >= condition, obj  --// Karlan: объект может понадобится и далее, для получения типа или еще чего, чтобы никого не нагружать отдадим его прямо тут :)
    end
    return false
end

function equal_item_name(item, equal_name) --// Karlan: вот эта конечно уже и ввосе не для итемов может использоваться
    -- assert(is_userdata(item), [[bad argument #1 to 'equal_item_name' (userdata excepted, got %s)]], type(item))
    if not is_userdata(item) then return end --// Karlan: в перегруженных коллбеках сюда поневоле залетает то строка, то еще что-нибудь, так что ругатся нельзя!
    local name = item:name():gsub('\r\n','')
    if name == equal_name then
        return true --// объект у нас уже есть, поэтому его возвращать не будем
    end
    return false
end
local item_locked_slots = {[USE_SLOT]=true,[PDA_SLOT]=true,[OUTFIT_SLOT]=true,[HELMET_SLOT]=true,[BACKPACK_SLOT]=true}
function inv_is_item_accessible(item)
	if item:binded_object() and not m_container.container_is_empty(item:id()) then return false end
	local ii = item:get_inventory_item()
	return not (ii.item_place == global_flags.eItemPlaceSlot and item_locked_slots[ii.slot])
end
--//-----------------------------------------------------------------
--// Infoportions Shell 
--// Description: оболочки для работы с инфопоршнями
--//-----------------------------------------------------------------
--//---------------------------------------------
--// Проверка
--//---------------------------------------------
local function has_alife_info(info_id)
	return sim and sim:has_info(0, info_id)
end
--// универсальная проверка инфо
local function has_one_info(info_id)
--	assert(is_string(info_id), [[bad argument #1 to 'has_one_info' (string excepted, got %s)]], type(info_id))
	if actor then
		return actor:has_info(info_id)
	elseif sim then
		return sim:has_info(0, info_id)
	end
	return false
end
--//-------------------------
--// универсальная функция проверки инфопоршней
function has_info(info, mode)
	if is_string(info) then
		return has_one_info(info)
	else
		assert(mode == nil)
--		assert((is_table(info)), [[bad argument #1 to 'has_info' (string or table excepted, got '%s')]], type(info))
		return has_info_portions(info)
	end
end
--//-------------------------
--// проверка наличия всех инфо из массива
function has_info_portions(tbl_info)
    for _, info_id in ipairs(tbl_info) do
        if not has_one_info(info_id) then
            return false
        end
    end
    return true
end
--// проверка наличия любого инфо из массива
function has_any_info_portions(tbl_info)
    for _, info_id in ipairs(tbl_info) do
        if has_one_info(info_id) then
            return true
        end
    end
    return false
end
--// проверка всех инфо из массива на соотвествие заданным значениям (для динамических массивов)
function check_info_portions(tbl_info)
    for info_id, status in pairs(tbl_info) do
        if has_one_info(info_id) ~= status then
            return false
        end
    end
    return true
end
--//---------------------------------------------
--// Включение
--//---------------------------------------------
--// Karlan: assert в локальных функциях оставил для особо тяжких случаев, когда кодер находится в невменяемом состоянии и не может понять что к чему, делаем функцию глобальной и проблема становится более локализованной :)
--// включение инфопоршня
local function give_one_info(info_id)
	assert(is_string(info_id), [[bad argument #1 to 'give_one_info' (string excepted, got %s)]], type(info_id))
	if actor then
		if actor:dont_has_info(info_id) then
			actor:give_info_portion(info_id)
		end
	else
		warning("give_one_info")
	end
end
--//-------------------------
--// включение всех инфопоршней из массива
local function give_info_portions(tbl_info)
    assert(is_array(tbl_info), [[bad argument #1 to 'give_info_portions' (array excepted, got %s)]], type(tbl_info))
    for _, info_id in ipairs(tbl_info) do
        give_one_info(info_id)
    end
end
--//-------------------------
--// универсальная функция включения инфопоршней
function give_info(info)
    if is_string(info) then
        give_one_info(info)
    else
--		assert((is_array(info)), [[bad argument #1 to 'give_info' (string or array excepted, got '%s')]], type(info))
        give_info_portions(info)
    end
end
--//-------------------------
--// включение инфопоршня(-ей) по условию
function give_info_cond(cond, info_id)
    if cond then
        give_info(info_id)
    end
end
--//---------------------------------------------
--// Исключение
--//---------------------------------------------
--// исключение инфопоршня
function disable_one_info(info_id)
	if actor then
		if actor:has_info(info_id) then
			actor:disable_info_portion(info_id)
		end
	else
		warning("disable_one_info")
	end
end
--//-------------------------
--// исключение всех инфопоршней из массива
local function disable_info_portions(tbl_info)
	assert(is_array(tbl_info), [[bad argument #1 to 'disable_info_portions' (array excepted, got %s)]], type(tbl_info))
	for _, info_id in ipairs(tbl_info) do
		disable_one_info(info_id)
	end
end
--//-------------------------
--// универсальная функция исключения инфопоршней
function disable_info(info)
    if is_string(info) then
        disable_one_info(info)
    else
--		assert((is_array(info)), [[bad argument #1 to 'disable_info' (string or array excepted, got '%s')]], type(info))
        disable_info_portions(info)
    end
end
--//-------------------------
--// выключение инфопоршня(-ей) по условию
function disable_info_cond(cond, info_id)
    if cond then
        disable_info(info_id)
    end
end
--//-----------------------------------------------------------------
--// General functions
--// Description: различные общие функции
--//-----------------------------------------------------------------
--//---------------------------------------------
--// Read/Write CTime (Karlan->ALL: ОСТАВЛЕНО ДЛЯ СЕРВЕРА, !!!НЕ МЕНЯТЬ, НЕ УДАЛЯТЬ!!!)
--//---------------------------------------------
local CTime_0 = game.CTime()
--// запись CTime в пакет. если t=nil, то запишет один нулевой байт
--// стандартные функции для работы с CTime у нет-пакета используются только при явном наличии CTime, эти функции работают гораздо быстрее, так как при отсутсвии выделяют всего один байт
function w_CTime(p, t, discard_ms)
    if t == nil then
        p:w_u8(-1)
        return
    end
    if is_CTime(t) and not (t == CTime_0) then
        local Y, M, D, h, m, s, ms = t:get()
        p:w_u8 (Y - 2000)
        p:w_u8 (M)
        p:w_u8 (D)
        p:w_u8 (h)
        p:w_u8 (m)
        p:w_u8 (s)
        if not discard_ms then p:w_u16(ms) end
    else
        p:w_u8 (0)
    end
end
--//-------------------------
--// чтение CTime из пакета
function r_CTime(p, discard_ms)
    local Y = p:r_u8()
    if Y == 255 then
        return nil
    end
    if Y ~= 0 then
        local t = game.CTime()
        local M, D, h, m, s, ms = p:r_u8(), p:r_u8(), p:r_u8(), p:r_u8(), p:r_u8(), discard_ms and 0 or p:r_u16()
        t:set(Y + 2000,  M, D, h, m, s, ms)
        return t
    else
        return CTime_0
    end
end
--//---------------------------------------------
--// CTime common functions
--//---------------------------------------------
local days_in_year_cache = nil
function days_in_year(reset)
    if reset then
        days_in_year_cache = nil
    end
    if not days_in_year_cache then
        local y = game.get_game_time():get()
        if (y % 4) == 0 then
            days_in_year_cache = 366
        else
            days_in_year_cache = 365
        end
    end
    return days_in_year_cache
end

passed_time = {
    msec = 0.001,
    def_time = 1,
    minutes = 60,
    hours = 3600,
    days = 86400,
}

function get_passed_time(flag, ctime)
    flag = flag or passed_time.def_time
    return math.floor(game.get_game_time():diffSec(ctime or sim:start_game_time())/flag)
end
--//---------------------------------------------
--// Время суток
--//---------------------------------------------
function in_hours_interval(val1, val2)
    local game_hours = level.get_time_hours()
	if val1 < val2 then
		return game_hours >= val1 and game_hours < val2
	end
	return game_hours >= val1 or game_hours < val2
end
--//-------------------------
is_nominal_day      = function() return in_hours_interval(5, 22)  end --// возможно в стиле function(h1, h2) in_hours_interval(h1 or 5, h2 or 22) end
is_nominal_night    = function() return in_hours_interval(22, 5)  end --// not is_nominal_day()
--//---------------
is_morning          = function() return in_hours_interval(6, 12)  end
is_afternoon        = function() return in_hours_interval(12, 18) end
is_evening          = function() return in_hours_interval(18, 0)  end
is_night            = function() return in_hours_interval(0, 6)   end
--//---------------------------------------------
-- Проверка, загружен ли уровень и есть ли actor
--//---------------------------------------------
function is_level_loaded()
    return (level.present() and actor ~= nil)
end
--//---------------------------------------------
--// Происходит ли в данный момент смена уровня?
--//---------------------------------------------
--// нужно для того, чтобы объекты знали, какую информацию записывать при сохранении, а какую нет
function level_changing()
    local actor_gv = game_graph():vertex(sim:actor().m_game_vertex_id)
    return actor_gv:level_id() ~= sim:level_id()
end
--//---------------------------------------------
--// Создание перехода
--//---------------------------------------------
--[[
1. секция (как правило - "level_changer")
2. позиция спавна перехода "с уровня"
3. левел вертекс уровня (рекомендуется вычислять автоматически в скрипте)
4. гейм вертекс уровня (рекомендуется вычислять автоматически в скрипте)
5. имя уровня на который перемещаемся
6. позиция спавна актора на другом уровне
7. направление взгляда актора на другом уровне
8. левел вертекс уровня на который пермещаемся (рекомендуется вычислять автоматически в скрипте)
9. гейм вертекс уровня на который пермещаемся (рекомендуется вычислять автоматически в скрипте)
10. радиус левел-ченджера (по умолчанию - 3 метра)
11. story id (по умолчанию - инвалид, т.е. u32)
12. silent - надо ли спрашивать о перехое на уровень(по умолчанию - спрашиваем)
13. custom data - если хотим сделать пинок ГГ в сторону откуда он пришел, чтобы, например, не пускать его туда, куда не надо
14. тип метки
15. текст метки
16. надо ли сохранять метку?
]]
--// vector, u32, u16, string, vector, vector, u32, u16, u32, float, int (0/1), string, string, string, boolean
function create_level_changer(pos, lv, gv, dest_lvl, dest_pos, dest_dir, dest_lv, dest_gv, sid, radius, silent, if_reject, spot_type, spot_text, spot_ser, dist)
    local sobj = sim:create_level_changer("level_changer", pos, lv, gv, dest_lvl, dest_pos, dest_dir, dest_lv, dest_gv)
    local cd = ""
    if if_reject then --// Karlan: позволяет задать reject path, а также, если надо, дополнить кастом дату всем необходимым
        cd = cd .. "[pt_move_if_reject]\n path = "..if_reject
    end
    if dist then
        cd = cd .. "\n[travel]\n distance = "..dist
    end
    if string.len(cd) > 0 then
        sobj:set_custom_data(cd)
    end
    if sid or radius or silent then --// warning! silent is always 'true' on lua
        sim:assign_lc_settings(sobj.id, radius, silent)
    end
    if is_number(sid) then
        sobj:set_story_id(sid)
    end
    if spot_type then
        add_map_spot(sobj.id, spot_type, spot_text, spot_ser) --// если метка не подключается к менеджеру, то рекомендуется ее сохранить
    end
    return sobj
end
--// будем пробрасывать через еще одну оболочку, это удобнее и понятнее
function create_level_changer_by_wp(path, dest_path, sid, radius, silent, if_reject, spot_type, spot_text, spot_ser, dist)
    local pd, dpd = get_path_data(path), get_path_data(dest_path)
    local dest_dir = is_table(dpd[1]) and dpd[1].pos or vector.zero()
    if (pd[0].lv < 0) or (pd[0].lv >= INVALID_LEVEL_VERTEX_ID) then --// need test, теоретически должно спасти от инвалидных вейпоинтов, а не то придется сетку комплиить :)
        pd[0].lv = level.vertex_id(pd[0].pos)
    end
    return create_level_changer(pd[0].pos, pd[0].lv, pd[0].gv, dpd[0].ln, dpd[0].pos, dest_dir, dpd[0].lv, dpd[0].gv, sid, radius, silent, if_reject, spot_type, spot_text, spot_ser, dist)
end
--//---------------------------------------------
--// Создание зон
--//---------------------------------------------
local function __create_restrictor(section, pos, lv, gv, custom_data, radius_or_matrix, sr_type, name, spot_type, spot_text, spot_ser)
    local sobj = sim['create_space_restrictor'..(is_matrix(radius_or_matrix) and '2' or '')](sim, section, pos, lv, gv, radius_or_matrix, sr_type, name)
    if custom_data then
        sobj:set_custom_data(custom_data)
    end
    if spot_type then
        add_map_spot(sobj.id, spot_type, spot_text, spot_ser) --// если метка не подключается к менеджеру, то рекомендуется ее сохранить
    end
    return sobj
end
function create_restrictor(pos, lv, gv, custom_data, radius_or_matrix, sr_type, name, spot_type, spot_text, spot_ser)
    return __create_restrictor('space_restrictor', pos, lv, gv, custom_data, radius_or_matrix, sr_type, name, spot_type, spot_text, spot_ser)
end
function create_script_zone(pos, lv, gv, custom_data, radius_or_matrix, sr_type, name, spot_type, spot_text, spot_ser)
    return __create_restrictor('script_zone', pos, lv, gv, custom_data, radius_or_matrix, sr_type, name, spot_type, spot_text, spot_ser)
end
--//---------------------------------------------
--// Сохранение и загрузка в pstor
--//---------------------------------------------
local function is_pstor_corrected_type(val)
	local _type = type(val)
	if _type ~= "thread" and _type ~= "userdata" then
		return true
	end
	if is_CTime(val) or is_vector(val) or is_vector2(val) then
		return true
	end
	return false
end

function write_pstor(varname, val, obj)
	if obj and is_number(obj.id) and not db.storage[obj.id] then
		return m_storage.write_pstor(obj, varname, val)
	end
    local st = db.storage[(obj and (is_function(obj.id) and obj:id() or obj.id)) or 0]
    assert(is_table(st), [[write_pstor: incorrect type of field in db.storage, object:<%s> (table excepted, got %s)]], obj, type(st))
    local pstor = st.pstor
    if not pstor then
        pstor = {}
        st.pstor = pstor
    end
    assert(is_pstor_corrected_type(val), [[write_pstor: variable [%s] has incorrect type for writing in pstor storage, value got %s"]], varname, type(val))
    pstor[varname] = val
    return val
end
--// Karlan: далее, если db.storage[(obj and obj:id()) or 0] не таблица, то будет вылет, то есть если объект не под сторажем (некоторые объекты не юзают стораж), то надо быть аккуратнее и заранее создавать массив для объекта (чтобы предыдущая функция в нем могла создать пстор) для объекта, так как при нашем хранилище в целом пофигу что куда сохранять, а так даже будет лучше. Дописать проверки на таблицу конечно труда не составляет, но не хочу нагромождать функции, так как если появляется ошибка такого плана, то ищите проблемы в своем коде, а не в этой системе.
function read_pstor(varname, defval, obj)
	if obj and is_number(obj.id) and not db.storage[obj.id] then
		return m_storage.read_pstor(obj, varname, defval)
	end
	local pstor = db.storage[(obj and (is_function(obj.id) and obj:id() or obj.id)) or 0].pstor
	local value = pstor and pstor[varname]
	if value ~= nil then
		return value
	end
	return defval
end
function remove_pstor(varname, obj)
	local value
	if obj and is_number(obj.id) and not db.storage[obj.id] then
		value = m_storage.read_pstor(obj, varname, nil)
		m_storage.write_pstor(obj, varname, nil)
		return value
	end
	local pstor = db.storage[(obj and (is_function(obj.id) and obj:id() or obj.id)) or 0].pstor
	if pstor then
		value = pstor[varname]
		pstor[varname] = nil
	end
	return value
end

--//---------------------------------------------
--// Effectors functional
--//---------------------------------------------
function generate_pp_effector_id()
    local start_id = 100000
    while level.get_pp_effector(start_id) do
        start_id = start_id+1
    end
    return start_id
end
function generate_cam_effector_id()
    local start_id = 100000
    while level.get_cam_effector(start_id) do
        start_id = start_id+1
    end
    return start_id
end
--//---------------------------------------------
local saw_obj_id = nil
function set_saw_obj_id(id)
    saw_obj_id = id
end
function get_saw_obj()
    local id = saw_obj_id
    if not is_number(id) then return nil end
    local so = sim:object(id)
    if so then
        local o = get_online_object(id)
        if o then
            return o
        end
    else
        saw_obj_id = nil
    end
    return nil
end
--//---------------------------------------------
--// возвращает случайный из переданных аргументов
function random_choice(...)
    local args = {...}
    return args[math.random(#args)]
end
--//---------------------------------------------
--// Осуществляется рандомный взвешенный выбор.
function random_weighed(t,not_stable)
	local stable = not not_stable	-- stable поддерживает только эти типы значений: number & string
	local max_rnd = 0
	local rnd_t = {}
	for val,weight in pairs(t) do
		max_rnd = max_rnd + weight*10000
		if stable then
			table.insert(rnd_t,{val,weight})
		end
	end
	if max_rnd == 0 then
		return
	end
	local p = math.random(0,max_rnd)
	if stable then
		local crc = math_utils.crc32_string
		local function sort_f(a,b)
			local tostra,tostrb = tostring(a[1]),tostring(b[1])
			if tostra and tostrb then
				return crc(tostra) > crc(tostrb)
			end
			return false
		end
		table.sort(rnd_t,sort_f)
		for i,rt in ipairs(rnd_t) do
			p = p - rt[2]*10000
			if p <= 0 then
				return rt[1]
			end
		end
	else
		for val,weight in pairs(t) do
			p = p - weight*10000
			if p <= 0 then
				return val
			end
		end
	end
	abort("random_weighed %s",t)
end
--//---------------------------------------------
--// выполнить функцию при указанном randomseed
function randomseed_execute(seed,func,...)
	assert(is_number(seed) and is_function(func))
	local rnd = math.random(1000000)
	math.randomseed(seed)
	local res = {func(...)}
	math.randomseed(rnd)
	return unpack(res)
end

function get_option_value(opt, num) --// get_integer_option_value ??? --// console[method](param) ???
    local c = get_console()
    if c then
        if not num then num = 1 end
        return c:get_integer(opt) == num
    end
    return false
end

function ph_fix_by_bone(obj, bone)
    assert(is_userdata(obj), [[bad argument #1 to 'ph_fix_by_bone' (userdata excepted, got %s)]], type(obj))
    assert(is_string(bone), [[bad argument #2 to 'ph_fix_by_bone' (string excepted, got %s)]], type(bone))
    local ph_shell = obj:get_physics_shell()
    if ph_shell then
        local ph_element = ph_shell:get_element_by_bone_name(bone)
        if ph_element and not ph_element:is_fixed() then
            ph_element:fix()
        end
    end
end

function ph_release_fix_by_bone(obj, bone)
    assert(is_userdata(obj), [[bad argument #1 to 'ph_release_fix_by_bone' (userdata excepted, got %s)]], type(obj))
    assert(is_string(bone), [[bad argument #2 to 'ph_release_fix_by_bone' (string excepted, got %s)]], type(bone))
    local ph_shell = obj:get_physics_shell()
    if ph_shell then
        local ph_element = ph_shell:get_element_by_bone_name(bone)
        if ph_element and ph_element:is_fixed() then
            ph_element:release_fixed()
        end
    end
end
