
--[[ 	General Scripting TIPS

	1. 	dealing with objects and userdata:
		NEVER keep userdata (ie. engine classes) in global scope, instead keep track of game object id, and grab object in the scope you need it in by using level.object_by_id or db.storage table. 
		Otherwise object can't deconstruct and you will end up with instances were script binder exists but object doesn't or level object exists but server object doesn't. This is because variables in lua are all references.
			Ex. _G.npc = level.object_by_id(2342) 
				alife():release(alife_object(2342))
		Above, 'npc' will not be nil but se_obj will be! scripts will blow up. Pure Virtual Function calls may occur! Or quite possibly silent errors like for example when player goes to talk to npc game crashes because self.object is nil
		Such thing is 'okay' in db.storage, but that is because on net_destroy db.storage[id] is set nil. So if you are keeping track of objects directly in a table, instead of ID 
		then you absolutely must rid of every single reference to the userdata so that it can be destroy and garbage collected. Otherwise you end up with undetectable issues.

	2.	naming variables:
		There are global namespace occupied for engine calls: task, level, weather
		Never use them as variables and such, you might break the game in no time by mistake
		You can check a full list of namespace in lua_help.script at the end
		
		Be aware with variables names inside scripts as well, don't use names found inside _G script if possible. And don't use names identical to scripts names
		_G is a core script where everything inside can be called directly from other scripts, without specifying the source
		Ex: calling IsStalker in some script will call the function found here directly, even if you didn't specify the script like this: _G.IsStalker
--]]

GAME_VERSION = "1.5.3"

if string.find(command_line(), "-dbg") then
	DEV_DEBUG = true
end
if string.find(command_line(), "-dbgdev") then -- because users all using dbg, need to hide OP features
	DEV_DEBUG_DEV = true
end

USE_INI_MEMOIZE = true

-- Improves performance if true, by disabling update methods for squads and smart terrains on levels not linked in configs\ai_tweaks\simulation_objects.ltx
-- But this means simulation will only be active on actor level and linked levels everything else will remain in an idle state

DEACTIVATE_SIM_ON_NON_LINKED_LEVELS = false

-------------------------------------------------------------------------------------------------
-- Use marshal library for saving persistent data (like xr_logic pstor)
-- marshal library can encode tables, functions, strings and numbers to easily allow persistent data storage to file
-- This is used for db.storage[id].pstor, surge_manager, mines, and treasure_manager.script if enabled
-- See alife_storage_manager.script for implementation
require("lua_extensions")
marshal = require "marshal"
USE_MARSHAL = marshal ~= nil

----------------------------------------------------------------------
-- Global use
ini_sys                  	= system_ini() 	-- global use for cached system_ini()
AC_ID                    	= 0				-- Used for getting actor id
-- AC_LVL_ID					= nil			-- Used for getting actor's level id, set by simulation_objects ------- replaced with function because crashes
mus_vol                  	= 0             -- Used for temp sound muting
amb_vol                  	= 0             -- Used for temp sound muting
timer_transparent        	= 0 			--
KEYS_UNLOCK					= true			-- Used for scripted GUI to disable action keybinds while they are active
----------------------------------------------------------------------
-- Utilities
local string_find   = string.find
local string_match  = string.match
local string_gmatch = string.gmatch
local string_sub    = string.sub
local string_gsub   = string.gsub
local string_format = string.format

local math_cos      = math.cos
local math_sin      = math.sin
local math_acos     = math.acos
local math_sqrt     = math.sqrt
local math_floor    = math.floor
local math_ceil     = math.ceil
----------------------------------------------------------------------

function start_game_callback()
	math.randomseed(device():time_global())
	printf("S.T.A.L.K.E.R. Anomaly version %s",GAME_VERSION)
	--printf("OpenXRay x64, Call of Chernobyl build %s",GAME_VERSION)
	if (USE_MARSHAL) then
		printf("using marshal library")
	end
		
	-- Alundaio
	if (axr_main) then axr_main.on_game_start() end
	-- End Alundaio

	--SIMBOARD = nil
	sim_board.get_sim_board()

	--smart_names.init_smart_names_table()
	task_manager.clear_task_manager()
	--xr_sound.start_game_callback()
	dialog_manager.fill_phrase_table()

	--sim_objects.clear()
	sr_light.clean_up ()
	pda.add_quick_slot_items_on_game_start()
end



-------------------------------------------------------------------------------------------------------
-- 											SCRIPTED CALLBACKS
-------------------------------------------------------------------------------------------------------
function RegisterScriptCallback(name,func_or_userdata)
	axr_main.callback_set(name,func_or_userdata)
end

function UnregisterScriptCallback(name,func_or_userdata)
	axr_main.callback_unset(name,func_or_userdata)
end

function SendScriptCallback(name,...)
-- Call this from a script to create a new callback to functions that register for it with RegisterScriptCallback
-- Every time this function is executed it will callback to all registered members
-- If axr_main.script has a function by this name, it will automatically trigger it!

	--utils_data.debug_write(strformat("BEFORE SendScriptCallback %s",name))
	-- callback to all registered functions
	axr_main.make_callback(name,...)
		--utils_data.debug_write(strformat("AFTER SendScriptCallback %s",name))
		-- check if axr_main has it's own function to execute
	if (axr_main[name]) then
		axr_main[name](...)
	end
end

function AddScriptCallback(name)
	axr_main.callback_add(name)
end


-------------------------------------------------------------------------------------------------------
-- 												GUI
-------------------------------------------------------------------------------------------------------
_HUD_STATE	= true
_GUIs = {}
_GUIsInstances = {}
_GUIs_keyfree = {} -- specify GUIs that allow global keybinds
function Register_UI(name, path, instance)
	if (not name) then
		return
	end
	
	printdbg("* Register UI: %s", name)
	_GUIs[name] = { path=path , instance=instance }
	_GUIsInstances[name] = { path=path , instance=instance }
	if (not _GUIs_keyfree[name]) then
		KEYS_UNLOCK = false
	end
	
	SendScriptCallback("GUI_on_show", name, path)
end

function Unregister_UI(name)
	if not (name and _GUIs[name]) then
		return
	end
	local path = _GUIs[name].path
	
	printdbg("* Unregister UI: %s", name)
	_GUIs[name] = nil
	if is_empty(_GUIs) then
		KEYS_UNLOCK = true
	else
		KEYS_UNLOCK = true
		for n,p in pairs(_GUIs) do
			if (not _GUIs_keyfree[n]) then
				KEYS_UNLOCK = false
			end
		end
	end
	
	SendScriptCallback("GUI_on_hide", name, path)
end

function DestroyAll_UI()
	hide_hud_all()
	
	for n,p in pairs(_GUIsInstances) do
		local path = p.path
		local ui = p.instance

		if path and _G[path] then
			if ui and _G[path][ui] then
				_G[path][ui] = nil
			elseif _G[path].GUI then
				_G[path].GUI = nil
			end
		end
		_GUIsInstances[n] = nil
	end
end

function Check_UI(name)
	if name then
		return _GUIs[name] and true or false
	end
	return is_not_empty(_GUIs)
end

function Overlapped_UI(name)
	if _GUIs["Dialog"] then -- it's ok for dialog to be opened below the inventory
		return (size_table(_GUIs) > 2)
	end
	return (size_table(_GUIs) > 1)
end

function CloseAll_UI()
	for name,t in pairs(_GUIs) do
		local path = t.path
		local ui = t.instance
		if path and _G[path] then
			if ui and _G[path][ui] and _G[path][ui].Close then
				_G[path][ui]:Close()
			elseif _G[path].GUI and _G[path].GUI.Close then
				_G[path].GUI:Close()
			end
		end
	end
end

-----
function GetActorMenu()
	return ActorMenu.get_actor_menu()
end

function hide_hud_all()
	for name,t in pairs(_GUIs) do
		local path = t.path
		local ui = t.instance
		if path and _G[path] then
			if ui and _G[path][ui] and _G[path][ui].Close then
				_G[path][ui]:Close()
			elseif _G[path].GUI and _G[path].GUI.Close then
				_G[path].GUI:Close()
			end
		end
	end
	
	hide_hud_inventory()
	hide_hud_pda()
end

function hide_hud_inventory()
	local hud = get_hud()
	if (hud) then
		hud:HideActorMenu()
		return true
	end
	return false
end

function hide_hud_pda()
	if db.actor and db.actor:active_slot() == 8 then
		db.actor:activate_slot(0)
		return true
	end
	return false
	
	--[[
	local hud = get_hud()
	if (hud) then
		hud:HidePdaMenu()
		return true
	end
	return false
	--]]
end

function show_all_ui(show)
	local hud = get_hud()
	if not (hud) then
		return
	end
	if(show) then
		show_indicators(true)
--	    db.actor:restore_weapon()
		db.actor:disable_hit_marks(false)
		hud:show_messages()
	else
		if db.actor:is_talking() then
			db.actor:stop_talk()
		end
		level.hide_indicators_safe()
		hide_hud_all()
		hud:hide_messages()
--	    db.actor:hide_weapon()
		db.actor:disable_hit_marks(true)
	end
end

function show_indicators(state)
	if state then
		level.show_indicators()
	else
		level.hide_indicators_safe()
	end
	_HUD_STATE = state
end

function main_hud_shown()
	if _HUD_STATE                       -- indicators enabled
	and (actor_menu.last_mode == 0)     -- inventory is off
	and (pda.dialog_closed == true)     -- dialog is off
	and (not Check_UI())                -- no active GUI
	and get_console_cmd(1,"hud_draw")   -- command hud is on
	and (not axr_main.binoc_is_zoomed) -- no zoomed weapon
	and (not axr_main.scoped_weapon_is_zoomed) -- no zoomed weapon
	then
		return true
	end
	return false
end


--------------------------------------------------------------------------------------------------
-- 											DELAYED EVENT QUEUE
-------------------------------------------------------------------------------------------------------
--[[
-- Events must have a unique id. Such as object id or another identifier unique to the occasion.
-- Action id must be unique to the specific Event. This allows a single event to have many queued
-- actions waiting to happen.
--
-- Returning true will remove the queued action. Returning false will execute the action continuously.
-- This allows for events to wait for a specific occurrence, such as triggering after a certain amount of
-- time only when object is offline
--
-- param 1 - Event ID as type<any>
-- param 2 - Action ID as type<any>
-- param 3 - Timer in seconds as type<number>
-- param 4 - Function to execute as type<function>
-- extra params are passed to executing function as table as param 1

-- see on_game_load or state_mgr_animation.script for example uses
-- This does not persists through saves! So only use for non-important things.
-- For example, do not try to destroy npcs unless you do not care that it can fail before player saved then loaded.
--]]
local ev_queue = {}
function CreateTimeEvent(ev_id,act_id,timer,f,...)
	if not (ev_queue[ev_id]) then
		ev_queue[ev_id] = {}
		ev_queue[ev_id].__size = 0
	end

	if not (ev_queue[ev_id][act_id]) then
		ev_queue[ev_id][act_id] = {}
		ev_queue[ev_id][act_id].timer = time_global() + timer*1000
		ev_queue[ev_id][act_id].f = f
		ev_queue[ev_id][act_id].p = {...}
		ev_queue[ev_id].__size = ev_queue[ev_id].__size + 1
	end
end
function RemoveTimeEvent(ev_id,act_id)
	if (ev_queue[ev_id] and ev_queue[ev_id][act_id]) then
		ev_queue[ev_id][act_id] = nil
		ev_queue[ev_id].__size = ev_queue[ev_id].__size - 1
	end
end
function ResetTimeEvent(ev_id,act_id,timer)
	if (ev_queue[ev_id] and ev_queue[ev_id][act_id]) then
		ev_queue[ev_id][act_id].timer = time_global() + timer*1000
	end
end
function ProcessEventQueue(force)
	if has_alife_info("sleep_active") then
		return false
	end
	
	for event_id,actions in pairs(ev_queue) do
		for action_id,act in pairs(actions) do
			if (action_id ~= "__size") then
				if (force) or (time_global() >= act.timer) then
					-- utils_data.debug_write(strformat("event_queue: event_id=%s action_id=%s",event_id,action_id))
					if (act.f(unpack(act.p)) == true) then
						ev_queue[event_id][action_id] = nil
						ev_queue[event_id].__size = ev_queue[event_id].__size - 1
					end
				end
			end
		end

		if (ev_queue[event_id].__size == 0) then
			ev_queue[event_id] = nil
		end
	end
	
	return false
end
function ProcessEventQueueState(m_data,save)
	if (save) then
		m_data.event_queue = ev_queue
	else
		ev_queue = m_data.event_queue or ev_queue
	end
end



-------------------------------------------------------------------------------------------------------
-- 											LEVEL HANDLERS
-------------------------------------------------------------------------------------------------------
local function timer_travel(pos,lvid,gvid,angle)
	change_level_now(pos,lvid,gvid,angle)
	return true
