local mcm_key_reload = DIK_keys.DIK_R local mcm_modifier_unjam = 1 local mcm_doubletap_behavior = "situational" local mcm_check_ammo_after_reload = false function on_game_start() RegisterScriptCallback("actor_on_first_update", actor_on_first_update) RegisterScriptCallback("on_option_change", on_option_change) RegisterScriptCallback("on_before_key_press", on_before_key_press) RegisterScriptCallback("on_key_hold", on_key_hold) end function on_mcm_load() op = { id = "reload", sh = true, gr = { {id = "slide_onekey_reload", type = "slide", link = "ui_options_slider_player", text = "ui_mcm_title_onekey_reload", size = {512, 50}, spacing = 20}, {id = "desc_onekey_reload", type = "desc", text = "ui_mcm_onekey_key_reload_desc" }, {id = "key_reload", type = "key_bind", val = 2, def = DIK_keys.DIK_R, hint = "onekey_key_reload" }, {id = "modifier_unjam", type = "radio_h", val = 2, def = 1, hint = "onekey_unjam_modifier", content = {{1,"mcm_kb_mod_shift"} , {2,"mcm_kb_mod_ctrl"}, {3,"mcm_kb_mod_alt"}}}, {id = "doubletap_behavior", type = "radio_h", val = 0, def = "situational", hint = "onekey_doubletap_behavior", content = {{"reload", "dt_reload"}, {"next_ammo_type", "dt_next_ammo_type"}, {"ammo_wheel", "dt_ammo_wheel"}, {"situational", "dt_situational"}}}, {id = "check_ammo_after_reload", type = "check", val = 1, def = false, hint = "onekey_check_ammo_after_reload"}, } } return op, "onekey" end function actor_on_first_update() on_option_change() end function on_option_change() if ui_mcm then mcm_key_reload = ui_mcm.get("onekey/reload/key_reload") mcm_modifier_unjam = ui_mcm.get("onekey/reload/modifier_unjam") mcm_doubletap_behavior = ui_mcm.get("onekey/reload/doubletap_behavior") mcm_check_ammo_after_reload = ui_mcm.get("onekey/reload/check_ammo_after_reload") end local show = false local pos = ActorMenu.get_maingame().m_ui_hud_states.m_ui_weapon_cur_ammo:GetWndPos() pos.x = ((not show and pos.x > 0) or (show and pos.x < 0)) and (-1 * pos.x) or pos.x ActorMenu.get_maingame().m_ui_hud_states.m_ui_weapon_cur_ammo:SetWndPos(pos) end function on_before_key_press(key, bind, dis, flags) if bind == key_bindings.kWPN_RELOAD or key == mcm_key_reload then -- passthrough if no weapon active local weapon = db.actor:active_item() if (weapon == nil or (not IsWeapon(weapon)) or IsItem("fake_ammo_wpn", nil, weapon)) then flags.ret_value = true return end if is_ammo_wheel_open() or weapon:get_state() ~= 0 then flags.ret_value = true return end -- Unjam or reload depending on modifier if ui_mcm.get_mod_key(mcm_modifier_unjam) then ui_mcm.simple_press("magazines_onekey_press_unjam", key, try_unjam_weapon) else ui_mcm.simple_press("magazines_onekey_press_reload", key, try_reload_weapon) end -- Double-tap to ammo-select if ui_mcm.double_tap("magazines_onekey_doubletap_changeammo", key) then -- printf("[OKR] doubletap: behavior = %s", mcm_doubletap_behavior) if mcm_doubletap_behavior == "situational" then _, _, num_avail = get_ammo_data(weapon, true) -- printf("[OKR] doubletap: behavior = %s, num_avail == %s", mcm_doubletap_behavior, num_avail) if num_avail > 2 then switch_ammo_wheel() elseif num_avail == 2 then switch_ammo_next() else try_reload_weapon() end elseif mcm_doubletap_behavior == "ammo_wheel" then switch_ammo_wheel() elseif mcm_doubletap_behavior == "next_ammo_type" then switch_ammo_next() else -- reload try_reload_weapon() end end -- Suppress the keypress since it has been handled. flags.ret_value = false end end function on_key_hold(key) local bind = dik_to_bind(key) -- Hold reload key to ammo check if ((bind == key_bindings.kWPN_RELOAD or key == mcm_key_reload) and ui_mcm.key_hold("magazines_onekey_hold", key)) then ammo_check_mcm.check_Ammo() end end function try_unjam_weapon() local weapon = db.actor:item_in_slot(db.actor:active_slot()) if weapon then arti_jamming.unjam(weapon) end end function try_reload_weapon() local weapon = db.actor:item_in_slot(db.actor:active_slot()) -- Don't reload unless weapon is in idle state local state = weapon and weapon:get_state() if not weapon or weapon:weapon_in_grenade_mode() or state ~= 0 then return end if magazine_binder.is_supported_weapon(weapon) then magazines.actor_on_weapon_reload(db.actor, weapon) else db.actor:reload_weapon() end if mcm_check_ammo_after_reload then if (not magazine_binder.is_supported_weapon(weapon)) and (weapon:get_ammo_in_magazine() >= SYS_GetParam(2, weapon:section(), "ammo_mag_size")) then -- Do not ammo check for non-magazined fully loaded weapons (shotguns, revolvers, bolt actions, etc) since those won't be reloaded return end CreateTimeEvent("onekey_reload", "ammo_check_on_reload", 0.1, ammo_check_mcm.check_ammo_after_reload) end end -- Ammo type select function switch_ammo_wheel() item_weapon.start_ammo_wheel() end function is_ammo_wheel_open() return (item_weapon.GUI and item_weapon.GUI:IsShown()) end function switch_ammo_next() local success = try_switch_next_ammo() if not success then try_reload_weapon() end end function try_switch_next_ammo() local weapon = db.actor:active_item() if weapon and IsWeapon(weapon) and (not IsItem("fake_ammo_wpn", nil, weapon)) then local next_ammo_type local ammo_types local has_ammo_types ammo_types, has_ammo_types = get_ammo_data(weapon, false) local current_ammo_type = get_current_ammo_type(weapon, ammo_types) -- a number local ammo_map = invert_table(magazines.get_ammo_map(weapon:id())) local success = false -- printf("[OKR] current_ammo_type[%s] = %s", current_ammo_type, ammo_map[current_ammo_type]) -- Find the next available ammo type for i = current_ammo_type + 1, #ammo_types do -- +1 because we need next type if has_ammo_types[i] then next_ammo_type = i break end end -- Loop around and find the next available ammo type if not next_ammo_type then for i = 1, current_ammo_type - 1 do if has_ammo_types[i] then next_ammo_type = i break end end end if next_ammo_type then -- printf("[OKR] found next_ammo_type[%s] = %s", next_ammo_type, ammo_map[next_ammo_type]) success = try_switch_to_ammo_type(weapon, next_ammo_type) end return success end end function try_switch_to_ammo_type(weapon, ammo_type) if magazines and magazine_binder.is_supported_weapon(weapon) then local ammo_map = utils_item.get_ammo(nil, weapon:id()) local magazine = magazines.find_magazine(weapon, ammo_map[ammo_type]) if magazines.get_mag_data(weapon:id()) then magazines.eject_magazine(weapon) end local pre_table = magazines.count_ammo(weapon) local first_round = nil if magazines.retain_round(weapon) and weapon:get_ammo_in_magazine() > 0 then first_round = magazines.get_sec_chambered(weapon) end weapon:switch_state(7) disable_info("sleep_active") magazines.action_start_reload() CreateTimeEvent("mag_redux", "delay_weapon"..weapon:id(), 0.1, magazines.delay_load_weapon, weapon, magazine, pre_table, first_round) else weapon:unload_magazine(true) weapon:set_ammo_type(ammo_type - 1) -- ammotype is zero-indexed db.actor:reload_weapon() end if mcm_check_ammo_after_reload then CreateTimeEvent("magazines_onekey_reload", "ammo_check_on_reload", 0.2, ammo_check_mcm.check_ammo_after_reload) end return true end -- Cache the available ammo types for each weapon local cached_ammo_types = {} function get_ammo_data(weapon, include_loaded) local id = weapon:id() local section = weapon:section() -- Find all magazines (or loose ammo for unsupported weapons) in player inventory local inventory_ammo = {} if magazines and magazine_binder.is_supported_weapon(weapon) then inventory_ammo = magazines.count_magazines(weapon) else local function itr(temp, itm) local section = itm:section() if IsItem("ammo",section) or IsItem("grenade_ammo",section) then inventory_ammo[section] = (inventory_ammo[section] or 0) + itm:ammo_get_count() end end db.actor:iterate_inventory(itr, nil) end if include_loaded then local loaded_section = get_loaded_ammo_section(weapon) if loaded_section then inventory_ammo[loaded_section] = (inventory_ammo[loaded_section] or 0) + 1 -- printf("[OKR] Loaded ammo is section %s, count is now %s", loaded_section, inventory_ammo[loaded_section]) end end -- Fetch usable ammo types from config local ammo_types if (not cached_ammo_types[id]) then ammo_types = utils_item.get_ammo(section, id) cached_ammo_types[id] = ammo_types else ammo_types = cached_ammo_types[id] end -- Check if ammo is in inventory (ignoring old ammo) num_avail = 0 local has_ammo_types = {} for i = 1, #ammo_types do local section = ammo_types[i] local is_bad_ammo_type = section and string.find(section, "verybad") and true or false has_ammo_types[i] = (not is_bad_ammo_type) and (inventory_ammo[section] and (inventory_ammo[section] > 0)) and true or false if has_ammo_types[i] then num_avail = num_avail + 1 end end return ammo_types, has_ammo_types, num_avail end -- Return top round type (one-based index) in the magazine (ignoring the chambered round) function get_current_ammo_type(weapon, ammo_types) local current_ammo_type if magazines and magazine_binder.is_supported_weapon(weapon) then local mag_data = magazine_binder.get_mag_loaded(weapon:id()) -- No mag or empty mag: use the round in chamber (or the last one fired if none chambered) if not mag_data or weapon:get_ammo_in_magazine() <= 1 then current_ammo_type = weapon:get_ammo_type() + 1 -- convert to one-based index else local top_round -- one-chambered, peek below to check top round in mag if magazines.retain_round(weapon) then top_round = mag_data.loaded[#mag_data.loaded - 1] -- open bolt: check top round in mag else top_round = stack.peek(mag_data.loaded) end current_ammo_type = weapon:get_ammo_type() + 1 -- fallback if the loop below did not find anything for i, sec in ipairs(ammo_types) do if top_round == sec then current_ammo_type = i -- keep as one-based break end end end else current_ammo_type = weapon:get_ammo_type() + 1 end return current_ammo_type end -- Return top round section (string) function get_loaded_ammo_section(weapon) local section if weapon:get_ammo_in_magazine() == 0 then section = nil return section end if magazines and magazine_binder.is_supported_weapon(weapon) then local mag_data = magazine_binder.get_mag_loaded(weapon:id()) -- No mag or empty mag: use the round in chamber (or the last one fired if none chambered) if not mag_data or (weapon:get_ammo_in_magazine() <= 1 and magazines.retain_round(weapon)) then section = nil else local top_round -- one-chambered, peek below to check top round in mag if magazines.retain_round(weapon) then top_round = mag_data.loaded[#mag_data.loaded - 1] -- open bolt: check top round in mag else top_round = stack.peek(mag_data.loaded) end section = top_round end else local ammo_map = invert_table(magazines.get_ammo_map(weapon:id())) section = ammo_map[weapon:get_ammo_type() + 1] end return section end