1087 lines
34 KiB
Plaintext
1087 lines
34 KiB
Plaintext
----------------------------------------------------------------
|
|
-- Refactored magazines by Arti and Raven. Most of this code is cribbed from wuut_mags original, but cleaned up significantly.
|
|
----------------------------------------------------------------
|
|
|
|
--Function alises
|
|
gc = game.translate_string
|
|
get_mag_data = magazine_binder.get_data
|
|
set_mag_data = magazine_binder.set_data
|
|
is_supported_weapon = magazine_binder.is_supported_weapon
|
|
get_magazine_caliber = magazine_binder.get_magazine_caliber
|
|
weapon_default_magazine = magazine_binder.weapon_default_magazine
|
|
is_compatible = magazine_binder.is_compatible
|
|
isMagazine = magazine_binder.is_magazine
|
|
is_magazine = magazine_binder.is_magazine
|
|
get_mag_loaded = magazine_binder.get_mag_loaded
|
|
is_carried_mag = magazine_binder.is_carried_mag
|
|
toggle_carried_mag = magazine_binder.toggle_carried_mag
|
|
get_carried_mags = magazine_binder.get_carried_mags
|
|
room_in_pouch = magazine_binder.room_in_pouch
|
|
get_retool_section = magazine_binder.get_retool_section
|
|
get_weapon_base_type = magazine_binder.get_weapon_base_type
|
|
get_magazine_base_type = magazine_binder.get_magazine_base_type
|
|
get_mags_for_basetype = magazine_binder.get_mags_for_basetype
|
|
dump_data = magazine_binder.dump_data
|
|
get_mags_by_ammo_type = magazine_binder.get_mags_by_ammo_type
|
|
get_config = magazines_mcm.get_config
|
|
|
|
|
|
|
|
local dbg_log
|
|
function print_dbg( text , ...)
|
|
if (magazines_mcm.get_config("debug") or false) then
|
|
dbg_log = dbg_log or mcm_log and mcm_log.new("DBG")
|
|
if dbg_log then
|
|
dbg_log.enabled = true
|
|
dbg_log:log( text , ...)
|
|
else
|
|
printf( "RAX: | %s | "..text ,time_global(), ...)
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local err_log
|
|
function print_err( text , ...)
|
|
if (magazines_mcm.get_config("debug") or false) then
|
|
err_log = err_log or mcm_log and mcm_log.new("ERR")
|
|
if err_log then
|
|
err_log.enabled = true
|
|
err_log:log( text , ...)
|
|
end
|
|
printe( "ERR: | %s | "..text ,time_global(), ...)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- actor action state
|
|
local action_state = 0
|
|
-- states (idle, mag to weapon reload in progress, ammo to mag load in progress)
|
|
ACTION_NONE = 0
|
|
ACTION_RELOAD = 1
|
|
ACTION_LOAD_MAG = 2
|
|
|
|
local jammed_guns = {}
|
|
local load_timeout = {}
|
|
|
|
-- due to changing how mag loading works (1 big event vs. several events) we need this here to persist which box we are taking ammo from
|
|
local ext_ammo_box = nil
|
|
|
|
-- flag to force reload during the animation instead of afterwards
|
|
local force_reload = false
|
|
------------------------
|
|
-- SECTION debug info --
|
|
------------------------
|
|
|
|
local function parent_is_box(obj)
|
|
local parent = obj:parent()
|
|
if parent:id() == 0 then return false end
|
|
local se_parent = alife_object(parent:id())
|
|
if se_parent:clsid() == clsid.inventory_box_s then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function action_in_progress()
|
|
return action_state ~= ACTION_NONE
|
|
end
|
|
|
|
function action_start_reload()
|
|
action_state = ACTION_RELOAD
|
|
end
|
|
|
|
function action_start_magload()
|
|
action_state = ACTION_LOAD_MAG
|
|
end
|
|
|
|
function do_interrupt_reload()
|
|
if action_state == ACTION_RELOAD then
|
|
action_state = ACTION_NONE
|
|
end
|
|
end
|
|
|
|
function do_interrupt_magload()
|
|
if action_state == ACTION_LOAD_MAG then
|
|
action_state = ACTION_NONE
|
|
end
|
|
end
|
|
|
|
function do_interrupt()
|
|
action_state = ACTION_NONE
|
|
end
|
|
|
|
local time_event_cache = {}
|
|
function create_time_event(ev_id,act_id,timer,f,...)
|
|
disable_info("sleep_active")
|
|
time_event_cache[ev_id .. "|" .. act_id] = true
|
|
CreateTimeEvent(ev_id,act_id,timer,f,...)
|
|
end
|
|
|
|
--------------------------------------
|
|
-- SECTION mag un/loading functions --
|
|
--------------------------------------
|
|
|
|
-- Get the time delay between loading/unloading rounds from/to magazine
|
|
--
|
|
-- **mag_section** *(string)* = section of the mag to get **load_delay** param if **ignore_loaddelay** is false
|
|
--
|
|
-- **is_unload** *(bool)* = should we get mag unload time
|
|
cache_mag_time = {}
|
|
function get_mag_time(mag_section,is_unload)
|
|
is_unload = is_unload or false
|
|
local lookup_key = mag_section .. (is_unload and "_unload" or "_load")
|
|
if not cache_mag_time[lookup_key] then
|
|
local default_delay = SYS_GetParam(2, mag_section, "load_delay") or 0.4
|
|
cache_mag_time[lookup_key] = default_delay * (is_unload and get_config('mag_unloadtime_factor') or get_config('mag_loadtime_factor'))
|
|
end
|
|
return cache_mag_time[lookup_key]
|
|
end
|
|
|
|
-- check if load/unload delay has been altered and wipe cache
|
|
function on_option_change()
|
|
empty_table(cache_mag_time)
|
|
end
|
|
|
|
|
|
function timed_refresh()
|
|
ResetTimeEvent("mag_redux", "timed_refresh", 0.5) -- if called while event is still active extend the time to allow the new change to take effect before refreshing.
|
|
create_time_event("mag_redux", "timed_refresh", 0.5, function() --needs a bit of delay to work consistently.
|
|
inventory_refresh(2)
|
|
return true
|
|
end)
|
|
end
|
|
|
|
function inventory_refresh(mode) --safer way to refresh inventory, mode == 1 refreshes sorting.
|
|
if not mode then
|
|
mode = get_config("sort_inv") and 1 or 2
|
|
end
|
|
local inventory = GetActorMenu()
|
|
if actor_menu.get_last_mode() == 0 then return end --don't refresh if UI not open.
|
|
if mode == 1 then
|
|
local sort = nil
|
|
for i=1,#inventory.sort_btn do
|
|
if inventory.sort_btn[i]:GetCheck() then
|
|
sort = i
|
|
end
|
|
end
|
|
inventory:On_Sort(sort or 1,false)
|
|
else
|
|
inventory.update_info = true
|
|
end
|
|
end
|
|
|
|
local ammo_maps = {} --cacheing. this function will get called a lot in combat. ammo_maps gets niled in load_weapon should be enough protect against id recycling and weapon rechamper upgrades
|
|
function get_ammo_map(id)
|
|
if not ammo_maps[id] then
|
|
ammo_maps[id] = invert_table(utils_item.get_ammo(nil, id))
|
|
end
|
|
-- print_dbg("ammo_maps[id][1]:%s ammo_maps[id]:%s", ammo_maps[id] and ammo_maps[id][1], ammo_maps[id])
|
|
return ammo_maps[id]
|
|
end
|
|
|
|
-- return the section of the ammo loaded in weapon
|
|
function get_sec_chambered(wpn)
|
|
local id = wpn:id()
|
|
local index = wpn:get_ammo_type()
|
|
if wpn:get_ammo_in_magazine() > 0 then
|
|
local ammo_map = invert_table(get_ammo_map(id))
|
|
return ammo_map[index + 1]
|
|
else return nil end
|
|
end
|
|
|
|
function retain_round(weapon)
|
|
return get_config("retain_round") and not magazine_binder.is_open_bolt_weapon(weapon)
|
|
end
|
|
|
|
local function se_item_on_unregister(se_obj, typ)
|
|
local id = se_obj.id
|
|
ammo_maps[id] = nil
|
|
end
|
|
|
|
function on_item_drag_dropped(item, weapon, from_slot, to_slot)
|
|
|
|
print_dbg("on_item_drag_dropped " .. item:section() .. " on " .. weapon:section() .. " to_slot " .. (to_slot or "nil"))
|
|
--Only do one thing at a time
|
|
if action_in_progress() then
|
|
do_interrupt()
|
|
return
|
|
end
|
|
-- Check capability
|
|
if not ((from_slot == EDDListType.iDeadBodyBag and to_slot == EDDListType.iDeadBodyBag) or (from_slot == EDDListType.iActorBag and (to_slot == EDDListType.iActorBag or to_slot == EDDListType.iActorSlot))) then
|
|
return
|
|
end
|
|
local active_id = db.actor:active_item() and db.actor:active_item():id() or 0
|
|
local item_id = item:id()
|
|
local item_sec = item:section()
|
|
local weapon_id = weapon:id()
|
|
local weapon_sec = weapon:section()
|
|
if(item_id == weapon_id) then
|
|
return
|
|
end
|
|
|
|
local is_box_mode = (parent_is_box(item) and from_slot == EDDListType.iDeadBodyBag) and true or false
|
|
|
|
-- bullet to mag
|
|
if(IsAmmo(item) and is_magazine(weapon)) then
|
|
-- check compatibility, then fire (heh) the loading loop
|
|
local ammos = invert_table(get_magazine_caliber(weapon))
|
|
local ammo_sec = item_sec
|
|
print_dbg("Checking compat for ammo sec %s", ammo_sec)
|
|
if ammos[ammo_sec] ~= nil then
|
|
-- start loading loop
|
|
local capacity = SYS_GetParam(2, weapon_sec, "max_mag_size") or 20
|
|
local behavior = is_box_mode and get_config("loadstash_behavior") or get_config("load_behavior")
|
|
|
|
if behavior == 1 then
|
|
fast_load_magazine(weapon, item)
|
|
elseif behavior == 2 then
|
|
fast_load_magazine(weapon, item, true)
|
|
else
|
|
local delay = get_mag_time(weapon_sec, false)
|
|
print_dbg("Start load magazine at speed %s", delay)
|
|
ext_ammo_box = item
|
|
action_start_magload()
|
|
RemoveTimeEvent("Mag_redux", "load_mag"..weapon_id)
|
|
create_time_event("Mag_redux", "load_mag"..weapon_id, 0.1, load_magazine, weapon, capacity, delay * 1000)
|
|
end
|
|
end
|
|
end
|
|
-- mag to weapon
|
|
if is_magazine(item) and IsWeapon(weapon) and is_compatible(weapon, item) then
|
|
if weapon:weapon_in_grenade_mode() then return end
|
|
-- init loading of weapon
|
|
print_dbg("Begin loading mag %s into weapon %s", item:section(), weapon:section())
|
|
if weapon_id == active_id then
|
|
local pre_table = count_ammo(weapon)
|
|
weapon:switch_state(7)
|
|
action_start_reload()
|
|
create_time_event("Mag_redux", "delay_weapon"..weapon:id(), 0.1, delay_load_weapon, weapon:id(), item, pre_table)
|
|
else
|
|
eject_magazine(weapon)
|
|
load_weapon(weapon, item)
|
|
end
|
|
end
|
|
|
|
-- bullet to weapon
|
|
if (IsAmmo(item) and IsWeapon(weapon)) then
|
|
chamber_single_round(item, weapon)
|
|
end
|
|
end
|
|
|
|
-- reassign mag data from magazine to weapon
|
|
function load_weapon(weapon, magazine)
|
|
if ACTION_IN_PROGRESS then return end
|
|
if is_magazine(magazine) and IsWeapon(weapon) and is_compatible(weapon, magazine) then
|
|
local wep_id = weapon:id()
|
|
local existing_mag = get_mag_loaded(wep_id)
|
|
if existing_mag then
|
|
print_dbg("Cancel loading. Another mag already in wpn: %s", existing_mag.section or "<unknown>")
|
|
return
|
|
end
|
|
|
|
local mag_id = magazine:id()
|
|
ammo_maps[wep_id] = nil -- nil this here to protect against weapon caliber changes or id recycling.
|
|
|
|
-- check for empty mag and initialize
|
|
local mag_data = get_mag_loaded(mag_id)
|
|
if mag_data then
|
|
if mag_data.section == "mag_ak_5.56x45_default" then mag_data.section = "mag_ak_5.56x45_modern" end
|
|
print_dbg("ammo in weapon existing: %s", weapon:get_ammo_in_magazine())
|
|
if retain_round(weapon) and (weapon:get_ammo_in_magazine() > 0 ) then
|
|
local first_round = get_sec_chambered(weapon)
|
|
print_dbg("extra round of type %s found, adding to mag", first_round)
|
|
stack.push(mag_data.loaded, first_round)
|
|
end
|
|
mag_data.is_weapon = true
|
|
set_mag_data(wep_id, mag_data)
|
|
set_mag_data(mag_id, nil)
|
|
alife_release_id(mag_id)
|
|
prep_weapon(weapon, false)
|
|
do_interrupt_reload()
|
|
end
|
|
end
|
|
end
|
|
|
|
local last_ejected
|
|
-- 0 = no ammo, 1 = one round, checked, 2 = no round, checked
|
|
local save_round = 0
|
|
function delay_load_weapon(id, magazine, pre_table)
|
|
local tg = time_global()
|
|
-- extra timer stuff so we don't let the time event linger
|
|
weapon = get_object_by_id(id)
|
|
if not load_timeout[id] then
|
|
load_timeout[id] = tg
|
|
end
|
|
|
|
local inventory = GetActorMenu()
|
|
local active_id = db.actor:active_item() and db.actor:active_item():id() or 0
|
|
if action_state ~= ACTION_RELOAD or tg > load_timeout[id] + 50000 or (active_id ~= id) or not weapon then
|
|
load_timeout[id] = nil
|
|
do_interrupt()
|
|
if weapon then weapon:switch_state(2) end
|
|
force_reload = false
|
|
last_ejected = nil
|
|
save_round = 0
|
|
print_dbg("Reload interrupted")
|
|
return true
|
|
end
|
|
local mag_data = get_mag_loaded(id)
|
|
if mag_data then
|
|
local has_room = room_in_pouch(magazine:id())
|
|
local ejected = eject_magazine(weapon)
|
|
-- if 3hands or the slots aren't full, store the mag
|
|
if has_room or get_config("three_hands") then
|
|
last_ejected = ejected
|
|
print_dbg("Ejected id %s. Is carried? %s", last_ejected, is_carried_mag(last_ejected))
|
|
end
|
|
end
|
|
|
|
if save_round == 0 then
|
|
save_round = weapon:get_ammo_in_magazine() > 0 and 1 or 2
|
|
end
|
|
if weapon:get_state() == 0 or force_reload then
|
|
weapon:unload_magazine()
|
|
if save_round == 1 then
|
|
weapon:set_ammo_elapsed(1)
|
|
end
|
|
save_round = 0
|
|
ACTION_IN_PROGRESS = false
|
|
load_weapon(weapon, magazine)
|
|
create_time_event("Mag_redux", "refund", 0.1, function()
|
|
refund_ammo(pre_table)
|
|
return true
|
|
end)
|
|
load_timeout[id] = nil
|
|
if last_ejected and not is_carried_mag(last_ejected) and retain_magazine(last_ejected) then
|
|
print_dbg("Toggling %s", last_ejected)
|
|
toggle_carried_mag(last_ejected)
|
|
end
|
|
force_reload = false
|
|
last_ejected = nil
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- put a single round in the chamber
|
|
--only works if they are compatible and weapon is empty
|
|
function chamber_single_round(ammo, weapon)
|
|
print_dbg("do chamber one round of %s", ammo:section())
|
|
if is_supported_weapon(weapon) and retain_round(weapon) and weapon:get_ammo_in_magazine() == 0 then
|
|
local ammo_map = get_ammo_map(weapon:id())
|
|
local typ = ammo_map[ammo:section()]
|
|
if not typ then return end
|
|
print_dbg("chamber single round, ammo is %s, type is %s", ammo:section(), typ - 1)
|
|
weapon:set_ammo_type(typ - 1)
|
|
weapon:set_ammo_elapsed(1)
|
|
utils_obj.play_sound("handgun_unjam")
|
|
if ammo:ammo_get_count() == 1 then
|
|
alife_release_id(ammo:id())
|
|
else
|
|
ammo:ammo_set_count(ammo:ammo_get_count() - 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- When weapon is reloaded in any way, it will subtract a box from player inventory. We don't want this to happen - loading should only happen from magazines. Record the prior amount of ammo before reload and refund it afterwards.
|
|
function count_ammo(weapon)
|
|
local ammo_table = {}
|
|
if type(weapon) == "table" then
|
|
for k, v in pairs(weapon) do
|
|
ammo_table[k] = 0
|
|
end
|
|
else
|
|
local ammo_types = utils_item.get_ammo(weapon:section(), weapon:id())
|
|
for i=1,#ammo_types do
|
|
ammo_table[ammo_types[i]] = 0
|
|
end
|
|
end
|
|
|
|
db.actor:inventory_for_each(function (item)
|
|
local item_section = item:section()
|
|
if ammo_table[item_section] then
|
|
ammo_table[item_section] = ammo_table[item_section] + item:ammo_get_count()
|
|
end
|
|
end)
|
|
return ammo_table
|
|
end
|
|
|
|
-- Refund ammo.
|
|
function refund_ammo(pre_ammo_table)
|
|
print_dbg("Refunding ammo")
|
|
if rax_inventory_highlights_mcm then -- yes i am adding a compatablity patch with my own mod.
|
|
rax_inventory_highlights_mcm.temp_disable() --don't flag this magazine as new.
|
|
end
|
|
local post_ammo_table = count_ammo(pre_ammo_table)
|
|
for ammo_type, count in pairs(pre_ammo_table) do
|
|
local to_refund = count - post_ammo_table[ammo_type]
|
|
if to_refund > 0 then
|
|
alife_create_item(ammo_type, db.actor, {ammo = to_refund})
|
|
-- create_ammo(ammo_type, db.actor:position(), db.actor:level_vertex_id(), db.actor:game_vertex_id(), AC_ID, to_refund)
|
|
end
|
|
end
|
|
end
|
|
|
|
function weapon_in_slot(id)
|
|
local carried_weapons = {}
|
|
for i,_ in pairs(SCANNED_SLOTS) do
|
|
if db.actor:item_in_slot(i) then
|
|
carried_weapons[db.actor:item_in_slot(i):id()] = true
|
|
end
|
|
end
|
|
return carried_weapons[id]
|
|
end
|
|
|
|
-- Determine if this magazine should be retained in loadout, based on MCM options.
|
|
function retain_magazine(id, ejection)
|
|
ejection = ejection or tonumber(get_config("ejection"))
|
|
if ejection == 0 then return true end
|
|
|
|
local mag_data = get_mag_loaded(id)
|
|
if mag_data then
|
|
local capacity = SYS_GetParam(2, mag_data.section, "max_mag_size") or 20
|
|
local ammo_percent = #mag_data.loaded/capacity
|
|
if #mag_data.loaded ~= 0 and ammo_percent > (get_config('mag_unequip_trs')/100) then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local modes = {
|
|
["inventory"] = true,
|
|
["loot"] = true,
|
|
}
|
|
local bags = {
|
|
["actor_bag"] = true,
|
|
["actor_equ"] = true,
|
|
["actor_belt"] = true,
|
|
["npc_bag"] = true,
|
|
}
|
|
-- should only fire on weapons!
|
|
function check_eject_magazine(weapon, bag, mode)
|
|
-- bag check
|
|
if not (bags[bag] and modes[mode]) or not is_supported_weapon(weapon) then return end
|
|
local mag_data = get_mag_loaded(weapon:id())
|
|
if mag_data and not weapon:weapon_in_grenade_mode() then
|
|
return gc("st_mag_eject_magazine")
|
|
end
|
|
end
|
|
|
|
function eject_magazine(weapon)
|
|
if rax_inventory_highlights_mcm then
|
|
rax_inventory_highlights_mcm.temp_disable() --don't flag this magazine as new.
|
|
end
|
|
if weapon:weapon_in_grenade_mode() or not is_supported_weapon(weapon) then return end
|
|
|
|
local id = weapon:id()
|
|
|
|
-- additional validation
|
|
magazine_binder.validate_wep(id)
|
|
|
|
local mag_data = get_mag_loaded(id)
|
|
if mag_data then
|
|
weapon:force_unload_magazine(false)
|
|
if retain_round(weapon) and #mag_data.loaded > 0 then
|
|
print_dbg("retaining round of type %s in chamber", stack.peek(mag_data.loaded))
|
|
stack.pop(mag_data.loaded)
|
|
weapon:set_ammo_elapsed(1)
|
|
end
|
|
|
|
se_mag = alife_create_item(mag_data.section, weapon:parent())
|
|
if se_mag then
|
|
create_time_event("mag_redux", "timer_eject_"..id, 0, timer_eject_magazine, id, se_mag.id, mag_data)
|
|
else
|
|
print_dbg("Could not create magazine %s", mag_data.section)
|
|
end
|
|
set_mag_data(id, {
|
|
section = "no_mag",
|
|
loaded = {},
|
|
is_weapon = true,
|
|
})
|
|
dump_data(mag_data)
|
|
return se_mag.id
|
|
end
|
|
end
|
|
|
|
function timer_eject_magazine(weapon_id, id, mag_data)
|
|
mag_data.is_weapon = false
|
|
set_mag_data(id, mag_data)
|
|
local ejection_type = tonumber(get_config("ejection"))
|
|
print_dbg("ejection type is %s", ejection_type)
|
|
local inv = GetActorMenu()
|
|
local inv_open = inv and inv:IsShown()
|
|
if weapon_in_slot(weapon_id) then
|
|
if retain_magazine(id, ejection_type) or inv_open then
|
|
toggle_carried_mag(id)
|
|
elseif ejection_type == 2 and not inv_open then
|
|
db.actor:drop_item(level.object_by_id(id))
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- locate appropriate mag in vest
|
|
function find_magazine(weapon, ammo_type)
|
|
if not ammo_type then
|
|
ammo_type = utils_item.get_ammo(nil, weapon:id())[weapon:get_ammo_type()+1]
|
|
end
|
|
local carried_mags = {}
|
|
get_carried_mags(carried_mags)
|
|
local largest_id = nil
|
|
local largest_of_type = nil
|
|
for k,v in pairs(carried_mags) do
|
|
print_dbg("checking id %s, type is %s, %s %s rounds loaded", k, v.section, #v.loaded, stack.peek(v.loaded))
|
|
if is_compatible(weapon, v.section) then
|
|
if largest_id == nil or #v.loaded > #carried_mags[largest_id].loaded then
|
|
print_dbg("largest so far is %s", k)
|
|
largest_id = k
|
|
end
|
|
if stack.peek(v.loaded) == ammo_type and (largest_of_type == nil or #v.loaded > #carried_mags[largest_of_type].loaded) then
|
|
print_dbg("largest of type so far is %s", k)
|
|
largest_of_type = k
|
|
end
|
|
|
|
end
|
|
end
|
|
if largest_of_type then
|
|
return level.object_by_id(largest_of_type)
|
|
elseif largest_id then
|
|
return level.object_by_id(largest_id)
|
|
else return nil end
|
|
end
|
|
|
|
--count of magazines by ammo type of first round in vest
|
|
function count_magazines(weapon)
|
|
local carried_mags = {}
|
|
local mag_inv = {}
|
|
|
|
get_carried_mags(carried_mags)
|
|
for k,v in pairs(carried_mags) do
|
|
if is_compatible(weapon, v.section) and #v.loaded >0 then
|
|
local first_round_sec = stack.peek(v.loaded)
|
|
mag_inv[first_round_sec] = (mag_inv[first_round_sec] or 0) + 1
|
|
end
|
|
end
|
|
return mag_inv
|
|
end
|
|
|
|
function actor_on_weapon_reload(actor, weapon, ammo_total)
|
|
|
|
local weapon = db.actor:active_item()
|
|
local id = weapon:id()
|
|
-- cancel reload only if we have a valid mag, do not allow reloads unless weapon is neutral
|
|
local state = weapon and weapon:get_state()
|
|
if not weapon or not is_supported_weapon(weapon) or weapon:weapon_in_grenade_mode() or state == 7 then
|
|
return
|
|
end
|
|
|
|
-- wpo edge case to handle denial of reload on unjam
|
|
if arti_jamming and arti_jamming.get_jammed and arti_jamming.get_jammed(id) then
|
|
--weapon:switch_state(2) -- no need to change state since reload event is suppressed
|
|
return
|
|
end
|
|
|
|
if is_jammed_weapon(weapon) then
|
|
weapon_unjammed(weapon)
|
|
unjam_weapon(weapon)
|
|
return
|
|
end
|
|
|
|
-- find appropriate magazine
|
|
local mag = find_magazine(weapon)
|
|
-- do the reload
|
|
if not mag then
|
|
print_dbg("No ready mags found, not reloading")
|
|
do_interrupt()
|
|
--[[ create_time_event("Mag_redux", "cancel_reload"..id, 0.1, function (weapon)
|
|
-- weapon:switch_state(2)
|
|
-- return true
|
|
-- end, weapon)
|
|
no need to change state since reload event is suppressed
|
|
--]]
|
|
return
|
|
end
|
|
|
|
local pre_table = count_ammo(weapon)
|
|
weapon:switch_state(7)
|
|
action_start_reload()
|
|
SendScriptCallback("actor_on_weapon_reload", weapon, 0)
|
|
-- note to self - setting delay as 0.1 to fix ghost reload
|
|
create_time_event("Mag_redux", "delay_weapon"..id, 0.1, delay_load_weapon, id, mag, pre_table)
|
|
|
|
end
|
|
|
|
function actor_on_update()
|
|
FADArun = enhanced_animations and enhanced_animations.used_item and true or false
|
|
if action_in_progress() and FADArun then
|
|
do_interrupt()
|
|
end
|
|
end
|
|
|
|
local timeout = nil
|
|
function unjam_weapon(weapon)
|
|
timeout = time_global()
|
|
local id = weapon:id()
|
|
local data = get_mag_data(id)
|
|
local slot = db.actor:active_slot()
|
|
db.actor:activate_slot(0)
|
|
local new_id = id
|
|
local replace = false
|
|
if not pcall(function(wpn)
|
|
wpn:cast_Weapon():SetMisfire(false)
|
|
end, weapon) then
|
|
-- replace
|
|
local old_weapon = alife_object(id)
|
|
local new_weapon = alife_clone_weapon(old_weapon)
|
|
new_id = new_weapon.id
|
|
set_mag_data(id, nil)
|
|
replace = true
|
|
end
|
|
SendScriptCallback("actor_on_weapon_reload", weapon, 0)
|
|
weapon_unjammed(weapon)
|
|
print_dbg("New weapon should have %s ammo, time global=%s", data and #data.loaded or "no", timeout)
|
|
create_time_event("Mag_redux", "unjam_weapon", 0, unjam_weapon_timer, new_id, data, slot, replace)
|
|
end
|
|
|
|
function unjam_weapon_timer(weapon_id, data, slot, replace)
|
|
-- keep rolling while weapon is being drawn, if five seconds total elapse do logic regardless
|
|
local current_weapon = db.actor:active_item()
|
|
if (current_weapon and current_weapon:id() ~= weapon_id) and time_global() < timeout + 5000 then return false end
|
|
print_dbg("Unjamming weapon, timeout=%s", timeout)
|
|
utils_obj.play_sound("handgun_unjam")
|
|
if replace then
|
|
local weapon = level.object_by_id(weapon_id)
|
|
print_dbg("ujwt weapon:%s", weapon and weapon:name())
|
|
set_mag_data(weapon_id, nil)
|
|
set_mag_data(weapon_id, data)
|
|
prep_weapon(weapon, false)
|
|
if not current_weapon then
|
|
db.actor:move_to_slot(weapon, slot)
|
|
end
|
|
end
|
|
db.actor:activate_slot(slot)
|
|
return true
|
|
end
|
|
-----------------------------------------
|
|
-- SECTION rounds un/loading functions --
|
|
-----------------------------------------
|
|
-- iterate inventory and return the next box for this ammo type. return nil if nothing found
|
|
function find_next_box(ammo_section, id)
|
|
local item_to_get = nil
|
|
function grab_inv_items(npc, item)
|
|
if IsAmmo(item) then
|
|
if ammo_section == item:section() and item:id() ~= id then
|
|
item_to_get = item
|
|
end
|
|
end
|
|
end
|
|
db.actor:iterate_inventory(grab_inv_items)
|
|
return item_to_get
|
|
end
|
|
|
|
|
|
local last_time = 0
|
|
-- position is ammo type as number
|
|
function load_magazine(magazine, capacity, delay)
|
|
|
|
local tg = time_global()
|
|
if tg < last_time + delay then return false end
|
|
if IsMoveState("mcAnyMove") and tg < last_time + (3 * delay) then return false end
|
|
last_time = tg
|
|
|
|
print_dbg("Try load magazine, Action state: %s", action_state)
|
|
if action_state ~= ACTION_LOAD_MAG or ext_ammo_box == nil then
|
|
print_dbg("ending load prematurely. is ammo box nil? %s", ext_ammo_box == nil)
|
|
do_interrupt()
|
|
inventory_refresh()
|
|
last_time = 0
|
|
return true
|
|
end
|
|
|
|
local id = magazine:id()
|
|
local mag_data = get_mag_data(id)
|
|
if (#mag_data.loaded >= capacity) then
|
|
print_dbg("ending load with full magazine")
|
|
inventory_refresh()
|
|
last_time = 0
|
|
ext_ammo_box = nil
|
|
do_interrupt()
|
|
return true
|
|
end
|
|
local num_bullets = ext_ammo_box:ammo_get_count()
|
|
local ammo_section = ext_ammo_box:section()
|
|
if num_bullets == 1 then
|
|
print_dbg("Searching for next box")
|
|
local ext_id = ext_ammo_box:id()
|
|
new_ammo_box = find_next_box(ammo_section, ext_id)
|
|
alife_release_id(ext_id)
|
|
ext_ammo_box = new_ammo_box
|
|
else
|
|
ext_ammo_box:ammo_set_count(num_bullets - 1)
|
|
end
|
|
stack.push(mag_data.loaded, ammo_section)
|
|
set_mag_data(id, mag_data)
|
|
print_dbg("Loaded 1 round of type %s. There are %s rounds loaded total", ammo_section, #mag_data.loaded)
|
|
utils_obj.play_sound("magazines\\ammo_load"..math.random(1, 7))
|
|
inventory_refresh(2)
|
|
return false
|
|
end
|
|
|
|
function unload_magazine(magazine, delay)
|
|
|
|
local tg = time_global()
|
|
if tg < last_time + delay then return false end
|
|
if IsMoveState("mcAnyMove") and tg < last_time + (3 * delay) then return false end
|
|
|
|
last_time = tg
|
|
|
|
local id = magazine:id()
|
|
local mag_data = get_mag_data(id)
|
|
-- note to self: create_ammo does not trigger item on move callback, hence cannot unload into corpse or box
|
|
local mag_parent = db.actor--magazine:parent() and magazine:parent() or db.actor
|
|
if not mag_data or #mag_data.loaded == 0 or action_state ~= ACTION_LOAD_MAG then
|
|
print_dbg("ending unload prematurely, state=%s", action_state)
|
|
do_interrupt()
|
|
inventory_refresh()
|
|
-- create_time_event("mag_redux", "box_ammo_aggregation", 0, item_weapon.ammo_aggregation_full, mag_parent:id())
|
|
last_time = 0
|
|
return true
|
|
end
|
|
|
|
local round = stack.pop(mag_data.loaded)
|
|
set_mag_data(id, mag_data)
|
|
if round ~= nil then
|
|
print_dbg("Unloaded 1 round of type %s. There are %s rounds left.", round, #mag_data.loaded)
|
|
create_ammo(round, mag_parent:position(), mag_parent:level_vertex_id(), mag_parent:game_vertex_id(), mag_parent:id(), 1)
|
|
utils_obj.play_sound("magazines\\ammo_load"..math.random(1, 7))
|
|
else
|
|
print_dbg("Mag empty, returning")
|
|
-- create_time_event("mag_redux", "box_ammo_aggregation", 0, item_weapon.ammo_aggregation_full, mag_parent:id())
|
|
inventory_refresh()
|
|
last_time = 0
|
|
do_interrupt()
|
|
return true
|
|
end
|
|
end
|
|
|
|
function fast_load_magazine(magazine, box, load_one)
|
|
local mag_data = get_mag_data(magazine:id())
|
|
print_dbg("begin fastload")
|
|
dump_data(mag_data)
|
|
local ammo_sec = box:section()
|
|
local parent = box:parent()
|
|
local ammo_to_load = SYS_GetParam(2, magazine:section(), "max_mag_size") - #mag_data.loaded
|
|
local boxes_to_load = {}
|
|
local count_so_far = 0
|
|
local function itr_box(temp, item)
|
|
if item:section() == ammo_sec and count_so_far < ammo_to_load then
|
|
boxes_to_load[item:id()] = true
|
|
count_so_far = count_so_far + item:ammo_get_count()
|
|
end
|
|
end
|
|
|
|
if load_one then
|
|
boxes_to_load[box:id()] = true
|
|
elseif IsInvbox(parent) then
|
|
parent:iterate_inventory_box(itr_box)
|
|
else
|
|
parent:iterate_inventory(itr_box)
|
|
end
|
|
|
|
for k,v in pairs(boxes_to_load) do
|
|
local ammo_box = level.object_by_id(k)
|
|
local loaded = ammo_box:ammo_get_count() > ammo_to_load and ammo_to_load or ammo_box:ammo_get_count()
|
|
local release_box = loaded == ammo_box:ammo_get_count()
|
|
for i=1,loaded do
|
|
stack.push(mag_data.loaded, ammo_sec)
|
|
end
|
|
|
|
if release_box then
|
|
ammo_to_load = ammo_to_load - ammo_box:ammo_get_count()
|
|
alife_release_id(k)
|
|
else
|
|
ammo_box:ammo_set_count(ammo_box:ammo_get_count() - loaded)
|
|
end
|
|
end
|
|
set_mag_data(magazine:id(), mag_data)
|
|
xr_sound.set_sound_play(AC_ID,"reload_shell")
|
|
timed_refresh()
|
|
end
|
|
|
|
function fast_unload_magazine(magazine)
|
|
local ammo_to_make = {}
|
|
local mag_data = get_mag_data(magazine:id())
|
|
while #mag_data.loaded > 0 do
|
|
local round = stack.pop(mag_data.loaded)
|
|
if not ammo_to_make[round] then
|
|
ammo_to_make[round] = 1
|
|
else
|
|
ammo_to_make[round] = ammo_to_make[round] + 1
|
|
end
|
|
end
|
|
set_mag_data(magazine:id(), mag_data)
|
|
local to_spawn = magazine:parent() and magazine:parent() or db.actor
|
|
for k,v in pairs(ammo_to_make) do
|
|
create_ammo(k, to_spawn:position(), to_spawn:level_vertex_id(), to_spawn:game_vertex_id(), to_spawn:id(), v)
|
|
create_time_event("ammo", "agg_"..magazine:id(), 0, function()
|
|
item_weapon.ammo_aggregation_full(to_spawn)
|
|
return true
|
|
end)
|
|
end
|
|
timed_refresh()
|
|
end
|
|
|
|
local load_cancel = {
|
|
[key_bindings.kQUIT] = true,
|
|
[key_bindings.kINVENTORY] = true
|
|
|
|
}
|
|
function on_key_press(key, bind, dis, flags)
|
|
-- do not interrupt if directional key pressed
|
|
if action_state ~= ACTION_NONE and (load_cancel[bind] or key == DIK_keys.DIK_LMENU) then
|
|
print_dbg("Interrupting mag loading")
|
|
do_interrupt_magload()
|
|
end
|
|
if bind == key_bindings.kWPN_RELOAD then
|
|
|
|
local current_weapon = db.actor:item_in_slot(db.actor:active_slot())
|
|
if current_weapon and is_supported_weapon(current_weapon) then
|
|
if is_jammed_weapon(current_weapon) then
|
|
|
|
local hud_section = SYS_GetParam(0, current_weapon:section(), "hud")
|
|
if SYS_GetParam(0, hud_section, "anm_reload_misfire") ~= nil then
|
|
weapon_unjammed(current_weapon)
|
|
return -- let the reload event thru so animation will unjam
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
actor_on_weapon_reload(db.actor, current_weapon)
|
|
flags.ret_value = false
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
function unload_ammo(obj)
|
|
if is_magazine(obj) then
|
|
local id = obj:id()
|
|
local mag_data = get_mag_data(id)
|
|
if mag_data and #mag_data.loaded > 0 then
|
|
return gc("st_mag_unload_ammo")
|
|
end
|
|
end
|
|
end
|
|
|
|
function func_unload_ammo(obj)
|
|
if not is_magazine(obj) then return end
|
|
local id = obj:id()
|
|
local mag_data = get_mag_data(id)
|
|
if not mag_data or #mag_data.loaded == 0 then return end
|
|
|
|
if parent_is_box(obj) or get_config("unload_behavior") == 1 then--get_config("fast_loading") and parent_is_box(obj) then
|
|
fast_unload_magazine(obj)
|
|
else
|
|
local delay = get_mag_time(obj:section(), true)
|
|
print_dbg("Start unload magazine at speed %s", delay)
|
|
action_start_magload()
|
|
RemoveTimeEvent("mag_redux", "unload_mag"..id)
|
|
create_time_event("mag_redux","unload_mag"..id, 0.1, unload_magazine, obj, delay * 1000)
|
|
end
|
|
|
|
end
|
|
|
|
function loadout_precondition(obj)
|
|
if is_magazine(obj) then
|
|
local id = obj:id()
|
|
if is_carried_mag(id) then
|
|
return game.translate_string("st_mag_loadout_remove")
|
|
elseif room_in_pouch(id) then
|
|
return game.translate_string("st_mag_loadout_add")
|
|
end
|
|
end
|
|
end
|
|
|
|
function loadout_func(obj)
|
|
if is_magazine(obj) then
|
|
local id = obj:id()
|
|
toggle_carried_mag(id)
|
|
inventory_refresh()
|
|
mag_hud.HUD:Update(true)
|
|
end
|
|
end
|
|
|
|
function retool_mag_precondition(obj)
|
|
if is_magazine(obj) and get_retool_section(obj) then
|
|
return game.translate_string("st_mag_retool")
|
|
end
|
|
end
|
|
|
|
function retool_mag_functor(obj)
|
|
local mag_data = get_mag_data(obj:id())
|
|
local tool = db.actor:object("leatherman_tool")
|
|
local retool_section = get_retool_section(obj)
|
|
if #mag_data.loaded > 0 then
|
|
actor_menu.set_item_news("fail", "detail", "st_mag_err_retool") --message to unload mag first
|
|
elseif not tool then
|
|
actor_menu.set_item_news('fail', 'weapon', "st_dis_text_3", " ")
|
|
else
|
|
new_obj = alife_create_item(retool_section, obj:parent())
|
|
if new_obj then
|
|
alife_release(obj)
|
|
utils_item.degrade( tool , 0) --change this
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-----------------------------------------
|
|
-- SECTION weapon logic -----------------
|
|
-----------------------------------------
|
|
|
|
function actor_on_weapon_fired(obj, weapon, ammo_elapsed, grenade_elapsed, ammo_type, grenade_type)
|
|
weapon_unjammed(weapon) --this is only needed here for automatic shotguns, because they do not trigger the reload callback.
|
|
|
|
if not is_supported_weapon(weapon) or weapon:weapon_in_grenade_mode() then
|
|
return
|
|
end
|
|
prep_weapon(weapon, true)
|
|
end
|
|
|
|
function weapon_unjammed(weapon)
|
|
local weapon_id = weapon:id()
|
|
if jammed_guns[weapon_id] then
|
|
print_dbg("Removing jammed weapon entry %s", weapon_id)
|
|
jammed_guns[weapon_id] = nil
|
|
end
|
|
end
|
|
|
|
function weapon_jammed(weapon)
|
|
if arti_jamming then
|
|
print_dbg("WPO detected, not jamming")
|
|
return
|
|
end
|
|
print_dbg("Logging jammed weapon %s", weapon:id())
|
|
jammed_guns[weapon:id()] = true
|
|
end
|
|
|
|
function is_jammed_weapon(weapon)
|
|
return jammed_guns[weapon:id()] or false
|
|
end
|
|
|
|
-- Load next round into the weapon.
|
|
-- feed_next removes top round and feeds one after. otherwise feeds current
|
|
function prep_weapon(weapon, feed_next)
|
|
local id = weapon:id()
|
|
local mag_data = get_mag_loaded(id)
|
|
local ammo_map = get_ammo_map(id) --cacheing function should keep this from being too laggy, got to look out for the potatos
|
|
|
|
if mag_data then
|
|
local count = #mag_data.loaded
|
|
if feed_next then
|
|
stack.pop(mag_data.loaded)
|
|
end
|
|
local round = stack.peek(mag_data.loaded)
|
|
print_dbg("chambering round of type %s, there are %s rounds loaded, ammo in mag is %s", round, count, weapon:get_ammo_in_magazine())
|
|
if round ~= nil and ammo_map[round] then
|
|
weapon:set_ammo_type(ammo_map[round]-1)
|
|
weapon:set_ammo_elapsed(count)
|
|
create_time_event("mag_redux","prep_weapon"..id, 0, function() local x = weapon:get_ammo_in_magazine() weapon:set_ammo_elapsed(0) weapon:set_ammo_elapsed(x) return true end )
|
|
elseif count ~= 0 then
|
|
weapon:set_ammo_elapsed(1)
|
|
print_dbg("mag has invalid ammo. If gun is not SVT this is a problem with the mag caliber entry")
|
|
else
|
|
print_dbg("mag empty")
|
|
end
|
|
set_mag_data(id, mag_data)
|
|
end
|
|
end
|
|
|
|
-- Reload mid animation with motion marks
|
|
-- Like vanilla, ignore name and force mag load on first motion mark
|
|
function actor_on_hud_animation_mark(state, mark, hud_item, owner)
|
|
if action_in_progress() and state == 7 and (owner and owner:id() == AC_ID or false) then
|
|
force_reload = true
|
|
end
|
|
end
|
|
|
|
-----------------------------------------
|
|
-- SECTION UI callbacks -----------------
|
|
-----------------------------------------
|
|
local focus_last_sec
|
|
local focus_last_id
|
|
local focus_tbl = {}
|
|
|
|
|
|
function ActorMenu_on_item_focus_receive(obj) -- highlight compatible items
|
|
|
|
local sec_focus = obj:section()
|
|
local id = obj:id()
|
|
empty_table(focus_tbl)
|
|
if (is_supported_weapon(obj) and id ~= focus_last_id) then
|
|
focus_tbl = get_mags_for_basetype(get_weapon_base_type(obj))
|
|
|
|
elseif (is_magazine(sec_focus) and sec_focus ~= focus_last_sec) then
|
|
focus_tbl = get_magazine_caliber(sec_focus)
|
|
elseif (IsAmmo(obj) and sec_focus ~= focus_last_sec) then
|
|
focus_tbl = get_mags_by_ammo_type(sec_focus)
|
|
end
|
|
|
|
local inventory = GetActorMenu()
|
|
if not focus_tbl or not ((#focus_tbl > 0) or (inventory and inventory:IsShown())) then
|
|
return
|
|
end
|
|
|
|
for i=1,#focus_tbl do
|
|
inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iActorBag)
|
|
inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iPartnerTradeBag)
|
|
inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iDeadBodyBag)
|
|
inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iActorTrade)
|
|
inventory:highlight_section_in_slot(focus_tbl[i],EDDListType.iPartnerTrade)
|
|
end
|
|
end
|
|
|
|
|
|
function ActorMenu_on_item_before_move(flags, npc_id, obj, mode, bag)
|
|
flags.ret_value = flags.ret_value and not is_carried_mag(obj and obj:id() or 0)
|
|
end
|
|
|
|
function on_game_load()
|
|
-- reset action state
|
|
do_interrupt()
|
|
-- purge any errant time events
|
|
for k,v in pairs(time_event_cache) do
|
|
args = str_explode(k, ",")
|
|
RemoveTimeEvent(args[1], args[2])
|
|
end
|
|
end
|
|
|
|
function on_game_start()
|
|
on_option_change()
|
|
custom_functor_autoinject.add_functor("mags_load", check_eject_magazine, check_eject_magazine, nil, eject_magazine, true)
|
|
|
|
RegisterScriptCallback("ActorMenu_on_item_before_move", ActorMenu_on_item_before_move)
|
|
RegisterScriptCallback("ActorMenu_on_item_focus_receive", ActorMenu_on_item_focus_receive)
|
|
-- RegisterScriptCallback("on_key_press", on_key_press)
|
|
RegisterScriptCallback("on_before_key_press", on_key_press)
|
|
|
|
RegisterScriptCallback("actor_on_weapon_fired", actor_on_weapon_fired)
|
|
RegisterScriptCallback("actor_on_weapon_jammed", weapon_jammed)
|
|
RegisterScriptCallback("on_game_load", on_game_load)
|
|
RegisterScriptCallback("ActorMenu_on_item_drag_drop", on_item_drag_dropped)
|
|
RegisterScriptCallback("server_entity_on_unregister",se_item_on_unregister)
|
|
RegisterScriptCallback("on_option_change",on_option_change)
|
|
RegisterScriptCallback("actor_on_update", actor_on_update)
|
|
RegisterScriptCallback("actor_on_hud_animation_mark",actor_on_hud_animation_mark)
|
|
on_option_change()
|
|
|
|
end
|