end
function ChangeLevel(pos,lvid,gvid,angle,anim)
-- IMPORTANT: You must realize that when you send this event it will happen immediately
-- if done in lua code it will not execute the rest of the block, level changes immediately happen!
--[[
		NET_Packet	p;
		p.w_begin	(M_CHANGE_LEVEL); -- M_CHANGE_LEVEL == 13
		p.w			(&m_game_vertex_id,sizeof(m_game_vertex_id));
		p.w			(&m_level_vertex_id,sizeof(m_level_vertex_id));
		p.w_vec3	(m_position);
		p.w_vec3	(m_angles);
		Level().Send(p,net_flags(TRUE));
--]]

	-- Hide PDA to avoid issue of loading in another map with PDA on, it might block interaction with travel UI
	hide_hud_all()
	
	-- Stop talking
		if db.actor:is_talking() then
			db.actor:stop_talk()
		end
		
	-- requires OpenXRay
	if anim then
		SendScriptCallback("on_before_level_changing")

		level.add_pp_effector("sleep_fade.ppe", 1313, false)
		
		CreateTimeEvent(0,"delay_travel",3,timer_travel,pos,lvid,gvid,angle) -- Tronex
	else
		change_level_now(pos,lvid,gvid,angle)
	end
end
function change_level_now(pos,lvid,gvid,angle)
	local p = net_packet()
	p:w_begin(13)
	p:w_u16(gvid)
	p:w_u32(lvid)
	p:w_vec3(pos)
	p:w_vec3(angle)
	level.send(p,true)
end

-- Wrapper for level.add_call 
-- For some reason in 1.6 engine level.remove_call does not work! If you know why, please contact me @alundaio
local level_add_call_unique = {}
function AddUniqueCall(functor_a)

	if (level_add_call_unique[functor_a]) then 
		return 
	end 
	
	local function wrapper()
		if not (level_add_call_unique[functor_a]) then 
			return true 
		end
		
		if (functor_a()) then
			level_add_call_unique[functor_a] = nil
			return true
		end
		
		return false
	end
	
	level_add_call_unique[functor_a] = true
	
	level.add_call(wrapper,function() end)
end 

function RemoveUniqueCall(functor_a)
	level_add_call_unique[functor_a] = nil
end

function JumpToLevel(new_level)
	-- requires OpenXray
	local level_name = level.name()
	if (level_name == new_level) then
		return false
	end
	
	local cvertex
	local sim,gg = alife(),game_graph()
	-- first try to find a smart_terrain on specified level
	for id,smart in pairs(db.smart_terrain_by_id) do
		cvertex = smart and gg:vertex(smart.m_game_vertex_id)
		if (cvertex and sim:level_name(cvertex:level_id()) == new_level) then
			ChangeLevel(cvertex:level_point(),cvertex:level_vertex_id(),smart.m_game_vertex_id,VEC_ZERO,true)
			return true
		end
	end

	-- in case level has no smarts then just teleport to first found gvid for level
	for gvid=0, 4860 do
		if gg:valid_vertex_id(gvid) then
			cvertex = gg:vertex(gvid)
			lvl = sim:level_name(cvertex:level_id())
			if (lvl == new_level) then
				ChangeLevel(cvertex:level_point(),cvertex:level_vertex_id(),gvid,VEC_ZERO,true)
				return true
			end
		else
			break
		end
	end
	return false
end

function TeleportObject(id,pos,lvid,gvid)
	-- Requires OpenXray
	if (db.offline_objects[id]) then
		db.offline_objects[id].level_vertex_id = nil
	end
	db.spawned_vertex_by_id[id] = nil
	alife():teleport_object(id,gvid,lvid,pos)
end

function TeleportSquad(squad,pos,lvid,gvid)
	-- Requires OpenXray
	local sim = alife()
	sim:teleport_object(squad.id,gvid,lvid,pos)
	for k in squad:squad_members() do
		if (db.offline_objects[k.id]) then
			db.offline_objects[k.id].level_vertex_id = nil
		end
		db.spawned_vertex_by_id[k.id] = nil
		sim:teleport_object(k.id,gvid,lvid,pos)
	end
end

function in_time_interval(val1, val2)
--   .
	local game_hours = level.get_time_hours()
	if val1 >= val2 then
		return game_hours < val2 or game_hours >= val1
	else
		return game_hours < val2 and game_hours >= val1
	end
end

function level_changing()
--       ?
--   ,   ,     ,   
	local sim = alife()

	if (not sim) then
		return false
	end

	local actor_gv = game_graph():vertex( sim:actor().m_game_vertex_id )
	return actor_gv:level_id() ~= sim:level_id()
end



--------------------------------------------------------------------
-- Serialization of userdata for Marshal Library
--------------------------------------------------------------------
if (marshal) then
	function game_CTime___persist(self)
		local Y, M, D, h, m, s, ms = 0,0,0,0,0,0,0
		if (self and self.get) then
			Y, M, D, h, m, s, ms = self:get(Y, M, D, h, m, s, ms)
		end
		return function ()
			local t = game.CTime()
			t:set(Y, M, D, h, m, s, ms)
			return t
		end
	end
	getmetatable(game.CTime()).__persist = game_CTime___persist
end

-- debug to find objects that shouldn't be calling game_object:alive()
--[[
game_object.alive = function(self)
	callstack()
	printf("alive %s",self:name())
	local se_obj = alife_object(self:id())
	return se_obj:alive()
end
--]]
--------------------------------------------------------------------

-- Used by modules.script for generic module management
schemes = {}
schemes_by_stype = {}
function LoadScheme(filename, scheme, ...)
	if not (_G[filename]) then
		printe("!ERROR: Trying to load scheme that does not exist! %s",filename)
		return
	end
	schemes[scheme] = filename
		local p = {...}
	for i=1,#p do
		if not (schemes_by_stype[p[i]]) then
			schemes_by_stype[p[i]] = {}
		end
		schemes_by_stype[p[i]][scheme] = true
	end
end



-------------------------------------------------------------------------------------------------------
-- 										 CONSOLE LOGGERS
-------------------------------------------------------------------------------------------------------
function printf(fmt,...) -- formatted logging
	if not (fmt) then return end
	local fmt = tostring(fmt)

	if (select('#',...) >= 1) then
		local i = 0
		local p = {...}
		local function sr(a)
			i = i + 1
			if (type(p[i]) == 'userdata') then
				if (p[i].x and p[i].y) then
					return vec_to_str(p[i])
				end
				return 'userdata'
			end
			return tostring(p[i])
		end
		fmt = string_gsub(fmt,"%%s",sr)
	end
	if (log) then
		log(fmt)
		--exec_console_cmd("flush")
	else
		exec_console_cmd("load ~#debug msg:"..fmt)
	end	
end

function printe(fmt,...) -- error logging
	if DEV_DEBUG and db.actor and (not device():is_paused()) and ui_options.get("other/debug_error") then 
		local hud = get_hud()
		if (hud) then
			local err = hud:GetCustomStatic("debug_error")
			if (err == nil) then
				hud:AddCustomStatic("debug_error", true)
			end
		end
	end
	printf(fmt,...)
end

function printdbg(fmt,...) -- debug logging
	if DEV_DEBUG then printf(fmt,...) end
end

function abort(msg, ...)
	if not (msg) then return end
	local fmt = tostring(msg)

	if (select('#',...) >= 1) then
		local i = 0
		local p = {...}
		local function sr(a)
			i = i + 1
			if (type(p[i]) == 'userdata') then
				return 'userdata'
			end
			return tostring(p[i])
		end
		fmt = string_gsub(fmt,"%%s",sr)
	end
	
	callstack()
	log(fmt)
	--[[
	error(fmt, 2)
	--]]
end

function callstack(c1, to_str) -- stack trace logging
-- Tronex: modified this for less shit to read
-- c1: only print caller function, useful to trace calls happening for specific function
-- to_str: return stack trace as string if we want to use it in custom prints 

	if (log and debug and type(debug.traceback) == 'function') then
		--log( debug.traceback('\n', 2) )
		
		-- Path is not needed
		local str = string_gsub( debug.traceback('', 2) , "%.%.%.(.-)\\scripts\\" , "... " )
		
		-- Line text
		for ss in string_gmatch(str,":(%d*):") do
			local num = string_gsub( ss , ":","")
			str = string_gsub( str , ":" .. num .. ":" , " (line: " .. num .. ")" )
		end
		str = string_gsub( str , "^(.-):" , "" )

		-- In case we want first caller only
		if c1 then
			local cnt = 0
			for ss in string_gmatch(str,"'(.-)'") do
				cnt = cnt + 1
				local f = string_gsub( ss , "'","")
				str = string_gsub( str , "'" .. f .. "'" , "'" .. f .. "'[" .. cnt .. "]" ) -- mark lines with numbers
			end
			str = string_gsub( str , "%[2%](.*)$" , "") -- remove everything after second line
			str = string_gsub( str , "^(.*)%.%.%." , "") -- remove crap at beginning
		end
		
		-- in case we want a string back
		if to_str then
			return str
			
		elseif c1 then
			log( "STACK TRACEBACK: " .. str )
		else
			-- Framing
			log( "~ ------------------------------------------------------------------------" )
			log( "~ STACK TRACEBACK:" )
			log( str )
			log( "~ ------------------------------------------------------------------------" )
		end
	end
end

function get_console_cmd(typ, name)
	if (type(name) ~= "string") then
		callstack()
		printe("!ERROR exec_console_cmd | missing command")
		return
	end
	
	if typ == 0 then
		return get_console():get_string(name)
	elseif typ == 1 then
		return get_console():get_bool(name)
	elseif typ == 2 then
		return get_console():get_float(name)
	end
	return get_console():get_token(name)
end

function exec_console_cmd(name)
	if (type(name) ~= "string") then
		callstack()
		printe("!ERROR exec_console_cmd | missing command")
		return
	end
	
	get_console():execute(name)
	SendScriptCallback("on_console_execute", unpack(str_explode(name," ")))
end


-------------------------------------------------------------------------------------------------------
-- 											MATHEMATICAL
-------------------------------------------------------------------------------------------------------
-- ,    ,      
time_infinite = 100000000

function time_global()
	return device():time_global()
end

function time_continual()
	return device():time_continual()
end

function round(value)
	return math.floor(value+0.5)
end

function round_idp(num, idp)
  local mult = 10^(idp or 0)
  return math_floor(num * mult + 0.5) / mult
end

function round_100(num)
  return math_floor(num * 100 + 0.5) / 100
end

function odd(x)
	return not ( ( math_floor( x * 0.5 ) * 2 ) == math_floor( x ) )
end

function clamp(val, min, max)
	return (val < min and min) or (val > max and max) or val
end

function normalize(val, min, max)
	local d = (val-min)/(max-min)
	return d < 0 and 0 or d
end

function normalize_100(val, min, max)
	return (normalize(val, min, max) * 100)
end

function random_choice(...)
	local arg = {...}
	if (#arg > 0) then
		local r = math.random(1, #arg)
		return arg[r]
	end
end

function random_number(min_value, max_value)
	if min_value == nil and max_value == nil then
		return math.random ()
	else
		return math.random (min_value, max_value)
	end
end

function random_float(min_value, max_value)
    return min_value + math.random() * (max_value - min_value)
end

--Tv yaw  
function yaw( 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
function yaw_degree( 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 ) ) ) * 57.2957)
end
function yaw_degree3d( v1, v2 )
	return  (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)))*57.2957)
end
function vector_cross(v1, v2)
	return vector():set(v1.y  * v2.z  - v1.z  * v2.y, v1.z  * v2.x  - v1.x  * v2.z, v1.x  * v2.y  - v1.y  * v2.x)
end
function vec_to_str (vector) --    .
	if vector == nil then return "nil" end
	return string_format("[%s:%s:%s]", vector.x, vector.y, vector.z)
end

--    y   .
function vector_rotate_y(v, angle)
	angle = angle * 0.017453292519943295769236907684886
	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

function distance_2d( a, b )
	return math.sqrt( (b.x-a.x)^2 + (b.z-a.z)^2 )
end

function distance_2d_sqr(a,b)
	return (b.x-a.x)^2 + (b.z-a.z)^2
end


-------------------------------------------------------------------------------------------------------
-- 											STRINGS
-------------------------------------------------------------------------------------------------------
function string.gsplit(s, sep, plain)
	local start = 1
	local done = false
	local function pass(i, j, ...)
		if i then
			local seg = s:sub(start, i - 1)
			start = j + 1
			return seg, ...
		else
			done = true
			return s:sub(start)
		end
	end
	return function()
		if done then return end
		if sep == '' then done = true return s end
		return pass(s:find(sep, start, plain))
	end
end

function trim(s)
	return (string_gsub(s, "^%s*(.-)%s*$", "%1"))
end

