433 lines
13 KiB
Plaintext
433 lines
13 KiB
Plaintext
--
|
|
-- ToggleScope v1.9.1
|
|
-- Last modified: 2022.08.19
|
|
-- https://github.com/Ishmaeel/toggle-scope
|
|
--
|
|
-- Special thanks:
|
|
-- RazorShultz, RavenAscendant, Big Angry Negro, SparksTheUnicorn, Lucy.xr
|
|
--
|
|
local WEAPON_INFINITE_QUEUE = -1
|
|
local SECONDARY_WEAPON_SLOT = 2
|
|
|
|
local switch_info = nil
|
|
local is_secondary = nil
|
|
local scopes_table = utils_data.collect_sections(ini_sys, { "addons_table" })
|
|
local scope_mappings = {}
|
|
|
|
local attach_scope_original = nil
|
|
local detach_scope_original = nil
|
|
|
|
-- because GetFireMode and SetFireMode speak different languages:
|
|
local mode_mappings = {
|
|
[1] = { value = 0, name = "single" },
|
|
[2] = { value = 1, name = "burst" }, -- abakan
|
|
[3] = { value = 1, name = "burst" },
|
|
[-1] = { value = 2, name = "auto" }
|
|
}
|
|
|
|
function switch_scope()
|
|
printp("#", "start.")
|
|
|
|
if switch_info then
|
|
switch_info.cycle_requested = switch_info.has_other_scopes
|
|
log("busy.")
|
|
return
|
|
end
|
|
|
|
local the_weapon = db.actor:active_item()
|
|
if not the_weapon then
|
|
reset_state("no weapon.")
|
|
return
|
|
end
|
|
|
|
local cWeapon = the_weapon:cast_Weapon()
|
|
if not cWeapon then
|
|
reset_state("not a weapon.")
|
|
return
|
|
end
|
|
|
|
switch_info = create_switch_info(the_weapon, cWeapon)
|
|
|
|
log("active weapon: %s (#%s) parent: %s", switch_info.weapon_section, switch_info.weapon_id, switch_info.parent_section)
|
|
|
|
if switch_info.weapon_section == switch_info.parent_section or switch_info.force_attach then
|
|
attach_valid_scope()
|
|
elseif switch_info.parent_section or switch_info.force_detach then
|
|
detach_current_scope()
|
|
else
|
|
reset_state("nothing to do.")
|
|
end
|
|
end
|
|
|
|
function attach_valid_scope()
|
|
log_trace("attach_valid_scope")
|
|
|
|
local valid_scopes = switch_info.valid_scopes
|
|
local preferred_index
|
|
|
|
log("there are %s scopes for %s", table.getn(valid_scopes), switch_info.weapon_section)
|
|
|
|
if switch_info.cycle_requested and switch_info.current_scope then
|
|
preferred_index = find_next_scope(valid_scopes, switch_info.current_scope)
|
|
else
|
|
preferred_index = find_preferred_scope(valid_scopes, scope_mappings[switch_info.weapon_section])
|
|
end
|
|
|
|
if not preferred_index then
|
|
preferred_index = find_first_scope(valid_scopes)
|
|
end
|
|
|
|
if preferred_index then
|
|
local scope_name = valid_scopes[preferred_index]
|
|
local the_scope = db.actor:object(scope_name)
|
|
|
|
if (the_scope) then
|
|
log("attaching %s...", scope_name)
|
|
switch_info.scope_id = the_scope:id()
|
|
else
|
|
log("scope %s not found in inventory.", scope_name)
|
|
end
|
|
end
|
|
|
|
if switch_info.scope_id then
|
|
if not switch_info.cycle_requested then
|
|
db.actor:hide_weapon()
|
|
end
|
|
CreateTimeEvent(0, "attach_delayed", 1, attach_scope_callback)
|
|
else
|
|
reset_state("scope not found.")
|
|
end
|
|
end
|
|
|
|
function attach_scope_callback()
|
|
log_trace("attach_scope_callback")
|
|
|
|
RemoveTimeEvent(0, "attach_delayed")
|
|
|
|
local the_weapon = level.object_by_id(switch_info.weapon_id) or level.object_by_id(switch_info.new_weapon_id)
|
|
local the_scope = level.object_by_id(switch_info.scope_id)
|
|
|
|
xr_effects.play_snd(db.actor, nil, { [1] = "interface\\inv_attach_addon" })
|
|
|
|
if (switch_info.force_attach) then
|
|
utils_item.attach_addon(the_weapon, the_scope, "scope", true)
|
|
else
|
|
item_weapon.attach_scope(the_scope, the_weapon)
|
|
end
|
|
|
|
CreateTimeEvent(0, "restore_delayed", 1, restore_weapon_callback)
|
|
end
|
|
|
|
function detach_current_scope()
|
|
log_trace("detach_current_scope")
|
|
|
|
local weapon_section = switch_info.weapon_section
|
|
local parent_section = switch_info.parent_section
|
|
local current_scope
|
|
|
|
if weapon_section and parent_section then
|
|
for scope, _ in pairs(scopes_table) do
|
|
if (string.find(weapon_section, scope)) then
|
|
log("removing %s from %s.", scope, parent_section)
|
|
current_scope = scope
|
|
break
|
|
end
|
|
end
|
|
elseif switch_info.force_detach then
|
|
parent_section = weapon_section
|
|
current_scope = switch_info.native_scope
|
|
else
|
|
reset_state("cannot detach scope")
|
|
end
|
|
|
|
if current_scope then
|
|
switch_info.current_scope = current_scope
|
|
scope_mappings[parent_section] = current_scope
|
|
|
|
db.actor:hide_weapon()
|
|
CreateTimeEvent(0, "detach_delayed", 1, detach_scope_callback)
|
|
end
|
|
end
|
|
|
|
function detach_scope_callback()
|
|
log_trace("detach_scope_callback")
|
|
|
|
RemoveTimeEvent(0, "detach_delayed")
|
|
|
|
local the_weapon = level.object_by_id(switch_info.weapon_id)
|
|
local the_scope = level.object_by_id(switch_info.scope_id)
|
|
|
|
xr_effects.play_snd(db.actor, nil, { [1] = "interface\\inv_detach_addon" })
|
|
|
|
if (switch_info.force_detach) then
|
|
utils_item.detach_addon(the_weapon, nil, "scope")
|
|
else
|
|
item_weapon.detach_scope(the_weapon)
|
|
end
|
|
|
|
if (switch_info.cycle_requested) then
|
|
log("Cycle requested from %s", switch_info.current_scope)
|
|
attach_valid_scope()
|
|
else
|
|
CreateTimeEvent(0, "restore_delayed", 1, restore_weapon_callback)
|
|
end
|
|
end
|
|
|
|
function restore_weapon_callback()
|
|
log_trace("restore_weapon_callback")
|
|
|
|
RemoveTimeEvent(0, "restore_delayed")
|
|
|
|
log("restoring weapon.")
|
|
db.actor:restore_weapon()
|
|
|
|
CreateTimeEvent(0, "reset_firemode", 0, reset_firemode_callback)
|
|
end
|
|
|
|
function reset_firemode_callback()
|
|
log_trace("reset_firemode_callback")
|
|
|
|
RemoveTimeEvent(0, "reset_firemode")
|
|
|
|
if switch_info.native_scope then
|
|
log("no reset required.")
|
|
else
|
|
local weapon = db.actor:active_item()
|
|
if weapon then
|
|
log("new weapon: %s (#%s)", weapon:section(), weapon:id())
|
|
|
|
local cWeaponMaggie = weapon:cast_WeaponMagazined()
|
|
|
|
if cWeaponMaggie and cWeaponMaggie.SetFireMode then
|
|
log("[1.5.2] resetting firing mode to: #%s (%s)", switch_info.saved_fire_mode.value, switch_info.saved_fire_mode.name)
|
|
cWeaponMaggie:SetFireMode(switch_info.saved_fire_mode.value)
|
|
else
|
|
local new_queue_size = switch_info.saved_queue_size
|
|
if new_queue_size == "A" then
|
|
new_queue_size = WEAPON_INFINITE_QUEUE
|
|
end
|
|
|
|
log("[1.5.1] resetting queue size to: %s (%s)", new_queue_size, switch_info.saved_queue_size)
|
|
weapon:set_queue_size(new_queue_size)
|
|
end
|
|
end
|
|
end
|
|
|
|
reset_state("done.")
|
|
end
|
|
|
|
function reset_state(msg)
|
|
printp("=", msg)
|
|
switch_info = nil
|
|
end
|
|
|
|
function ishy_on_key_release(key)
|
|
if (key == ish_toggle_scope_mcm.KEY_TOGGLE_SCOPE) then
|
|
local success, err = pcall(switch_scope)
|
|
|
|
if not success then
|
|
log_error("whoops %s", err)
|
|
reset_state("unexpected error.")
|
|
end
|
|
end
|
|
end
|
|
|
|
function create_switch_info(the_weapon, cWeapon)
|
|
log_trace("create_switch_info")
|
|
|
|
local info = { weapon_id = the_weapon:id(), weapon_section = the_weapon:section() }
|
|
info.parent_section = ini_sys:r_string_ex(info.weapon_section, "parent_section")
|
|
info.valid_scopes = parse_list(ini_sys, info.weapon_section, "scopes")
|
|
|
|
local native_scope = cWeapon:GetScopeName()
|
|
if native_scope then
|
|
local is_attached = the_weapon:cast_Weapon():IsScopeAttached()
|
|
|
|
log("weapon has native scope: %s (%s)", native_scope, (is_attached and "attached" or "detached"))
|
|
info.native_scope = native_scope
|
|
info.force_detach = is_attached
|
|
info.force_attach = not is_attached
|
|
|
|
table.insert(info.valid_scopes, info.native_scope)
|
|
end
|
|
|
|
for _, scope_name in pairs(info.valid_scopes) do
|
|
if db.actor:object(scope_name) then
|
|
info.has_other_scopes = true
|
|
break
|
|
end
|
|
end
|
|
|
|
info.saved_fire_mode = mode_mappings[cWeapon:GetFireMode() or 1]
|
|
info.saved_queue_size = ActorMenu.get_maingame().m_ui_hud_states.m_fire_mode:GetText()
|
|
|
|
if info.saved_queue_size == "" then
|
|
log("HUD turned off, defaulting to fire mode.")
|
|
info.saved_queue_size = info.saved_fire_mode
|
|
end
|
|
|
|
log("saved fire mode #%s (%s) queue size: %s", info.saved_fire_mode.value, info.saved_fire_mode.name, info.saved_queue_size)
|
|
|
|
return info
|
|
end
|
|
|
|
function find_first_scope(valid_scopes)
|
|
log_trace("find_first_scope")
|
|
|
|
for idx, scope_name in pairs(valid_scopes) do
|
|
if db.actor:object(scope_name) then
|
|
return idx
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function find_next_scope(valid_scopes, current_scope)
|
|
log_trace("find_next_scope")
|
|
|
|
for idx, scope_name in pairs(valid_scopes) do
|
|
if current_scope == scope_name then
|
|
local next_scope = valid_scopes[idx + 1]
|
|
|
|
if next_scope and db.actor:object(next_scope) then
|
|
return idx + 1
|
|
end
|
|
|
|
current_scope = next_scope
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function find_preferred_scope(valid_scopes, preferred_scope)
|
|
log_trace("find_preferred_scope")
|
|
|
|
for idx, scope_name in pairs(valid_scopes) do
|
|
if scope_name == preferred_scope then
|
|
log("preferred scope is %s (%s)", preferred_scope, idx)
|
|
return idx
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function ishy_actor_on_item_take(item)
|
|
if not (item and IsWeapon(item)) then
|
|
return -- not interested.
|
|
end
|
|
|
|
log_trace("ishy_actor_on_item_take")
|
|
|
|
if switch_info then
|
|
switch_info.new_weapon_id = item:id()
|
|
log("grabbed new weapon: %s", switch_info.new_weapon_id)
|
|
end
|
|
|
|
if is_secondary then
|
|
log("re-equipping secondary weapon.")
|
|
db.actor:move_to_slot(item, SECONDARY_WEAPON_SLOT)
|
|
is_secondary = nil
|
|
end
|
|
end
|
|
|
|
function attach_scope_override(item, weapon)
|
|
if not item or not weapon then
|
|
return
|
|
end
|
|
|
|
log_trace("attach_scope_override")
|
|
|
|
is_secondary = is_secondary_weapon(weapon)
|
|
log("processing %s weapon.", (is_secondary and 'secondary' or 'primary'))
|
|
|
|
local weapon_section = weapon:section()
|
|
local parent_section = ini_sys:r_string_ex(weapon_section, "parent_section")
|
|
|
|
if parent_section and parent_section ~= weapon_section then -- current weapon is already a scoped variant.
|
|
log("something already attached: %s", weapon_section) -- original [item_weapon.attach_scope] function will ignore this case and prevent changing scopes unless we do something about it.
|
|
|
|
local current_scope = string.gsub(weapon_section, parent_section .. "_", "")
|
|
|
|
if current_scope then
|
|
local item_section = item:section()
|
|
local new_weapon_section = (parent_section .. "_" .. item_section)
|
|
|
|
log("looking up weapon %s...", new_weapon_section)
|
|
local new_weapon_class = ini_sys:r_string_ex(new_weapon_section, "class")
|
|
|
|
if not new_weapon_class then
|
|
reset_state("nice try. no such weapon.")
|
|
return
|
|
end
|
|
|
|
local old_weapon = alife_object(weapon:id())
|
|
|
|
log("spawning detached scope in inventory: %s", current_scope)
|
|
local owner = old_weapon and old_weapon.parent_id and get_object_by_id(old_weapon.parent_id)
|
|
alife_create_item(current_scope, owner)
|
|
|
|
log("spawning weapon variant with the new attachment: %s", new_weapon_section)
|
|
local new_weapon = alife_clone_weapon(old_weapon, new_weapon_section)
|
|
|
|
log("releasing attached scope into the void: %s (#%s)", item_section, item:id())
|
|
alife_release(item)
|
|
|
|
return -- all done, no need to call the base function, just bail out.
|
|
end
|
|
end
|
|
|
|
return attach_scope_original(item, weapon) -- if we are here, it's none of our business. call the base function to do its thing.
|
|
end
|
|
|
|
function detach_scope_override(weapon)
|
|
if not weapon then
|
|
return
|
|
end
|
|
|
|
log_trace("detach_scope_override")
|
|
|
|
is_secondary = is_secondary_weapon(weapon)
|
|
log("detaching from %s weapon.", (is_secondary and 'secondary' or 'primary'))
|
|
|
|
return detach_scope_original(weapon)
|
|
end
|
|
|
|
function is_secondary_weapon(weapon)
|
|
log_trace("is_secondary_weapon")
|
|
|
|
local secondary_weapon = db.actor:item_in_slot(SECONDARY_WEAPON_SLOT)
|
|
local retval = secondary_weapon and weapon:id() == secondary_weapon:id()
|
|
return retval
|
|
end
|
|
|
|
function on_game_start()
|
|
log_trace("on_game_start")
|
|
|
|
RegisterScriptCallback("on_key_release", ishy_on_key_release)
|
|
RegisterScriptCallback("actor_on_item_take", ishy_actor_on_item_take)
|
|
|
|
-- monkey patch the original attach/detach functions to intercept all scope change attempts.
|
|
if not attach_scope_original then
|
|
attach_scope_original = item_weapon.attach_scope
|
|
item_weapon.attach_scope = attach_scope_override
|
|
end
|
|
|
|
if not detach_scope_original then
|
|
detach_scope_original = item_weapon.detach_scope
|
|
item_weapon.detach_scope = detach_scope_override
|
|
end
|
|
end
|
|
|
|
function log(msg, ...) printp(" ", msg, ...) end
|
|
|
|
function log_trace(msg, ...) printp("*", msg, ...) end
|
|
|
|
function log_error(msg, ...) printp("!", msg, ...) end
|
|
|
|
function printp(prefix, msg, ...)
|
|
--printf(prefix .. " [IshyScope] " .. msg, ...)
|
|
end
|