---------------------------------------------------------------- -- 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 "") 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