function strformat(text,...)
	if not (text) then return end
	local i = 0
	local p = {...}
	local function sr(a)
		i = i + 1
		if (type(p[1]) == "userdata") then
			return "userdata"
		end
		return tostring(p[i])
	end
	-- so that it doesn't return gsub's multiple returns
	local s = string_gsub(text,"%%s",sr)
	return s
end

function str_explode(str, sep, plain)
	if not (str and sep) then
		printe("!ERROR str_explode | missing parameter str = %s, sep = %s",str,sep)
		callstack()
	end
	if not (sep ~= "" and string_find(str,sep,1,plain)) then
		return { str }
	end
	local t = {}
	local size = 0
	for s in str:gsplit(sep,plain) do 
		size = size + 1
		t[size] = trim(s)
	end
	return t
end

function parse_list(ini, key, val, convert)
	if not (ini and key and val) then 
		error(strformat("parse_list ini is nil for key=%s val=%s",key,val))
	end
	local str = ini:r_string_ex(key,val)
	local t = str and str ~= "" and str_explode(str,",") or {}
	if (convert and #t > 0) then
		local l = {}
		for i=1,#t do
			l[t[i]] = true
		end
		return l
	end
	return t
end

function parse_names( s )
	local t = {}
	local size_t = 0
	for name in string_gmatch( s, "([%w_%-.\\]+)%p*" ) do
		size_t = size_t + 1
		t[size_t] = name
	end
	return t
end

function parse_key_value( s )
	local t = {}
	if s == nil then
		return nil
	end
	local key, nam = nil, nil
	for name in string_gmatch( s, "([%w_\\]+)%p*" ) do
		if key == nil then
			key = name
		else
			t[key] = name
			key = nil
		end
	end
	return t
end

function parse_nums( s )
	local t = {}
	for entry in string_gmatch( s, "([%-%d%.]+)%,*" ) do
		t[#t+1] = tonumber(entry)
	end
	return t
end

function parse_func(sec, param, ...)
	local func = ini_sys:r_string_ex(sec, param)
	func = str_explode(func,"%.")
	return func and _G[func[1]] and _G[func[1]][func[2]] and _G[func[1]][func[2]](...) or nil
end

function starts_with(str, start_txt)
   return str:sub(1, #start_txt) == start_txt
end

function has_translation(string_id)
	return (game.translate_string(string_id) ~= string_id)
end

function get_param_string(src_string, obj) --      
	--printf("src_string is [%s] obj name is [%s]", tostring(src_string), obj:name())
	local script_ids = db.script_ids[obj:id()]
	local out_string, num = string_gsub(src_string, "%$script_id%$", tostring(script_ids))
	if num > 0 then
		return out_string , true
	else
		return src_string , false
	end
end



-------------------------------------------------------------------------------------------------------
-- 											ACTIONS
-------------------------------------------------------------------------------------------------------
function execute_func(file, func, ...)
	if file and func and _G[file] and _G[file][func] then
		return _G[file][func](...)
	end
end

function reset_action (npc, script_name)
	if npc:get_script () then
		 npc:script (false, script_name)
	end
	npc:script (true, script_name)
end

function stop_play_sound(obj)
	if (IsStalker(obj) and not obj:alive()) then
		return
	end
	obj:set_sound_mask(-1)
	obj:set_sound_mask(0)
end

--[[ does not work?
function wait_game(time_to_wait)
	verify_if_thread_is_running()
	if (time_to_wait == nil) then
		coroutine.yield()
	else
		local time_to_stop = game.time() + time_to_wait
		while game.time() <= time_to_stop do
			coroutine.yield()
		end
	end
end

function wait(time_to_wait)
	verify_if_thread_is_running()
	if (time_to_wait == nil) then
		coroutine.yield()
	else
		local time_to_stop = time_global() + time_to_wait
		while time_global() <= time_to_stop do
			coroutine.yield()
		end
	end
end
--]]

function action(obj,...)
	local arg = {...}
	local e_act = entity_action()
	for i=1,#arg do 
		e_act:set_action(arg[i])
	end 
	if (obj ~= nil) then
		obj:command(e_act,false)
	end
	return entity_action(e_act)
end

function action_first(obj,...)
	local arg = {...}
	local e_act = entity_action()
	for i=1,#arg do 
		e_act:set_action(arg[i])
	end 
	if (obj ~= nil) then
		obj:command(e_act,true)
	end
	return entity_action(e_act)
end

-- +  v  v - , v    v 
function interrupt_action(who, script_name)
	if who:get_script() then
		who:script(false, script_name)
	end
end

function get_clsid(obj)
	if not (obj) then
		callstack()
		printe("!ERROR: get_clsid - obj is nil!")
		return
	end
	if not (obj.clsid) then
		callstack()
		printe("!ERROR: no clsid method for %s",obj:name())
		return
	end
	return obj:clsid()
end

--' L
function on_actor_critical_power()

end

function on_actor_critical_max_power()
end

--' 
function on_actor_bleeding()

end

function on_actor_satiety()
end

--' 
function on_actor_radiation()

end

--'  
function on_actor_weapon_jammed()

end

--'     
function on_actor_cant_walk_weight()

end

--'  
function on_actor_psy()
end

local save_marker_result = {}
--      
function set_save_marker(p, mode, check, prefix)
	prefix = tostring(prefix)
		if (check ~= true) then
		if mode == "save" then
			save_marker_result[prefix] = p:w_tell() or 0
			if p:w_tell() > 16000 then
				abort("ERROR: You are saving too much")
			end
		else
			save_marker_result[prefix] = p:r_tell() or 0
		end
		return
	end
		if not (save_marker_result[prefix]) then
		abort("ERROR set_save_marker:%s: Trying to check without marker mode=%s",prefix,mode)
		if (mode == "save") then
			p:w_u16(0)
		elseif (mode == "load") then
			p:r_u16()
		end
		return
	end
		if mode == "save" then
		local dif = p:w_tell() - save_marker_result[prefix]
		if dif >= 8000 then
			printf("!ERROR set_save_marker:%s: WARNING! may be this is problem save point dif=%s",prefix,dif)
		end
		p:w_u16(dif)
	else
		local c_dif = p:r_tell() - save_marker_result[prefix]
		local dif = p:r_u16()
		if dif ~= c_dif then
			printf("!ERROR set_save_marker:%s: INCORRECT LOAD dif=%s c_dif=%s", prefix, dif, c_dif)
		end
	end
		save_marker_result[prefix] = nil
end



------------------------------------------------------------------------------------------------------
-- 									ENGINE EXPORTS!!! (Player utilities)
------------------------------------------------------------------------------------------------------
-- added: flags, CInventory__eat, CActor__BeforeHitCallback, 
local flags = { ret_value = true }

-- called when an inventory item is eaten/used
-- returning false will prevent the item from being used
function CInventory__eat(item)
	flags.ret_value = true
	SendScriptCallback("actor_on_item_before_use",item,flags)
	return flags.ret_value
end

-- called after actor hit callbacks, can be used to replace engine's artefact resistance calculation
-- How to override hit_power:
-- hit_table["override"] = true
-- hit_table["hit_power"] = {new damage value}
function CActor__HitArtefactsOnBelt(hit_table, hit_power, hit_type)
	SendScriptCallback("actor_on_before_hit_belt",hit_table, hit_power, hit_type)
	return hit_table
end

-- Called on CHudItem Motion Mark
function CHudItem__OnMotionMark(state, mark)
	SendScriptCallback("actor_on_hud_animation_mark",state,mark)
end

function CHudItem__PlayHUDMotion(anm_table, obj)
	SendScriptCallback("actor_on_hud_animation_play",anm_table,obj)
	return anm_table
end

function player_hud__OnMovementChanged(cmd)
	SendScriptCallback("actor_on_movement_changed",cmd)
end

-- Called before actor hit callback
-- returning false will ignore the hit completely
function CActor__BeforeHitCallback(actor,shit,bone_id)
	--printf("power=%s impuse=%s type=%s dir=%s who=%s",shit.power,shit.impulse,shit.type,shit.direction and vec_to_str(shit.direction),shit.draftsman and shit.draftsman:name())
	if (shit.type ~= hit.strike) then
		if (bind_stalker_ext.invulnerable_time and time_global() < bind_stalker_ext.invulnerable_time) then 
			bind_stalker_ext.invulnerable_time = bind_stalker_ext.invulnerable_time - 500
			return false
		end
	end
	
	if (shit.power > 0) then 
		if (shit.draftsman and shit.draftsman:id() ~= 0 and IsStalker(shit.draftsman) and shit.draftsman:relation(db.actor) == game_object.friend) then 
			return false 
		end
	end
	
	flags.ret_value = true
	SendScriptCallback("actor_on_before_hit",shit,bone_id,flags)
	return flags.ret_value
end

-- called in ai_stalker_fire.cpp CAI_Stalker::Hit()
-- returning false will ignore the hit completely
function CAI_Stalker__BeforeHitCallback(npc,shit,bone_id)
	flags.ret_value = true
	SendScriptCallback("npc_on_before_hit",npc,shit,bone_id,flags)
	return flags.ret_value
end

-- called in base_monster.cpp CBaseMonster::Hit()
-- returning false will ignore the hit completely
function CBaseMonster__BeforeHitCallback(monster,shit,bone_id)
	flags.ret_value = true
	SendScriptCallback("monster_on_before_hit",monster,shit,bone_id,flags)
	return flags.ret_value
end

-- called in step_manager.cpp CStepManager::update()
-- returning false will not play a footstep sound
function CActor__FootstepCallback(material,power,hud_view)
	flags.ret_value = true
	SendScriptCallback("actor_on_footstep",material,power,hud_view,flags)
	return flags.ret_value
end

-- Called upon weapon fire keybind, weapons can be stopped from firing if returns false
function CActor_Fire()
	flags.ret_value = true
	SendScriptCallback("actor_on_weapon_before_fire",flags)
	return flags.ret_value
end

-- called in CustomZone.cpp CCustomZone::shedule_Update()
-- returning false will make the CustomZone ignore the object
function CCustomZone_BeforeActivateCallback(zone,obj)
	flags.ret_value = true
	SendScriptCallback("anomaly_on_before_activate",zone,obj,flags)
	return flags.ret_value
end

-- called in burer.cpp CBurer::StaminaHit()
-- returning false will stop the burer from knocking player weapon away
function CBurer_BeforeWeaponDropCallback(monster,wpn)
	flags.ret_value = true
	SendScriptCallback("burer_on_before_weapon_drop",monster,wpn,flags)
	return flags.ret_value
end

-- Called upon thowing bolt, returning false will prevent the bolt from being released aftering throwing. Parameter is id of npc who threw the bolt
function CBolt__State(id)
	if (id == AC_ID) and game_difficulties.get_eco_factor("limited_bolts") then
		return true
	end
	return false
end

-- Called when actor is close/in-touch with an anomaly field
function CZone_Touch(obj)
	flags.ret_value = true
	SendScriptCallback("actor_on_feeling_anomaly", obj, flags)
	return flags.ret_value
end

-- Called on resolution change (after vid_restart)
function CHUDManager_OnScreenResolutionChanged()
	DestroyAll_UI()
	SendScriptCallback("on_screen_resolution_changed")
end

-- Called when the actor jumps
function CActor_on_jump()
	SendScriptCallback("actor_on_jump")
end

-- Called when the actor lands
function CActor_on_land(landing_speed)
	SendScriptCallback("actor_on_land", landing_speed)
end

get_console():execute("r__clear_models_on_unload 0")
function CALifeUpdateManager__on_before_change_level(packet)
--[[
	C++:
	net_packet.r					(&graph().actor()->m_tGraphID,sizeof(graph().actor()->m_tGraphID));
	net_packet.r					(&graph().actor()->m_tNodeID,sizeof(graph().actor()->m_tNodeID));
	net_packet.r_vec3				(graph().actor()->o_Position);
	net_packet.r_vec3				(graph().actor()->o_Angle);
--]]
-- Here you can do stuff when level changes BEFORE save is called, even change destination!. Packet is constructed as stated above

	-- Release dead bodies on level change (TODO: Determine if it's a bad idea to do this here)
	--
	local rbm = release_body_manager.get_release_body_manager()
	if (rbm) then
		rbm:clear(true)
	end
	--
	
	-- READ PACKET
	local pos,angle = vector(),vector()
	local gvid = packet:r_u16()
	local lvid = packet:r_u32()
	packet:r_vec3(pos)
	packet:r_vec3(angle)
	-- crazy hack to help prevent crash on Trucks Cemetery
	--[[local gg = game_graph()
	if (gg:valid_vertex_id(gvid) and alife():level_name(gg:vertex(gvid):level_id()) == "k02_trucks_cemetery") then
		log("k02_trucks_cemetery hack r__clear_models_on_unload 1")
		exec_console_cmd("r__clear_models_on_unload 1")
	end --]]
	--printf("CALifeUpdateManager__on_before_change_level pos=%s gvid=%s lvid=%s angle=%s",pos,gvid,lvid,angle)
	-- fix for car in 1.6 (TODO*kinda For some reason after loading a game ALL physic objects will not be teleported by TeleportObject need to investigate as to why, possibly something to do with object flags)
	local car = db.actor and db.actor:get_attached_vehicle()
	if (car) then
		TeleportObject(car:id(),pos,lvid,gvid)
	end
		-- REPACK it for engine method to read as normal
	--[[
	packet:w_begin(13)
	packet:w_u16(gvid)
	packet:w_u32(lvid)
	packet:w_vec3(pos)
	packet:w_vec3(angle)
	--]]
	-- reset read pointer
	packet:r_seek(2)
		
	if (bind_container.containers) then
		for id,t in pairs(bind_container.containers) do
			if (t.id) then
				pos.y = pos.y+100
				TeleportObject(t.id,pos,lvid,gvid)
			end
		end	
	end
end

-- '  .
function run_dynamic_element(folder,close_inv)
   if close_inv==false then
      folder:ShowDialog(true)
   elseif close_inv==true then
		folder:ShowDialog(true)
		hide_hud_all()
		level.show_weapon(false)
   else
      folder:ShowDialog(true)
   end
end

-- '    .
function give_object_to_actor(sec,count)
	if count==nil then count=1 end
	for i=1, count do
		alife_create_item(sec, db.actor)
	end
end

actor_move_states = {
	['mcFwd'] 			= 1,
	['mcBack'] 			= 2,
	['mcLStrafe'] 		= 4,
	['mcRStrafe']		= 8,
	['mcCrouch'] 		= 16,
	['mcAccel'] 		= 32,
	['mcTurn'] 			= 64,
	['mcJump'] 			= 128,
	['mcFall'] 			= 256,
	['mcLanding']		= 512,
	['mcLanding2'] 		= 1024,
	['mcClimb'] 		= 2048,
	['mcSprint'] 		= 4096,
	['mcLLookout'] 		= 8192,
	['mcRLookout'] 		= 16384,
	
	['mcAnyMove'] 		= 15,
	['mcAnyAction'] 	= 1935,
	['mcAnyState'] 		= 6192,
	['mcLookout']		= 24576,
	
}
function IsMoveState(state, compare_state)
	local bit = actor_move_states[state]
	if (not bit) then 
		printf("~IsMoveState | state {%s) not found ", state)
		return false 
	end
	
	if (not compare_state) then
		compare_state = level.actor_moving_state()
	end
	
	return (bit_and(compare_state, bit) ~= 0)
end


-------------------------------------------------------------------------------------------------------
-- 										INI EXTENSIONS
-------------------------------------------------------------------------------------------------------
local ini_cache = { [ini_sys] = {} }
function clear_ini_cache(ini)
	ini_cache[ini] = empty_table(ini_cache[ini])
	empty_table(INISYS_CACHE)
end

function reload_ini_sys()
	if (reload_system_ini) then 
		reload_system_ini()
		ini_sys = system_ini()
		printf("# reload_system_ini")
	else
		printf("! reload_system_ini is not supported in engine")
	end
end

if (USE_INI_MEMOIZE) then
	-- memoize ini results
	local function r_memoize(ini,s,k,def,typ)
		if not (s) then
			callstack()
		end
		
		if (ini_cache[ini]) then
			local key = s.."&"..k
			if (ini_cache[ini][key]) then
				return ini_cache[ini][key]
			end
		end
		
		if not (ini:section_exist(s) and ini:line_exist(s,k)) then
			return def
		end
		
		local result = ini:r_string(s,k)
		if (result) then
			if (typ == 0) then
				result = result == "true" or result == "1" or false
			elseif (typ == 1) then
				result = tonumber(result)
			end
			
			if (result ~= nil) then
				if (ini_cache[ini]) then
					local key = s.."&"..k
					if (ini_cache[ini][key]) then
						ini_cache[ini][key] = result
					end
				end
			end
		end
		
		return result == nil and def or result
	end

	function ini_file.r_string_ex(ini,s,k,def)
		return r_memoize(ini,s,k,def)
	end

	-- It is wise to use the def with r_bool_ex, because false and nil are consider 'not'. def is only returned on nil
	function ini_file.r_bool_ex(ini,s,k,def)
		return r_memoize(ini,s,k,def,0)
	end
	function ini_file.r_float_ex(ini,s,k,def)
		return r_memoize(ini,s,k,def,1)
	end
	function ini_file.r_sec_ex(ini,s,k,def)
		local result = ini_file.r_string_ex(ini,s,k,def)
		if result and ini_sys:section_exist(result) then
			return result
		end
		callstack()
		printe("!ERROR: section [%s] don't exist! Reading from section: %s - value: %s", result, s, k)
		return nil
	end
	function ini_file.r_line_ex(ini,s,k)
		if not (ini_cache[ini]) then
			ini_cache[ini] = {}
		end
		
		if (ini_cache[ini]) then
			local key = s .. "&" .. k
			if (ini_cache[ini][key]) then
				return unpack(ini_cache[ini][key])
			end
		end
		
		local a,b,c = ini:r_line(s,k,"","")
		
		if (ini_cache[ini]) then
			local key = s.."&"..k
			if (ini_cache[ini][key]) then
				ini_cache[ini][key] = {a,b,c}
			end
		end
		
		return a,b,c
	end
else
	function ini_file.r_string_ex(ini,s,k)
		--callstack()
		--printf("r_string_ex(%s,%s)",s,k)
		return ini:section_exist(s) and ini:line_exist(s,k) and ini:r_string(s,k) or nil
	end
	function ini_file.r_float_ex(ini,s,k)
		--callstack()
		--printf("r_float_ex(%s,%s)",s,k)
		return ini:section_exist(s) and ini:line_exist(s,k) and ini:r_float(s,k) or nil
	end
	function ini_file.r_bool_ex(ini,s,k,def)
		--callstack()
		if not (ini:section_exist(s) and ini:line_exist(s,k)) then
			return def
		end
		--printf("r_bool_ex(%s,%s)",s,k)
		local v = ini:r_string(s,k)
		return v == nil and def or v == "true" or v == "1" or false
	end
	function ini_file.r_sec_ex(ini,s,k,def)
		local result = ini_file.r_string_ex(ini,s,k,def)
		if result and ini_sys:section_exist(result) then
			return result
		end
		callstack()
		printe("!ERROR: section [%s] don't exist! Reading from section: %s - value: %s", result, s, k)
		return nil
	end
	function ini_file.r_line_ex(ini,s,k)
		--callstack()
		return ini:r_line(s,k,"","")
	end
end

function ini_file.r_string_to_condlist(ini,s,k,def)
	local src = ini:r_string_ex(s,k) or def
	if (src) then
		return xr_logic.parse_condlist(nil, s, k, src)
	end
end

function ini_file.r_list(ini,s,k,def)
	local src = ini:r_string_ex(s,k) or def
	if (src) then
		return parse_names(src)
	end
end

function ini_file.r_mult(ini,s,k,...)
	local src = ini:r_string_ex(s,k)
	if (src) then
		return unpack(parse_names(src))
	end
	return ...
end

-----------------------------------------
-- New INI wrapper to replace utils_data.cfg_file
class "ini_file_ex"
function ini_file_ex:__init(fname,advanced_mode)
	self.fname = getFS():update_path('$game_config$', '')..fname
	self.ini = ini_file(fname)
	self.cache = {}
	if (advanced_mode) then
		self.ini:set_override_names(true)
		self.ini:set_readonly(false)
		self.ini:save_at_end(false)
	end
end

function ini_file_ex:save()
	self.ini:save_as(self.fname)
end

-- r_value and w_value cache results
function ini_file_ex:r_value(s,k,typ,def)
	local cache_result = self.cache[s.."&"..k]
	if (cache_result) then
		return cache_result
	end
	if not (self.ini:section_exist(s) and self.ini:line_exist(s,k)) then
		return def
	end
	local v = self.ini:r_string(s,k)
	if (typ == 1) then
		v = v == nil and def or v == "true" or false
	elseif (typ == 2) then
		v = tonumber(v) or def
	end
	self.cache[s.."&"..k] = v
	return v == nil and def or v
end

function ini_file_ex:w_value(s,k,val,comment)
	self.cache[s.."&"..k] = val
	self.ini:w_string(s,k,val ~= nil and tostring(val) or "",comment ~= nil and tostring(comment) or "")
end

function ini_file_ex:collect_section(section)
	local _t = {}

	local n = self.ini:section_exist(section) and self.ini:line_count(section) or 0
	if (n > 0) then
		for i = 0,n-1 do
			local res,id,val = self.ini:r_line(section,i,"","")
			_t[id] = val
		end
	end

	return _t
end

function ini_file_ex:get_sections(keytable)
	local t = {}
	local function itr(section)
		if (keytable) then
			t[section] = true
		else
			t[#t+1] = section
		end
		return false
	end
	self.ini:section_for_each(itr)
	return t
end

function ini_file_ex:remove_line(section,key)
	self.ini:remove_line(section,key)
end

function ini_file_ex:section_exist(section)
	return self.ini:section_exist(section)
end

function ini_file_ex:line_exist(section,key)
	return self.ini:section_exist(section) and self.ini:line_exist(section,key)
end

function ini_file_ex:r_string_ex(s,k)
	return self.ini:section_exist(s) and self.ini:line_exist(s,k) and self.ini:r_string(s,k) or nil
end

function ini_file_ex:r_bool_ex(s,k,def)
	if not(self.ini:section_exist(s) and self.ini:line_exist(s,k)) then
		return def
	end
	local v = self.ini:r_string(s,k)
	return v == nil and def or v == "true" or v == "1" or false
end

function ini_file_ex:r_float_ex(s,k)
	return self.ini:section_exist(s) and self.ini:line_exist(s,k) and tonumber(self.ini:r_string(s,k)) or nil
end

function ini_file_ex:r_string_to_condlist(s,k,def)
	local src = self:r_string_ex(s,k) or def
	if (src) then
		return xr_logic.parse_condlist(nil, s, k, src)
	end
end

function ini_file_ex:r_list(s,k,def)
	local src = self:r_string_ex(s,k) or def
	if (src) then
		return parse_names(src)
	end
end

function ini_file_ex:r_mult(s,k,...)
	local src = self:r_string_ex(s,k) or def
	if (src) then
		return unpack(parse_names(src))
	end
	return ...
end

-- param caching
INISYS_CACHE = {}
function SYS_GetParam(typ, sec, param, def)
	if not (sec and param) then
		printe("!ERROR SYS_GetParam(%s, %s, %s) | What's missing?", typ, sec, param)
		callstack()
		return
	end
	
	if (INISYS_CACHE[sec] == nil) then
		INISYS_CACHE[sec] = {}
	end
	if (INISYS_CACHE[sec][param] == nil) then
		if typ == 0 then
			INISYS_CACHE[sec][param] = ini_sys:r_string_ex(sec,param) or def
		elseif typ == 1 then
			INISYS_CACHE[sec][param] = ini_sys:r_bool_ex(sec,param)
		elseif typ == 2 then
			INISYS_CACHE[sec][param] = ini_sys:r_float_ex(sec,param) or def
		end
	end
	return INISYS_CACHE[sec][param]
end

-------------------------------------------------------------------------------------------
-- 										TABLES HANDLERS
-------------------------------------------------------------------------------------------
function is_empty(t)
	if not (t) then
		return true
	end
	for i,j in pairs(t) do
		return false
	end
	return true
end

function is_not_empty(t)
	return (not is_empty(t))
end

function iempty_table(t)
	if not (t) then
		return {}
	end
	while #t > 0 do
		table.remove(t)
	end
	return t
end

function empty_table(t)
	if not (t) then
		return {}
	end
	for k,v in pairs(t) do
		t[k] = nil
	end
	return t
end

function size_table(t)
	assert(t and type(t) == "table", "global function size_table() called with invalid argument (nil or not table)")
	local n = 0
	for k,v in pairs(t) do
		n = n + 1
	end
	return n
end

function random_key_table(t)
	local check = t and type(t) == "table"
	if not check then
		callback()
		assert(nil, "global function random_key_table() called with invalid argument (nil or not table)")
	end
	local n = {}
	for k,v in pairs(t) do
		n[#n+1] = k
	end
	return #n > 0 and n[math.random(#n)] or nil
end

function copy_table(dest, src)
	for k,v in pairs(src) do
		if type(v) == "table" then
			--'      
			dest[k] = {}
			copy_table(dest[k], v)
		else
			dest[k] = v
		end
	end
end

function dup_table(src)
	local t = {}
	copy_table(t, src)
	return t
end

local function swap(array, index1, index2)
    array[index1], array[index2] = array[index2], array[index1]
end

function shuffle_table(t)
    local counter = #t
    while counter > 1 do
        local index = math.random(counter)
        swap(t, index, counter)
        counter = counter - 1
    end
end

function invert_table(t)
	local a = {}
	for k,v in pairs(t) do
		a[v] = k
	end
	return a
end

function t2k_table(t)
	local a = {}
	for i=1,#t do
		a[t[i]] = true
	end
	empty_table(t)
	copy_table(t,a)
end

function k2t_table(t)
	local a = {}
	for k,v in pairs(t) do
		a[#a+1] = k
	end
	empty_table(t)
	copy_table(t,a)
end

function print_table(table, subs)
	--[[
	local sub
	if subs ~= nil then
		sub = subs
	else
		sub = ""
	end
	for k,v in pairs(table) do
		if type(v) == "table" then
			print_table(v, sub.."["..k.."]----->")
		elseif type(v) == "function" then
			printf(sub.."%s = function",k)
		elseif type(v) == "userdata" then
			if (v.x) then
				printf(sub.."%s = %s",k,utils_data.vector_to_string(v))
			else
				printf(sub.."%s = userdata", k)
			end
		elseif type(v) == "boolean" then
					if v == true then
							if(type(k)~="userdata") then
									printf(sub.."%s = true",k)
							else
									printf(sub.."userdata = true")
							end
					else
							if(type(k)~="userdata") then
									printf(sub.."%s = false", k)
							else
									printf(sub.."userdata = false")
							end
					end
		else
			if v ~= nil then
				printf(sub.."%s = %s", k,v)
			else
				printf(sub.."%s = nil", k,v)
			end
		end
	end
	--]]
end

function store_table(table, subs)
	local sub
	if subs ~= nil then
		sub = subs
	else
		sub = ""
	end
	printf(sub.."{")
	for k,v in pairs(table) do
		if type(v) == "table" then
			printf(sub.."%s = ", tostring(k))
			store_table(v, sub.."    ")
		elseif type(v) == "function" then
			printf(sub.."%s = \"func\",", tostring(k))
			elseif type(v) == "userdata" then
					printf(sub.."%s = \"userdata\",", tostring(k))
		elseif type(v) == "string" then
			printf(sub.."%s = \"%s\",", tostring(k), tostring(v))
		else
			printf(sub.."%s = %s,", tostring(k), tostring(v))
		end
	end
	printf(sub.."},")
end

function spairs(t, order)
    -- collect the keys
    local keys = {}
    for k in pairs(t) do keys[#keys+1] = k end

    -- if order function given, sort by it by passing the table and keys a, b,
    -- otherwise just sort the keys
    if order then
        table.sort(keys, function(a,b) return order(t, a, b) end)
    else
        table.sort(keys)
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if keys[i] then
            return keys[i], t[keys[i]]
        end
    end
end



-------------------------------------------------------------------------------------------
-- 										VECTORS
-------------------------------------------------------------------------------------------
VEC_ZERO 	= vector():set(0,0,0)
VEC_X 		= vector():set(1,0,0)
VEC_Y 		= vector():set(0,1,0)
VEC_Z 		= vector():set(0,0,1)

function vec_sub(a,b)
	return vector():set(a):sub(b)
end

function vec_add(a,b)
	return vector():set(a):add(b)
end

function vec_set(vec)
	return vector():set(vec)
end


-------------------------------------------------------------------------------------------
-- 										CONSTANTS
-------------------------------------------------------------------------------------------
BoneID = {
--	[""]                      = 1 ,
	["bip01_pelvis"]          = 2 ,
	["bip01_l_thigh"]         = 3 ,
	["bip01_l_calf"]          = 4 ,
	["bip01_l_foot"]          = 5 ,
--	[""]                      = 6 ,
	["bip01_r_thigh"]         = 7 ,
	["bip01_r_calf"]          = 8 ,
	["bip01_r_foot"]          = 9 ,
--	[""]                      = 10,
	["bip01_spine"]           = 11,
	["bip01_spine1"]          = 12,
	["bip01_spine2"]          = 13,
	["bip01_neck"]            = 14,
	["bip01_head"]            = 15,
	["eye_left"]              = 16,
	["eye_right"]             = 17,
	["eyelid_1"]              = 18,
	["jaw_1"]                 = 19,
	["bip01_l_clavicle"]      = 20,
	["bip01_l_upperarm"]      = 21,
	["bip01_l_forearm"]       = 22,
	["bip01_l_hand"]          = 23,
--	[""]                      = 24,
}

HitTypeID = {
	["Burn"]         = 0 ,
	["Shock"]        = 1 ,
	["ChemicalBurn"] = 2 ,
	["Radiation"]    = 3 ,
	["Telepatic"]    = 4 ,
	["Wound"]        = 5 ,
	["FireWound"]    = 6 ,
	["Strike"]       = 7 ,
	["Explosion"]    = 8 ,
	["Wound_2"]      = 9 ,
	["LightBurn"]    = 10,
}

BoosterID = {
	["HpRestore"]              = 0,
	["PowerRestore"]           = 1,
	["RadiationRestore"]       = 2,
	["BleedingRestore"]        = 3,
	["MaxWeight"]              = 4,
	["RadiationProtection"]    = 5,
	["TelepaticProtection"]    = 6,
	["ChemicalBurnProtection"] = 7,
	["BurnImmunity"]           = 8,
	["ShockImmunity"]          = 9,
	["RadiationImmunity"]      = 10,
	["TelepaticImmunity"]      = 11,
	["ChemicalBurnImmunity"]   = 12,
	["ExplImmunity"]           = 13,
	["StrikeImmunity"]         = 14,
	["FireWoundImmunity"]      = 15,
	["WoundImmunity"]          = 16,
	["MaxCount"]               = 17,
}

SCANNED_SLOTS = {
	[1] = true,         --knife
	[2] = true,	        --wpn 1
	[3] = true,	        --wpn 2
	[4] = true,	        --grenades
	[5] = true,	        --binoculars
	[6] = true,	        --bolt
	[7] = true,	        --outfit
	[8] = true,	        --PDA
	[9] = true,	        --detector
	[10] = true,        --torch
	[11] = true,        --artefact
	[12] = true,        --helmet
	[13] = true,        --backpack
	--[14] = true,		--script animation
}



-------------------------------------------------------------------------------------------------------
-- 											SERVER OBJECTS
-------------------------------------------------------------------------------------------------------
_ALIFE_CNT          = 0 		-- collects total number of server objects
_ALIFE_WARNING      = 64000		-- warn user if alife storage exceeds this number
_ALIFE_CACHE        = {}		-- collect names of objects that get created or released in order
_ALIFE_CACHE_RECORD = false		-- record _ALIFE_CACHE
_ALIFE_UNREGISTER = {}

function alife_object(id)
	if (id == nil or id >= 65535) then
		callstack()
		printe("!ALIFE OBJECT ID IS %s!",id)
		return
	end
	return alife():object(id)
end

function alife_create(sec,pos,lid,gid,id,state)
	--[[
	Use it just like how you use alife():create
	
	Alternative use:
		alife_create(sec,obj,with_id,state)
		
		This is fast way to spawn something using other object properties
			with_id: [bool] count object id (to spawn item inside object)
			state: [bool - false] to spawn unregisted object
			
		Example: alife_create(sec,obj) will spawn object based on obj properties, no id included so it will spawn on world
	--]]
	
	--[[ return if alife storage is almost filled
	if alife_on_limit() then
		return
	end
	--]]

	-- In case we passed object to spawn at
	if (type(pos) == "userdata") and (not pos.x) then
		local obj = pos
		local with_id = (lid == true)
		state = gid
		
		if (type(obj.id) == "function") then
			pos = obj:position()
			lid = obj:level_vertex_id()
			gid = obj:game_vertex_id()
			id  = with_id and obj:id()
		elseif obj.id then
			pos = obj.position
			lid = obj.m_level_vertex_id
			gid = obj.m_game_vertex_id
			id  = with_id and obj.id
		else
			callstack()
			printe("!ERROR: alife_create | invalid usedata type")
			return
		end
	end
	
	-- Validate
	if not (sec and ini_sys:section_exist(sec)) then
		callstack()
		printe("!ERROR: alife_create | section [%s] is invalid!",sec)
		return
	elseif not (pos and lid and gid) then
		callstack()
		printe("!ERROR: invalid parameter for alife():create!",sec)
		return
	end
	
	--callstack()
	local se_obj
	if (state ~= nil) then
		--printf("- alife_create [%s] unregistered", sec)
		se_obj = alife():create(sec, pos, lid, gid, id, state)
	elseif id then
		--printf("- alife_create [%s] on owner (%s)", sec, id)
		se_obj = alife():create(sec, pos, lid, gid, id)
	else
		--printf("- alife_create [%s] in world", sec)
		se_obj = alife():create(sec, pos, lid, gid)
	end
	
	if (not se_obj) then
		printe("!ERROR: alife_create | failed to create object [%s]!",sec)
		return
	else
		alife_record(se_obj,true)
		return se_obj
	end
end

function alife_create_item(section, obj, t)
	--[[
	section: [string] (required)
		item section
	
	obj: [object/table] (required)
		owner of the item we want to spawn. can be a game or server object
		it can be a table {pos,lvl_id,game_id,id} for custom spawn in the Zone
	
	t: [table] (optional)
		item property table, can be used to process spawned item, like spawning multiuse item with specific uses for example.
		available keys:
			cond [num] = to apply custom condition on the item with condition bars
			uses [num] = to apply custom uses on the multi-use items
			ammo [num] = to apply custom ammo amount on the ammo boxes
			
			cond_r [table] = same as cond, but it picks a random condition in specified range (if it has more than 2 keys, then it will pick up a value randomly)
			cond_ct [string] = specific item type that it accept different condition range (from cond_cr)
			cond_cr [table] = same as cond_r, but applies it to specified item type (by cond_ct)
	
	NOTES:
		item property table (except ammo) only works on online items, if an item is spawned somewhere outside of online radius then it won't be processed
	
	TODO:
		- Add support for offline items properties
		- Add support for spawning unregistered items
	--]]
	
	--printf("alife_create_item | [%s]",section)
	--callstack()
		
	--[[ return if alife storage is almost filled
	if alife_on_limit() then
		return
	end
	--]]
	
	return itms_manager.create_item(section, obj, t)
end

function alife_process_item(section, id, t)
	-- id = item id to process
	-- t.cond [num] = apply passed condition
	-- t.uses [num] = apply passed uses count
	-- t.ammo [num] = apply passed ammo count
	
	itms_manager.process_item(section, id, t)
end

function alife_release(se_obj, msg)
	--printf("alife_release")
	--msg = msg or ""
	--callstack()
	
	-- Convert to server object if its a game object
	if se_obj and (type(se_obj.id) == "function") then
		se_obj = alife_object(se_obj:id())
	end
	
	if (not se_obj) then
		callstack()
		printe("!ERROR: alife_release | no server object!")
		return nil
	end
	
	-- Custom prints for debugging
	if msg then
		printf("~ alife_release [%s] (%s) | %s", se_obj:section_name(), se_obj.id, msg)
	end
	
	-- Walkaround for unregisted server objects callbacks for objects that can't be used at the moment for class registor
	local cls = se_obj:clsid()
	local class = ini_sys:r_string_ex(se_obj:section_name(),"class") or ""
	if _ALIFE_UNREGISTER[class] then
		SendScriptCallback("server_entity_on_unregister", se_obj, _ALIFE_UNREGISTER[class])
	end
	
	-- NPC
	if IsStalker(nil,cls) or IsMonster(nil,cls) then
		local squad = se_obj.group_id and se_obj.group_id ~= 65535 and alife_object(se_obj.group_id)
		if squad then
			squad:remove_npc(se_obj.id, true)
			return true
		end
		
	-- Squads
	elseif (cls == clsid.online_offline_group_s or cls == clsid.online_offline_group) then
		se_obj:remove_squad()
		return true
		
	-- Others
	else
		alife_record(se_obj,false)
		alife():release(se_obj,true)
		return true
	end
	
	return false
end

function alife_release_id(id, msg)
	if (not id) then
		callstack()
		printe("!ERROR: alife_release_id | no id passed!")
		return
	end
	
	return alife_release( alife_object(id), msg)
end

function alife_clone_weapon(se_obj, section, parent_id)
	if (not se_obj) then
		callstack()
		printe("!ERROR: alife_clone_weapon | no server object passed!")
		return
	end
	
	local se_obj_new = alife():clone_weapon(se_obj, section or se_obj:section_name(), se_obj.position, se_obj.m_level_vertex_id, se_obj.m_game_vertex_id, parent_id or se_obj.parent_id, false)
	if (se_obj_new) then

		-- Clone server object data
		local m_data_se = alife_storage_manager.get_state().se_object
		if m_data_se[se_obj.id] then
			m_data_se[se_obj_new.id] = {}
			copy_table(m_data_se[se_obj_new.id], m_data_se[se_obj.id])
		end
		
		-- Clone parts states
		if item_parts then
			item_parts.copy_parts_con(se_obj.id, se_obj_new.id)
			item_parts.clear_parts_con(se_obj.id)
		end
		
		-- Release the old weapon.
		alife_record(se_obj_new,true)
		alife_release(se_obj)
		
		-- Register the new weapon.
		alife():register(se_obj_new)
	end
	
	return se_obj_new
end

function alife_character_community(se_obj)
	if not (se_obj) then
		return
	end
	if IsStalker(nil, se_obj:clsid()) then
		return se_obj:community()
	end
	return "monster"
end

function alife_on_limit()
	return (_ALIFE_CNT > _ALIFE_WARNING)
end

function alife_record(se_obj,state)
	_ALIFE_CNT = _ALIFE_CNT + (state and 1 or -1)
	
	if _ALIFE_CACHE_RECORD and se_obj then
		local str = (state and "+" or "-") .. se_obj:section_name() .. " (" .. se_obj.id .. ")"
		table.insert(_ALIFE_CACHE, 1, str)
	end
	
	if alife_on_limit() then
		--callstack()
		printe("~WARNING alife ID storage is almost filled: %s", _ALIFE_CNT)
	end
end

function alife_first_update()
	_ALIFE_CNT = 0
	--empty_table(_ALIFE_CACHE)
	
	local sim = alife()
	local se_obj
	for i=1,65534 do
		se_obj = sim:object(i)
		if se_obj then
			alife_record(nil,true)
		end
	end
end

function create_ammo(section, position, lvi, gvi, pid, num)
	local num_in_box = ini_sys:r_u32(section, "box_size")
	local sim = alife()
	local t = {}
	while num > num_in_box do
		t[#t+1] = sim:create_ammo(section, position, lvi,	gvi, pid, num_in_box)
		alife_record(t[#t],true)
		num = num - num_in_box
	end
	local se_obj = sim:create_ammo(section, position, lvi,	gvi, pid, num)
	alife_record(se_obj,true)
	table.insert(t, se_obj)
	return t
end

function SetSwitchDistance(dist)
	if (alife()) then
		local p = net_packet()
		p:w_begin(18)
		p:w_float(dist or 2.0)
		level.send(p,true,true)
	end
end



-------------------------------------------------------------------------------------------------------
-- 										GAME OR SERVER OBJECTS
-------------------------------------------------------------------------------------------------------
is_squad_monster = {
		["monster_predatory_day"] 	= true,
		["monster_predatory_night"] = true,
		["monster_vegetarian"] 		= true,
		["monster_zombied_day"] 	= true,
		["monster_zombied_night"] 	= true,
		["monster_special"] 		= true,
		["monster"]					= true,
		["zoo_monster"]				= true
}
squad_community_by_behaviour = {
		["stalker"]							= "stalker",
		["bandit"]							= "bandit",
        ["csky"]							= "csky",
		["dolg"]							= "dolg",
		["freedom"]							= "freedom",
		["army"]							= "army",
		["ecolog"]							= "ecolog",
		["killer"]							= "killer",
		["zombied"]							= "zombied",
		["monolith"]						= "monolith",
		["greh"] 							= "greh",
		["isg"] 							= "isg",
		["renegade"]						= "renegade",
		["greh_npc"] 						= "greh_npc",
		["army_npc"] 						= "army_npc",
		["monster"]							= "monster",
		["monster_predatory_day"]			= "monster",
		["monster_predatory_night"]			= "monster",
		["monster_vegetarian"]				= "monster",
		["monster_zombied_day"]				= "monster",
		["monster_zombied_night"]			= "monster",
		["monster_special"]					= "monster",
		["zoo_monster"]						= "monster"
}

function get_object_community(obj)
	if type(obj.id) == "function" then
		return character_community(obj)
	else
		return alife_character_community(obj)
	end
end

function get_object_by_id(id)
	if not (id and type(id) == "number") then
		callstack()
		printe("!ERROR get_object_by_id | id (%s) is invalid", id)
		return
	end
	
	if (id == AC_ID) then
		return db.actor
	end
	
	local obj = db.storage[id] and db.storage[id].object or level.object_by_id(id)
	if (not obj) then
		printe("!ERROR get_object_by_id | no game object recieved from id (%s)", id)
		return
	end
	return obj
end

function character_community(obj)
	if not (obj) then
		return
	end
	if IsStalker(obj) then
		return obj:character_community()
	end
	return "monster"
end

function get_actor_true_community() -- no "actor_"
	return gameplay_disguise.get_default_comm()
end

function set_actor_true_community(new_comm, now) -- no "actor_"
	local curr_comm = get_actor_true_community()
	
	-- Update disguise default faction
	gameplay_disguise.update_default(new_comm, now)
	
	if IsStoryMode() then
		-- Change LTTZ info portion if needed first
		dialogs_lostzone.update_lttz_faction_info(curr_comm, new_comm)
		
		-- Recycle DRX availability to the new faction
		xr_effects.drx_sl_cancel_questlines(nil,nil,{})
		xr_effects.drx_sl_setup_questlines(nil,nil,{new_comm})
	end
end

function get_object_squad(object,caller) --   !!!!!
	if not (object) then
		return
	end
	if (object.group_id ~= nil and object.group_id ~= 65535) then
		return alife_object(object.group_id)
	end
	local sim = alife()
	local se_obj = type(object.id) == "function" and sim:object(object:id())
	return se_obj and se_obj.group_id ~= 65535 and sim:object(se_obj.group_id) or nil
end

function set_inactivate_input_time(delta)
	db.storage[AC_ID].disable_input_time = game.get_game_time()
	db.storage[AC_ID].disable_input_idle = delta
	level.disable_input()
end

function npc_in_actor_frustrum(npc) --'   NPC   
	local actor_dir = device().cam_dir
	--local actor_dir = db.actor:direction()
	local npc_dir = vec_sub(npc:position(),db.actor:position())
	local yaw = yaw_degree3d(actor_dir, npc_dir)
	--printf("YAW %s", tostring(yaw))
	return yaw < 35
end

function change_team_squad_group(se_obj, team, squad, group) --  team:squad:group .
	local cl_obj = db.storage[se_obj.id] and db.storage[se_obj.id].object
	if cl_obj ~= nil then
		cl_obj:change_team(team, squad, group)
	else
		se_obj.team = team
		se_obj.squad = squad
		se_obj.group = group
	end
	--printf("_G:TSG: [%s][%s][%s]", tostring(se_obj.team), tostring(se_obj.squad), tostring(se_obj.group))
end

function get_speaker(safe, all)
	if all then
		for k,v in pairs(db.storage) do
			local npc = v.object
			if npc ~= nil then
				if npc:is_talking() and npc:id() ~= AC_ID then
					return npc
				end
			end
		end
	else
		local npc_id = GetEvent("used_npc_id")
		local npc = npc_id and (db.storage[npc_id] and db.storage[npc_id].object or level.object_by_id(npc_id))
		if safe then
			if npc and npc:is_talking() and npc:id() ~= AC_ID then
				return npc
			end
		else
			return npc
		end
	end
	return nil
end

function distance_between(obj1, obj2)
	return obj1:position():distance_to(obj2:position())
end

function distance_between_safe(obj1, obj2)
	if (obj1 == nil or obj2 == nil) then
		return 100000
	end
	return obj1:position():distance_to(obj2:position())
end


-------------------------------------------------------------------------------------------------------
-- 										GLOBAL ALIFE INFO
-------------------------------------------------------------------------------------------------------
function has_alife_info(info_id)
	return alife():has_info(0, info_id)
end

function give_info(info)
	db.actor:give_info_portion(info)
	--printf("DEBUG: GIVE INFO %s",info)
	--if (xrs_debug_tools and xrs_debug_tools.actor_info) then
	--	xrs_debug_tools.actor_info[info] = true
	--end
end

function disable_info(info)
	if has_alife_info(info) then
		--printf("DEBUG: DISABLE INFO %s",info)
		--printf("*INFO*: disabled npc='single_player' id='%s'", info)
		db.actor:disable_info_portion(info)
		--if (xrs_debug_tools and xrs_debug_tools.actor_info) then
		--	xrs_debug_tools.actor_info[info] = nil
		--end
	end
end



-------------------------------------------------------------------------------------------------------
-- 										OBJECT DATA STORAGE
-------------------------------------------------------------------------------------------------------
function pstor_is_registered_type(tv)
	if tv ~= "boolean" and tv ~= "string" and tv ~= "number" then
		return false
	end
	return true
end

function save_var(obj, varname, val)
	if (obj == nil) then
		callstack()
	end
	local id = obj:id()
	if not (db.storage[id]) then
		printe("!save_var: Warning: no db.storage for %s, db.storage is for online objects only",id)
		return
	end

	if not (db.storage[id].pstor) then
		db.storage[id].pstor = {}
	end

	if not (USE_MARSHAL) then
		local tv = type(val)
		if val ~= nil and not pstor_is_registered_type(tv) then
			printe("!save_var: not registered type '%s' encountered [name=%s varname=%s ]", tv,obj:name(),varname)
			return
		end
	end

	if (type(val) == "userdata") then
		printe("!WARNING: save_var ONLY USE LUA DATA TYPES!!! %s",varname)
	else
		--printf("* save_var | id: %s - name: %s, value: %s", id, varname, val)
		db.storage[id].pstor[varname] = val
	end
end

function load_var(obj, varname, defval)
	if (obj == nil) then
 		callstack()
		printf("load_var failed for %s", varname)
		return defval
	end
	local id = obj:id()
	if not (db.storage[id]) then
 		printe("!load_var: Warning: no db.storage for %s, db.storage is for online objects only", id)
		return defval
	end

	--printf("* load_var | id: %s - name: %s", id, varname)
	return db.storage[id].pstor and db.storage[id].pstor[varname] or defval
end

function save_ctime(obj, varname, val)
	if (obj == nil) then
		callstack()
	end
	local id = obj:id()
	if not (db.storage[id]) then
		printe("!save_ctime: Warning: no db.storage for %s, db.storage is for online objects only",id)
		return
	end

	if not (db.storage[id].pstor_ctime) then
		db.storage[id].pstor_ctime = {}
	end

	db.storage[id].pstor_ctime[varname] = val
	
	--printf("* se_save_var | id: %s - name: %s", id, varname)
end

function load_ctime(obj, varname)
	if (obj == nil) then
		callstack()
	end
	local id = obj:id()
	if not (db.storage[id]) then
		printe("!load_var: Warning: no db.storage for %s, db.storage is for online objects only",id)
		return
	end

	--printf("* load_ctime | id: %s - name: %s", id, varname)
	return db.storage[id].pstor_ctime and db.storage[id].pstor_ctime[varname] or nil
end

function se_save_var(id, name, varname, val)
	local m_data = alife_storage_manager.get_state()
	if not (m_data.se_object[id]) then
		if not (val) then
			return -- already nil
		end
		local se_obj = alife_object(id)
		if not (se_obj) then 
			return
		end
		m_data.se_object[id] = {}
		m_data.se_object[id].name = se_obj:name()
	end
	m_data.se_object[id][varname] = val
	
	--printf("* se_save_var | id: %s - name: %s", id, varname)
end

function se_load_var(id, name, varname)
	local m_data = alife_storage_manager.get_state()
	if (m_data.se_object[id]) then
		--printf("* se_load_var | id: %s - name: %s", id, varname)
		return m_data.se_object[id][varname]
	end
end



-------------------------------------------------------------------------------------------------------
-- 										STORY ID HANDLERS
-------------------------------------------------------------------------------------------------------
function get_story_se_object(story_id)
	local obj_id = story_objects.object_id_by_story_id[story_id]
	return obj_id and alife_object(obj_id)
end

function get_story_se_item(story_id)
	return story_id and story_objects.get_story_se_item(story_id)
end

function get_story_object(story_id)
	local obj_id = story_objects.object_id_by_story_id[story_id]
	return obj_id and level.object_by_id(obj_id)
end

function get_object_story_id(obj_id)
	return obj_id and story_objects.story_id_by_object_id[obj_id]
end

function get_story_object_id(story_id)
	return story_id and story_objects.object_id_by_story_id[story_id]
end

function get_story_squad(story_id)
	return get_story_se_object(story_id)
end

function unregister_story_object_by_id(obj_id) -- remove
	story_objects.unregister(obj_id)
end

function level_object_by_sid( story_id ) -- get game object by story id
	local sim = alife()
	if sim then
		local se_obj = sim:story_object( story_id )
		if se_obj then
			return level.object_by_id( se_obj.id )
		end
	end
	return nil
end

function id_by_sid( story_id ) -- get object ID by story id
	local sim = alife()
	if sim then
		local se_obj = sim:story_object( story_id )
		if se_obj then
			return se_obj.id
		end
	end
	return nil
end



-------------------------------------------------------------------------------------------------------
-- 										EVENT STORAGE
-------------------------------------------------------------------------------------------------------
-- global temp table to store and get values in global scope. Handy for scripts to pick up values without the need for callbacks or accessing other scripts
_EVENT = {}

function GetEvent(k, v)
	--printf("GetEvent | [%s] [%s] | caller: %s", k, v, callstack(true,true))
	
	if v then
		return _EVENT[k] and _EVENT[k][v]
	end
	return _EVENT[k]
end

function SetEvent(k, v1, v2)
	--printf("SetEvent | [%s] [%s] [%s] | caller: %s", k, v1, v2, callstack(true,true))
	
	if (not k) then
		printf("!ERROR SetEvent | no key is passed")
		return
	end
	
	if (v2 ~= nil) then
		if (type(_EVENT[k]) ~= "table") then
			if _EVENT[k] then
				printf("~WARNING SetEvent | key: %s has value (%s) that got overwritten with a new table", k, _EVENT[k])
			end
			_EVENT[k] = {}
		end
		
		if (type(v1) ~= "string") then
			printf("!ERROR SetEvent | invalid arguments | k: %s - v: %s", k, v1)
			return
		end
		
		_EVENT[k][v1] = v2
	else
		_EVENT[k] = v1
	end
end



-------------------------------------------------------------------------------------------------------
-- 										MODE TESTING
-------------------------------------------------------------------------------------------------------
function IsAzazelMode()
	return axr_main.config and axr_main.config:r_value("character_creation","new_game_azazel_mode",1) == true or alife_storage_manager.get_state().enable_azazel_mode == true
end

function IsHardcoreMode()
	return axr_main.config and axr_main.config:r_value("character_creation","new_game_hardcore_mode",1) == true or (alife_storage_manager.get_state().ironman and alife_storage_manager.get_state().ironman.uuid ~= nil)
end

function IsStoryMode()
	return axr_main.config and axr_main.config:r_value("character_creation","new_game_story_mode",1) == true or not has_alife_info("story_mode_disabled")
end

function IsSurvivalMode()
	return axr_main.config and axr_main.config:r_value("character_creation","new_game_survival_mode",1) == true or alife_storage_manager.get_state().enable_survival_mode == true
end

function IsAgonyMode()
	return axr_main.config and axr_main.config:r_value("character_creation","new_game_conditions_mode",1) == true or alife_storage_manager.get_state().enable_conditions_mode == true
end

function IsTimerMode()
	return axr_main.config and axr_main.config:r_value("character_creation","new_game_timer_mode",1) == true or type(alife_storage_manager.get_state().enable_timer_mode) == "number"
end

function IsCampfireMode()
	return axr_main.config and axr_main.config:r_value("character_creation","new_game_campfire_mode",1) == true or alife_storage_manager.get_state().enable_campfire_mode == true
end

function IsWarfare()
	return axr_main.config and axr_main.config:r_value("character_creation","new_game_warfare",1) == true or alife_storage_manager.get_state().enable_warfare_mode == true
end

function IsTestMode()
	return (level.name() == "fake_start")
end

local story_comm = {
	["csky"] = true,
	["dolg"] = true,
	["ecolog"] = true,
	["freedom"] = true,
	["killer"] = true,
	["stalker"] = true,
}
function IsStoryPlayer()
	local comm = gameplay_disguise.get_default_comm()
	return story_comm[comm]
end



-------------------------------------------------------------------------------------------------------
-- 										CLASS TESTING
-------------------------------------------------------------------------------------------------------
local monster_classes
local weapon_classes
local artefact_classes
local anomaly_classes

function IsStalker(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	return c and (c == clsid.script_stalker or c == clsid.script_actor or c == clsid.stalker or c == clsid.actor) or false
end

function IsMonster(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	if not (monster_classes) then
		monster_classes = {
		[clsid.bloodsucker] 			= true,
		[clsid.bloodsucker_s] 			= true,
		[clsid.boar] 					= true,
		[clsid.boar_s] 					= true,
		[clsid.dog_red] 				= true,
		[clsid.dog_s] 					= true,
		[clsid.flesh] 					= true,
		[clsid.flesh_s] 				= true,
		[clsid.dog_black] 				= true,
		[clsid.pseudodog_s] 			= true,
		[clsid.burer] 					= true,
		[clsid.burer_s] 				= true,
		[clsid.cat] 					= true,
		[clsid.cat_s] 					= true,
		[clsid.rat] 					= true,
		[clsid.rat_s]					= true,
		[clsid.chimera] 				= true,
		[clsid.chimera_s] 				= true,
		[clsid.controller] 				= true,
		[clsid.controller_s] 			= true,
		[clsid.fracture] 				= true,
		[clsid.fracture_s] 				= true,
		[clsid.poltergeist] 			= true,
		[clsid.poltergeist_s] 			= true,
		[clsid.pseudo_gigant] 			= true,
		[clsid.gigant_s] 				= true,
		[clsid.zombie] 					= true,
		[clsid.zombie_s] 				= true,
		[clsid.snork] 					= true,
		[clsid.snork_s] 				= true,
		[clsid.tushkano] 				= true,
		[clsid.tushkano_s] 				= true,
		[clsid.psy_dog_s] 				= true,
		[clsid.psy_dog_phantom_s] 		= true
		}
	end
	return c and monster_classes[c] or false
end

function IsAnomaly(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	if not (anomaly_classes) then
		anomaly_classes = {
		[clsid.zone]				= true,
		[clsid.ameba_zone]			= true,
		[clsid.zone_ameba_s]		= true,
		[clsid.zone_acid_fog]		= true,
		[clsid.zone_bfuzz]			= true,
		[clsid.zone_campfire]		= true,
		[clsid.zone_dead]			= true,
		[clsid.zone_galantine]		= true,
		[clsid.zone_mincer]			= true,
		[clsid.zone_mosquito_bald]	= true,
		[clsid.zone_radioactive]	= true,
		[clsid.zone_rusty_hair]		= true,
		[clsid.zone_bfuzz]			= true,
		[clsid.zone_bfuzz_s]		= true,
		[clsid.zone_mosquito_bald]	= true,
		[clsid.zone_mbald_s]		= true,
		[clsid.zone_galantine]		= true,
		[clsid.zone_galant_s]		= true,
		[clsid.zone_mincer]			= true,
		[clsid.zone_mincer_s]		= true,
		[clsid.zone_radioactive]	= true,
		[clsid.zone_radio_s]		= true,
		[clsid.torrid_zone]			= true,
		[clsid.zone_torrid_s]		= true,
		[clsid.nogravity_zone]		= true,
		[clsid.zone_nograv_s]		= true,
		}
	end
	return c and anomaly_classes[c] or false
end

function IsTrader(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	return c and (c == clsid.script_trader or c == clsid.trader) or false
end 

function IsCar(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	return c and (c == clsid.car or c == clsid.car_s) or false
end

function IsHelicopter(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	return c and (c == clsid.helicopter or c == clsid.script_heli) or false
end

function IsInvbox(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	return c and (c == clsid.inventory_box_s or c == clsid.inventory_box)
end

function isLc(obj)
	return (obj:clsid() == clsid.level_changer or obj:clsid() == clsid.level_changer_s)
end

function IsWounded(o)
	if not ((o:clsid() == clsid.stalker or o:clsid() == clsid.script_stalker) and o:alive()) then 
		return false 
	end 
	
	if (o:critically_wounded() or o:in_smart_cover()) then 
		return false 
	end
	
	if o:best_enemy() and load_var(o, "wounded_fight") == "true" then
		return false
	end
	
	local state = tostring(load_var(o, "wounded_state"))
	if (state == "nil") then
		return false
	end
	
	return true
end

-- Items

function IsOutfit(o,c)
	if not c then
		c = o and o:clsid()
	end
	return c and (c == clsid.equ_stalker_s or c == clsid.equ_stalker)
end

function IsHeadgear(o,c)
	if not c then
		c = o and o:clsid()
	end
	return c and (c == clsid.equ_helmet_s or c == clsid.helmet)
end

function IsExplosive(o,c)
	if not c then
		c = o and o:clsid()
	end
	return c and (c == clsid.obj_explosive_s or  c == clsid.obj_explosive)
end

function IsPistol(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	local pistol = {
					[clsid.wpn_pm_s] = true,
					[clsid.wpn_walther_s] = true,
					[clsid.wpn_usp45_s] = true,
					[clsid.wpn_hpsa_s] = true,
					[clsid.wpn_lr300_s] = true,
					[clsid.wpn_pm] = true,
					[clsid.wpn_walther] = true,
					[clsid.wpn_usp45] = true,
					[clsid.wpn_hpsa] = true,
					[clsid.wpn_lr300] = true
	}
	return c and pistol[c] or false
end

function IsMelee(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	return c and (c == clsid.wpn_knife or c == clsid.wpn_knife_s) or false
end

function IsSniper(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	local sniper = {
				[clsid.wpn_svu_s] = true,
				[clsid.wpn_svd_s] = true,
				[clsid.wpn_vintorez_s] = true,
				[clsid.wpn_svu] = true,
				[clsid.wpn_svd] = true,
				[clsid.wpn_vintorez] = true
	}
	return c and sniper[c] or false
end

function IsLauncher(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	local launcher = {
					[clsid.wpn_rg6_s] = true,
					[clsid.wpn_rpg7_s] = true,
					[clsid.wpn_rg6] = true,
					[clsid.wpn_rpg7] = true
	}
	return c and launcher[c] or false
end

function IsShotgun(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	local shotgun = {
				[clsid.wpn_bm16_s] = true,
				[clsid.wpn_shotgun_s] = true,
				[clsid.wpn_auto_shotgun_s] = true,
				[clsid.wpn_bm16] = true,
				[clsid.wpn_shotgun] = true
	}
	return c and shotgun[c] or false
end

function IsRifle(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	local rifle = {
				[clsid.wpn_ak74_s] = true,
				[clsid.wpn_groza_s] = true,
				[clsid.wpn_val_s] = true,
				[clsid.wpn_ak74] = true,
				[clsid.wpn_groza] = true,
				[clsid.wpn_val] = true
	}
	return c and rifle[c] or false
end

function IsWeapon(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	if not (weapon_classes) then
		weapon_classes = {
				[clsid.wpn_vintorez_s] 			= true,
				[clsid.wpn_ak74_s] 				= true,
				[clsid.wpn_lr300_s] 			= true,
				[clsid.wpn_hpsa_s] 				= true,
				[clsid.wpn_pm_s] 				= true,
				[clsid.wpn_shotgun_s] 			= true,
				[clsid.wpn_auto_shotgun_s]		= true,
				[clsid.wpn_bm16_s] 				= true,
				[clsid.wpn_svd_s] 				= true,
				[clsid.wpn_svu_s] 				= true,
				[clsid.wpn_rg6_s] 				= true,
				[clsid.wpn_rpg7_s] 				= true,
				[clsid.wpn_val_s] 				= true,
				[clsid.wpn_walther_s] 			= true,
				[clsid.wpn_usp45_s] 			= true,
				[clsid.wpn_groza_s] 			= true,
				[clsid.wpn_knife_s] 			= true,
				[clsid.wpn_vintorez] 			= true,
				[clsid.wpn_ak74] 				= true,
				[clsid.wpn_lr300] 				= true,
				[clsid.wpn_hpsa] 				= true,
				[clsid.wpn_pm] 					= true,
				[clsid.wpn_shotgun] 			= true,
				[clsid.wpn_bm16] 				= true,
				[clsid.wpn_svd] 				= true,
				[clsid.wpn_svu] 				= true,
				[clsid.wpn_rg6] 				= true,
				[clsid.wpn_rpg7] 				= true,
				[clsid.wpn_val] 				= true,
				[clsid.wpn_walther] 			= true,
				[clsid.wpn_usp45] 				= true,
				[clsid.wpn_groza] 				= true,
				[clsid.wpn_knife] 				= true
		}
	end
	return c and weapon_classes[c] or false
end

function IsAmmo(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	return c and (c == clsid.wpn_ammo or c == clsid.wpn_ammo_s)
end

function IsGrenade(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	
	if not (grenade_classes) then
		grenade_classes = {
				[clsid.wpn_grenade_f1_s] 		= true,
				[clsid.wpn_grenade_rgd5_s] 		= true,
				[clsid.wpn_grenade_launcher_s] 	= true,
				[clsid.wpn_grenade_fake] 		= true,
				[clsid.wpn_grenade_f1]			= true,
				[clsid.wpn_grenade_launcher] 	= true,
				[clsid.wpn_grenade_rgd5] 		= true,
				[clsid.wpn_grenade_rpg7]		= true
		}
	end
	return c and grenade_classes[c] or false
end

function IsBolt(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	return c and (c == clsid.obj_bolt)
end

function IsArtefact(o,c)
	if not (c) then
		c = o and o:clsid()
	end
	if not (artefact_classes) then
		artefact_classes = {
		[clsid.art_bast_artefact] 		= true,
		[clsid.art_black_drops] 		= true,
		[clsid.art_dummy] 				= true,
		[clsid.art_electric_ball] 		= true,
		[clsid.art_faded_ball] 			= true,
		[clsid.art_galantine] 			= true,
		[clsid.art_gravi] 				= true,
		[clsid.art_gravi_black] 		= true,
		[clsid.art_mercury_ball] 		= true,
		[clsid.art_needles] 			= true,
		[clsid.art_rusty_hair] 			= true,
		[clsid.art_thorn] 				= true,
		[clsid.art_zuda] 				= true,
		[clsid.artefact] 				= true,
		[clsid.artefact_s] 				= true
		}
	end
	return c and artefact_classes[c] or false
end



-------------------------------------------------------------------------------------------
-- 										ITEMS LOOKUP TABLE
-------------------------------------------------------------------------------------------
-- parsed list of item sections and values grouped in types, easy to deal with instead of reading system_ini
_ITM = {} 

function IsItem(typ, sec, obj)
	sec = obj and obj:section() or sec
	return sec and _ITM[typ] and _ITM[typ][sec] or nil
end
function GetItemList(typ)
	return _ITM[typ] or {}
end

function Parse_ITM()

	local function compare_ITM_value( param, value )
		if (param == nil) then
			return false
		end
		if (value == nil) then
			return true
		end
		return param == value
	end
	
	local function Parse_ITM_value(section, typ, condition, value )
		if (not _ITM[typ]) then
			_ITM[typ] = {}
		end
		if condition == true then
			_ITM[typ][section] = true
		elseif type(condition) == "table" then
			if (condition[2] == 0) and compare_ITM_value( ini_sys:r_string_ex(section,condition[1]), condition[3]) then
				_ITM[typ][section] = true
			elseif (condition[2] == 1) and ini_sys:r_bool_ex(section,condition[1]) then
				_ITM[typ][section] = true
			elseif (condition[2] == 2) and compare_ITM_value( ini_sys:r_float_ex(section,condition[1]), condition[3]) then
				_ITM[typ][section] = true
			elseif (condition[2] == 5) and compare_ITM_value( ini_sys:r_s32(section,condition[1]), condition[3]) then
				_ITM[typ][section] = true
			end
		end
		
		if _ITM[typ][section] and value and type(value) == "table" then
			if (value[2] == 0) then
				_ITM[typ][section] = ini_sys:r_string_ex(section,value[1]) or value[3]
			elseif (value[2] == 1) then
				_ITM[typ][section] = ini_sys:r_bool_ex(section,value[1]) or value[3]
			elseif (value[2] == 2) then
				_ITM[typ][section] = ini_sys:r_float_ex(section,value[1]) or value[3]
			elseif (value[2] == 3) then
				_ITM[typ][section] = parse_list(ini_sys,section,value[1],value[3])
			elseif (value[2] == 4) then
				_ITM[typ][section] = (string_find(section,"1") and 1) or (string_find(section,"2") and 2) or 3
			elseif (value[2] == 5) then
				_ITM[typ][section] = ini_sys:r_s32(section,value[1]) or value[3]
			end
		end
	end
	
	ini_sys:section_for_each(function(section)
		if ini_sys:r_float_ex(section,"inv_grid_x") then
			local cls = ini_sys:r_string_ex(section,"class") or "unknown"
			local kind = ini_sys:r_string_ex(section,"kind") or "unknown"
			local bind = ini_sys:r_string_ex(section,"script_binding")
			
			Parse_ITM_value( section , "anim"   	 , {"anim_item",1}             )
			Parse_ITM_value( section , "release"   	 , {"release_on_first_update",1})
						
			Parse_ITM_value( section , "fake_ammo"   , {"fake_ammo",1}             )
			Parse_ITM_value( section , "grenade_ammo", {"grenade_ammo",1}          )
			Parse_ITM_value( section , "ammo"     	 , (cls == "AMMO")             , {"box_size",5})
			Parse_ITM_value( section , "ammo"     	 , (cls == "AMMO_S")           , {"box_size",5})
			Parse_ITM_value( section , "sil"		 , (cls == "WP_SILEN")   	   )
			Parse_ITM_value( section , "scope"		 , {"repair_type",0,"scope"}   ) 
			Parse_ITM_value( section , "scope"		 , (cls == "WP_SCOPE")   	   )
			Parse_ITM_value( section , "gl"			 , (cls == "WP_GLAUN")         )
			
			Parse_ITM_value( section , "helmet"  	 , (cls == "E_HLMET")          )
			Parse_ITM_value( section , "outfit"  	 , (cls == "E_STLK") or (cls == "EQU_STLK"))
			Parse_ITM_value( section , "backpack" 	 , (cls == "EQ_BAKPK")         )
			Parse_ITM_value( section , "artefact" 	 , (cls == "ARTEFACT") or (cls == "SCRPTART")         , {"af_rank",2})
			
			Parse_ITM_value( section , "device"      , (bind == "item_device.bind") )
			Parse_ITM_value( section , "craft"       , {"craft_tool",1}            )
			Parse_ITM_value( section , "repair"      , {"repair_tool",1}           , {"repair_only",3,true})
			Parse_ITM_value( section , "workshop"    , {"workshop_tool",1}         , {"workshop_support",3,true})
			Parse_ITM_value( section , "disassemble" , {"disassemble_tool",1}      , {"degradation_factor",2,0.02})
			Parse_ITM_value( section , "cook"        , {"cooking_tool",1}          )
			Parse_ITM_value( section , "map"         , {"use_map",0}               )
			Parse_ITM_value( section , "money"       , {"money_amount",0}          , {"money_amount",3})
			Parse_ITM_value( section , "recipe"      , {"recipe",1}                )
			Parse_ITM_value( section , "letter"      , {"letter",1}                )
			Parse_ITM_value( section , "meal"        , {"meal",2}                  , {"meal",2})
			Parse_ITM_value( section , "eatable"  	 , (kind == "i_mutant_cooked") )
			Parse_ITM_value( section , "eatable"  	 , (kind == "i_food")          )
			Parse_ITM_value( section , "eatable"  	 , (kind == "i_drink")         )
			Parse_ITM_value( section , "tool"     	 , (kind == "i_tool")          )
			Parse_ITM_value( section , "tool"     	 , (kind == "i_kit")           )
			Parse_ITM_value( section , "part"     	 , (kind == "i_part")          )
			Parse_ITM_value( section , "upgrade"  	 , (kind == "i_upgrade")       , {"",4})
			
			Parse_ITM_value( section , "quest"  	 , {"quest_item",1}          	)
			Parse_ITM_value( section , "quest"  	 , (kind == "i_quest")       	)

			
			-- Multi-use items
			if (cls == "II_FOOD") then
				Parse_ITM_value( section , "consumable"  , true )
				
				local max_uses = ini_sys:r_float_ex(section,"max_uses")
				local condition = ini_sys:r_bool_ex(section,"use_condition")
				if condition and max_uses and (max_uses > 1) then
				
					Parse_ITM_value( section , "multiuse"  , true , {"max_uses",2}	)
					if (not ini_sys:r_bool_ex(section,"remove_after_use")) then
						Parse_ITM_value( section , "multiuse_r"  , true , {"max_uses",2}	)
					end
				end
			end
			
			-- Cache weapons with fake ammo
			local ammo = ini_sys:r_string_ex(section,"ammo_class")
			if ammo then
				local ammo_list = parse_list(ini_sys,section,"ammo_class")
				Parse_ITM_value( section , "fake_ammo_wpn"  , IsItem("fake_ammo", ammo_list[1]) )
			end
		end
	end)
	
	--[[
	for typ,v in pairs(_ITM) do
		for sec,val in pairs(v) do
			printf("$_ITM[%s][%s] = %s", typ, sec, val)
		end
	end
	--]]
end
Parse_ITM()

function add_console_command(name, f)
	cmd = debug_cmd_list.command_get_list()
	if cmd[name] then
		printf('a command named "%s" already exists', name)
		return false
	else
		cmd[name] = f
		return true
	end
end

local __g_last_level
local __g_last_lvid
function get_player_level_id()
	if IsTestMode() or level.name() == "fake_start" then return end
	if level.name() == __g_last_level then
		return __g_last_lvid
	end
	__g_last_level = level.name()
	__g_last_lvid = game_graph():vertex(alife():actor().m_game_vertex_id):level_id()
end

-- if return game_object then it ignores engine. If return nil, then engine tries to find item to kill
-- called from engine!
function update_best_weapon(npc,cur_wpn)
	--[[
	local st = db.storage[npc:id()]
	if not (st) then 
		return cur_wpn
	end 

	-- we want to choose new best_weapon every n seconds unless we don't have one
	local tg = time_global()
	if (cur_wpn and st._choose_bw_timer and tg < st._choose_bw_timer) then
		return cur_wpn
	end 
	
	st._choose_bw_timer = tg+10000
	
	-- weapon priority
	local prior = {}
	
	local be = npc:best_enemy()
	if (be) then 
		if (IsMonster(nil,be:clsid())) then 
			if (be:position():distance_to_sqr(npc:position()) > 2500) then 
				prior[#prior+1] = _G.IsRifle
				prior[#prior+1] = _G.IsSniper
				prior[#prior+1] = _G.IsPistol
				prior[#prior+1] = _G.IsShotgun
				prior[#prior+1] = _G.IsLauncher
			else 
				prior[#prior+1] = _G.IsShotgun 
				prior[#prior+1] = _G.IsRifle
				prior[#prior+1] = _G.IsPistol
				prior[#prior+1] = _G.IsSniper
				prior[#prior+1] = _G.IsLauncher
			end
		elseif (be:position():distance_to_sqr(npc:position()) > 1000) then 
			prior[#prior+1] = _G.IsSniper
			prior[#prior+1] = _G.IsRifle
			prior[#prior+1] = _G.IsPistol
			prior[#prior+1] = _G.IsShotgun
			prior[#prior+1] = _G.IsLauncher
		elseif (be:position():distance_to_sqr(npc:position()) > 2500) then 
			prior[#prior+1] = _G.IsRifle
			prior[#prior+1] = _G.IsSniper
			prior[#prior+1] = _G.IsPistol
			prior[#prior+1] = _G.IsShotgun
			prior[#prior+1] = _G.IsLauncher
		else 
			prior[#prior+1] = _G.IsRifle
			prior[#prior+1] = _G.IsShotgun
			prior[#prior+1] = _G.IsPistol
			prior[#prior+1] = _G.IsSniper
			prior[#prior+1] = _G.IsLauncher
		end
	elseif (axr_npc_vs_heli.is_under_npc_vs_heli(npc)) then
		prior[#prior+1] = _G.IsLauncher 
		prior[#prior+1] = _G.IsSniper 
		prior[#prior+1] = _G.IsRifle
		prior[#prior+1] = _G.IsPistol
		prior[#prior+1] = _G.IsShotgun
	else
		prior[#prior+1] = _G.IsRifle
		prior[#prior+1] = _G.IsSniper
		prior[#prior+1] = _G.IsPistol
		prior[#prior+1] = _G.IsShotgun
		prior[#prior+1] = _G.IsLauncher
	end

	local best_weapons = {}
	
	local function itr(npc,wpn)
		local cls = wpn:clsid()
		if (IsWeapon(nil,cls) ~= true) then 
			return false
		end
		--if (wpn:get_ammo_in_magazine() > 0) then
			for i=1,#prior do 
				if (prior[i](nil,cls)) then
					best_weapons[wpn] = ini_sys:r_float_ex(wpn:section(),"cost")
					break
				end
			end
		--end
		return false
	end
	
	npc:iterate_inventory(itr,npc)

	
	for wpn,cost in spairs(best_weapons,function(t,a,b) return t[a] > t[b] end) do 
		DEBUG_NPC = level.get_target_obj and level.get_target_obj()
		if (DEBUG_NPC) then 
			actor_menu.set_msg(1, strformat("%s bw=%s",DEBUG_NPC:name(),wpn and wpn:name()), 2)
		end
		return wpn
	end

	--]]
	
	local flags = {gun_id = nil}
	SendScriptCallback("npc_on_choose_weapon", npc, cur_wpn, flags)
	
	if flags.gun_id and tonumber(flags.gun_id) then	
		local se_gun = alife_object(tonumber(flags.gun_id))
		if se_gun and IsWeapon(nil, se_gun:clsid()) and se_gun.parent_id == npc:id() then
			local obj = level.object_by_id(tonumber(flags.gun_id))
			if obj then
				return obj
			else
				printf('%s online object is not available, %s will choose gun from engine', se_gun:name(), npc:name())
				return
			end
		else
			printf('%s does not point to a valid gun in npc inventory, %s will choose gun from engine', flags.gun_id, npc:name())
			return
		end
	end
	
	return
end

