HT.Mission = {}

HT.Mission.enableDisableAi = false
HT.Mission.enableInvisiblePlayer = false
HT.Mission.carryVerifyDisabled = false
HT.Mission.enableFreezeCivilians = false

function HT.Mission:setInvisiblePlayer(value)
    HT:setSetting("enable_invisible_player", value)
    HT:updateJsonValue("mission", "ut_item_mission_toggle_invisible_player", value)
    HT.Mission.enableInvisiblePlayer = value
    if value then
        local player = managers.player:player_unit()
        if not alive(player) then
            return
        end
        local playerKey = player:key()
        local AiState = managers.groupai:state()
        for attention_object, data in pairs(AiState._attention_objects.all) do
            if playerKey == attention_object then
                HT.Mission._invisibleAttentionBackup = data
            end
        end
        AiState:unregister_AI_attention_object(player:key())
    else
        local player = managers.player:player_unit()
        if not alive(player) then
            return
        end
        local playerKey = player:key()
        local AiState = managers.groupai:state()
        if HT.Mission._invisibleAttentionBackup then
            AiState._attention_objects.all[playerKey] = HT.Mission._invisibleAttentionBackup
            AiState:on_AI_attention_changed(playerKey)
            HT.Mission._invisibleAttentionBackup = nil
        end
    end
    if value then
        HT:addAlert("ut_alert_invisible_player_enabled", HT.colors.success)
    else
        HT:addAlert("ut_alert_invisible_player_disabled", HT.colors.success)
    end
end

function HT.Mission:accessCameras()
    game_state_machine:change_state_by_name("ingame_access_camera")
end

function HT.Mission:removeInvisibleWalls()
    local units = World:find_units_quick("all", 1)
    for key, unit in pairs(units) do
        if HT.Utils:inTable(unit:name():key(), HT.Tables.invisibleWalls) then
            HT:removeUnit(unit)
        end
    end
    HT:addAlert("ut_alert_invisible_walls_removed", HT.colors.success)
end

function HT.Mission:convertAllEnemies()
    HT:enableUnlimitedConversions()
    for key, data in pairs(managers.enemy:all_enemies()) do
        if not alive(data.unit) then
            goto continue
        end
        managers.groupai:state():convert_hostage_to_criminal(data.unit)
        managers.groupai:state():sync_converted_enemy(data.unit)
        ::continue::
    end
    HT:addAlert("ut_alert_converted_all_enemies", HT.colors.success)
end

HT.Mission.enableInstantConvert = false
HT.Mission.instantConvertInstalled = false

-- Instant / unlimited convert: lets you joker any enemy (no Joker skill needed)
-- and removes the minion cap. Installs a single flag-gated upgrade_value override
-- so toggling never re-clones the class. Adapted from pirate-perfection inf_converts.
function HT.Mission:setInstantConvert(value)
    HT:setSetting("enable_instant_convert", value)
    HT:updateJsonValue("mission", "ut_item_mission_toggle_instant_convert", value)
    HT.Mission.enableInstantConvert = value

    if value and not HT.Mission.instantConvertInstalled then
        _G.CloneClass(PlayerManager)
        local orig_upgrade_value = PlayerManager.orig.upgrade_value
        function PlayerManager:upgrade_value(category, upgrade, default)
            if HT.Mission.enableInstantConvert and category == "player" then
                if upgrade == "convert_enemies" then
                    return true
                elseif upgrade == "convert_enemies_max_minions" then
                    return HT.fakeMaxInteger
                end
            end
            return orig_upgrade_value(self, category, upgrade, default)
        end
        HT.Mission.instantConvertInstalled = true
    end

    HT:addAlert(value and "Instant convert enabled" or "Instant convert disabled", HT.colors.success, false)
end

function HT.Mission:triggerAlarm()
    managers.groupai:state():on_police_called("empty")
    HT:addAlert("ut_alert_alarm_triggered", HT.colors.success)
end

function HT.Mission:setDisableAi(value)
    HT:setSetting("enable_disable_ai", value)
    HT:updateJsonValue("mission", "ut_item_mission_toggle_disable_ai", value)
    HT.Mission.enableDisableAi = value
    if not value then
        for key, value in pairs(managers.enemy:all_civilians()) do
            value.unit:brain():set_active(true)
        end

        for key, value in pairs(managers.enemy:all_enemies()) do
            value.unit:brain():set_active(true)
        end

        if SecurityCamera and SecurityCamera.cameras then
            for key, unit in pairs(SecurityCamera.cameras) do
                unit:base()._detection_interval = 0.1
            end
        end

        if managers.groupai:state():turrets() then
            for key, unit in pairs(managers.groupai:state():turrets()) do
                unit:brain():set_active(true)
            end
        end
    end
    if value then
        HT:addAlert("ut_alert_disable_ai_enabled", HT.colors.success)
    else
        HT:addAlert("ut_alert_disable_ai_disabled", HT.colors.success)
    end
end

function HT.Mission:disableAi()
    for key, data in pairs(managers.enemy:all_civilians()) do
        if data.unit:brain():is_active() then
            data.unit:brain():set_active(false)
        end
    end

    for key, data in pairs(managers.enemy:all_enemies()) do
        if data.unit:brain():is_active() then
            data.unit:brain():set_active(false)
        end
    end

    if SecurityCamera and SecurityCamera.cameras then
        for key, unit in pairs(SecurityCamera.cameras) do
            if unit:base()._detection_interval ~= HT.fakeMaxInteger then
                unit:base()._detection_interval = HT.fakeMaxInteger
            end
        end
    end

    if managers.groupai:state():turrets() then
        for key, unit in pairs(managers.groupai:state():turrets()) do
            if unit:brain():is_active() then
                unit:brain():set_active(false)
            end
        end
    end
end

function HT.Mission:setInstantDrilling(value)
    HT:setSetting("enable_instant_drilling", value)
    HT:updateJsonValue("mission", "ut_item_mission_toggle_instant_drilling", value)
    _G.CloneClass(TimerGui)
    if _G.SecurityLockGui then _G.CloneClass(SecurityLockGui) end
    if value then
        function TimerGui:_set_jamming_values()
        end

        function TimerGui:_start(timer, current_timer)
            return TimerGui.orig._start(self, 0.01, 0.01)
        end
        if _G.SecurityLockGui then
            function SecurityLockGui:_start(bar, timer, current_timer)
                return SecurityLockGui.orig._start(self, bar, 0.01, 0.01)
            end
        end
    else
        TimerGui._set_jamming_values = TimerGui.orig._set_jamming_values
        TimerGui._start = TimerGui.orig._start
        if _G.SecurityLockGui and SecurityLockGui.orig then
            SecurityLockGui._start = SecurityLockGui.orig._start
        end
    end
    if value then
        HT:addAlert("ut_alert_instant_drilling_enabled", HT.colors.success)
    else
        HT:addAlert("ut_alert_instant_drilling_disabled", HT.colors.success)
    end
end

function HT.Mission:setPreventAlarmTriggering(value)
    HT:setSetting("enable_prevent_alarm_triggering", value)
    HT:updateJsonValue("mission", "ut_item_mission_toggle_prevent_alarm_triggering", value)
    _G.CloneClass(GroupAIStateBase)
    if value then
        function GroupAIStateBase:on_police_called()
        end
    else
        GroupAIStateBase.on_police_called = GroupAIStateBase.orig.on_police_called
    end
    if value then
        HT:addAlert("ut_alert_prevent_alarm_triggering_enabled", HT.colors.success)
    else
        HT:addAlert("ut_alert_prevent_alarm_triggering_disabled", HT.colors.success)
    end
end

function HT.Mission:setUnlimitedPagers(value)
    -- copbrain.lua uses bluff_success_chance_w_skill when the player has the pager
    -- bluff upgrade, so set BOTH tables or the cheat fails for skilled players.
    local pager = tweak_data.player.alarm_pager
    if not HT.Mission._pagerChanceBackup then
        HT.Mission._pagerChanceBackup = {
            normal = deep_clone(pager.bluff_success_chance),
            skilled = deep_clone(pager.bluff_success_chance_w_skill)
        }
    end
    HT:setSetting("enable_unlimited_pagers", value)
    HT:updateJsonValue("mission", "ut_item_mission_toggle_unlimited_pagers", value)
    if value then
        pager.bluff_success_chance = { 1, 1, 1, 1, 1 }
        pager.bluff_success_chance_w_skill = { 1, 1, 1, 1, 1 }
    else
        pager.bluff_success_chance = deep_clone(HT.Mission._pagerChanceBackup.normal)
        pager.bluff_success_chance_w_skill = deep_clone(HT.Mission._pagerChanceBackup.skilled)
    end
    if value then
        HT:addAlert("ut_alert_unlimited_pagers_enabled", HT.colors.success)
    else
        HT:addAlert("ut_alert_unlimited_pagers_disabled", HT.colors.success)
    end
end

function HT.Mission:setXray(value)
    HT:setSetting("enable_xray", value)
    HT:updateJsonValue("mission", "ut_item_mission_toggle_xray", value)
    _G.CloneClass(EnemyManager)
    if value then
        for key, data in pairs(managers.enemy:all_enemies()) do
            data.unit:contour():add("mark_enemy", false, HT.fakeMaxInteger)
        end
        for key, data in pairs(managers.enemy:all_civilians()) do
            data.unit:contour():add("mark_enemy", false, HT.fakeMaxInteger)
        end
        if SecurityCamera and SecurityCamera.cameras then
            for key, unit in pairs(SecurityCamera.cameras) do
                unit:contour():add("mark_unit", false, HT.fakeMaxInteger)
            end
        end
        function EnemyManager:register_enemy(unit, ...)
            EnemyManager.orig.register_enemy(self, unit, ...)
            unit:contour():add("mark_enemy", false, HT.fakeMaxInteger)
        end

        function EnemyManager:register_civilian(unit, ...)
            EnemyManager.orig.register_civilian(self, unit, ...)
            unit:contour():add("mark_enemy", false, HT.fakeMaxInteger)
        end

        function EnemyManager:on_enemy_died(unit, ...)
            EnemyManager.orig.on_enemy_died(self, unit, ...)
            unit:contour():remove("mark_enemy", false)
        end

        function EnemyManager:on_civilian_died(unit, ...)
            EnemyManager.orig.on_civilian_died(self, unit, ...)
            unit:contour():remove("mark_enemy", false)
        end
    else
        for key, data in pairs(managers.enemy:all_civilians()) do
            data.unit:contour():remove("mark_enemy", false)
        end
        for key, data in pairs(managers.enemy:all_enemies()) do
            data.unit:contour():remove("mark_enemy", false)
        end
        if SecurityCamera and SecurityCamera.cameras then
            for key, unit in pairs(SecurityCamera.cameras) do
                unit:contour():remove("mark_unit", false)
            end
        end
        EnemyManager.register_enemy = EnemyManager.orig.register_enemy
        EnemyManager.register_civilian = EnemyManager.orig.register_civilian
        EnemyManager.on_enemy_died = EnemyManager.orig.on_enemy_died
        EnemyManager.on_civilian_died = EnemyManager.orig.on_civilian_died
    end
    if value then
        HT:addAlert("ut_alert_xray_enabled", HT.colors.success)
    else
        HT:addAlert("ut_alert_xray_disabled", HT.colors.success)
    end
end

function HT.Mission:disableCarryVerify()
    if HT.Mission.carryVerifyDisabled then return end
    _G.CloneClass(PlayerManager)
    function PlayerManager:verify_carry()
        return true
    end
    HT.Mission.carryVerifyDisabled = true
end

function HT.Mission:throwBag(carryId)
    if not HT:isInHeist() then
        HT:addAlert("ut_alert_in_heist_only_feature", HT.colors.warning)
        return false
    end
    if not HT:isHost() then
        HT:addAlert("ut_alert_host_only_feature", HT.colors.warning)
        return false
    end

    HT.Mission:disableCarryVerify()

    local carryData = tweak_data.carry[carryId]
    if not carryData then
        HT:addAlert("ut_alert_spawn_unit_failure", HT.colors.danger)
        return false
    end

    local position = HT:getPlayerCameraPosition()
    local rotation = HT:getPlayerCameraRotation()
    local forward = HT:getPlayerCameraForward()
    local throwLevel = managers.player:upgrade_level("carry", "throw_distance_multiplier", 0)
    local localPeer = managers.network:session():local_peer()

    managers.player:server_drop_carry(carryId, 1, nil, nil, nil, position, rotation, forward, throwLevel, nil, localPeer)
    return true
end

function HT.Mission:spawnBagOfCurrentType()
    local carryId = "money"

    local carryData = managers.player:get_my_carry_data()
    if carryData and carryData.carry_id then
        carryId = carryData.carry_id
    end

    local ok = HT.Mission:throwBag(carryId)
    if ok then
        HT:addAlert("ut_alert_bag_spawned", HT.colors.success, false, carryId, HT.colors.white)
    end
end

function HT.Mission:spawnMoneyBag()
    local ok = HT.Mission:throwBag("money")
    if ok then HT:addAlert("ut_alert_money_bag_spawned", HT.colors.success) end
end

function HT.Mission:spawnGoldBag()
    local ok = HT.Mission:throwBag("gold")
    if ok then HT:addAlert("ut_alert_gold_bag_spawned", HT.colors.success) end
end

function HT.Mission:giveHeistXp()
    if not HT:isInHeist() then
        HT:addAlert("ut_alert_in_heist_only_feature", HT.colors.warning)
        return
    end

    -- debug_add_points -> add_points(points, present, debug=true); the debug flag
    -- bypasses the "presence must be Playing/Mission_end" guard that made the old
    -- _set_current_level approach silently do nothing.
    local ok = pcall(function()
        managers.experience:debug_add_points(1000000, false)
        HT.Player:refreshAndSave()
    end)

    if ok then
        HT:addAlert("ut_alert_heist_xp_given", HT.colors.success)
    else
        HT:addAlert("ut_alert_spawn_unit_failure", HT.colors.danger)
    end
end

function HT.Mission:getOutOfCustody()
    if not HT:isInHeist() then
        HT:addAlert("ut_alert_in_heist_only_feature", HT.colors.warning)
        return
    end
    pcall(function()
        IngameWaitingForRespawnState.request_player_spawn()
    end)
    HT:addAlert("Escaping custody...", HT.colors.success, false)
end

HT.Mission.autoCookEnabled = false

local function ht_can_interact() return true end

local function ht_cook_interact(interaction_name)
    -- Cooking interactions are host-authoritative; running interact() on them as a
    -- client sends malformed network data and hard-crashes (access violation).
    if not Network:is_server() then return end
    local player = managers.player:player_unit()
    if not alive(player) then return end
    for _, unit in pairs(managers.interaction._interactive_units) do
        if alive(unit) then
            local interaction = unit:interaction()
            if interaction and interaction.tweak_data == interaction_name then
                -- Only force-interact a station the game considers ready. Interacting
                -- with an inactive/disabled cook station crashes natively even on host.
                local ready = true
                pcall(function()
                    if interaction.active and interaction:active() == false then ready = false end
                    if interaction.disabled and interaction:disabled() then ready = false end
                end)
                if ready then
                    pcall(function()
                        interaction.can_interact = ht_can_interact
                        interaction:interact(player)
                        interaction.can_interact = nil
                    end)
                end
                break
            end
        end
    end
end

function HT.Mission:triggerCookInteraction(name)
    ht_cook_interact(name)
end

function HT.Mission:setCookOption(name, value)
    HT:setSetting("cook_" .. name, value)
end

function HT.Mission:announceCook(text)
    if not HT:getSetting("cook_announcements") then return end
    pcall(function()
        managers.chat:_receive_message(1, "HEX COOKER", tostring(text), tweak_data.system_chat_color)
        if Network:is_server() and managers.network:session() then
            managers.network:session():send_to_peers("send_chat_message", ChatManager.GAME, "[HEX COOKER] " .. tostring(text))
        end
    end)
end

local function ht_drop_bag()
    local carry_data = managers.player:get_my_carry_data()
    if not carry_data then return end
    local player = managers.player:player_unit()
    if not alive(player) then return end
    local pos = player:camera():position()
    local rot = player:camera():rotation()
    local fwd = player:camera():forward()
    if Network:is_server() then
        managers.player:server_drop_carry(carry_data.carry_id, carry_data.multiplier,
            carry_data.dye_initiated, carry_data.has_dye_pack, carry_data.dye_value_multiplier,
            pos, rot, fwd, 1, nil, managers.network:session():local_peer())
    else
        managers.network:session():send_to_host("server_drop_carry",
            carry_data.carry_id, carry_data.multiplier, carry_data.dye_initiated,
            carry_data.has_dye_pack, carry_data.dye_value_multiplier, pos, rot, fwd, 1, nil)
    end
    pcall(function() managers.player:set_player_state("standard") end)
end

function HT.Mission:enableAutoCook()
    if HT.Mission.autoCookEnabled then
        HT:addAlert("Auto cook already active", HT.colors.warning, false)
        return
    end
    HT.Mission.autoCookEnabled = true
    if _G.UseInteractionExt and not HT.Mission._cook_interact_backup then
        HT.Mission._cook_interact_backup = UseInteractionExt.interact
        function UseInteractionExt:interact(player, ...)
            local chemical = self.tweak_data == "muriatic_acid" or self.tweak_data == "caustic_soda" or self.tweak_data == "hydrogen_chloride"
            if HT:getSetting("cook_prevent_explosion") and chemical and HT.Mission.expectedChemical and self.tweak_data ~= HT.Mission.expectedChemical then
                HT:addAlert("Blocked wrong ingredient: " .. tostring(self.tweak_data), HT.colors.warning, false)
                return
            end
            return HT.Mission._cook_interact_backup(self, player, ...)
        end
    end

    if not HT.Mission._dialog_backup then
        HT.Mission._dialog_backup = DialogManager.queue_dialog
    end
    function DialogManager:queue_dialog(id, params)
        HT.Mission._dialog_backup(self, id, params)
        if not HT.Mission.autoCookEnabled then return end
        if id == "pln_rt1_20" or id == "Play_loc_mex_cook_03" then
            HT.Mission.expectedChemical = "muriatic_acid"
            HT.Mission:announceCook("Adding muriatic acid")
            ht_cook_interact("muriatic_acid")
            DelayedCalls:Add("ht_cook_step1", 0.3, function()
                ht_cook_interact("methlab_bubbling")
            end)
        elseif id == "pln_rt1_22" or id == "Play_loc_mex_cook_04" then
            HT.Mission.expectedChemical = "caustic_soda"
            HT.Mission:announceCook("Adding caustic soda")
            ht_cook_interact("caustic_soda")
            DelayedCalls:Add("ht_cook_step2", 0.3, function()
                ht_cook_interact("methlab_caustic_cooler")
            end)
        elseif id == "pln_rt1_24" or id == "Play_loc_mex_cook_05" then
            HT.Mission.expectedChemical = "hydrogen_chloride"
            HT.Mission:announceCook("Adding hydrogen chloride")
            ht_cook_interact("hydrogen_chloride")
            DelayedCalls:Add("ht_cook_step3", 0.3, function()
                ht_cook_interact("methlab_gas_to_salt")
            end)
        elseif id == "pln_rat_stage1_20" then
            HT.Mission.expectedChemical = "muriatic_acid"
            HT.Mission:announceCook("Adding muriatic acid")
            ht_cook_interact("muriatic_acid")
            DelayedCalls:Add("ht_cook_rat1", 0.3, function() ht_cook_interact("methlab_bubbling") end)
        elseif id == "pln_rat_stage1_22" then
            HT.Mission.expectedChemical = "caustic_soda"
            HT.Mission:announceCook("Adding caustic soda")
            ht_cook_interact("caustic_soda")
            DelayedCalls:Add("ht_cook_rat2", 0.3, function() ht_cook_interact("methlab_caustic_cooler") end)
        elseif id == "pln_rat_stage1_24" then
            HT.Mission.expectedChemical = "hydrogen_chloride"
            HT.Mission:announceCook("Adding hydrogen chloride")
            ht_cook_interact("hydrogen_chloride")
            DelayedCalls:Add("ht_cook_rat3", 0.3, function() ht_cook_interact("methlab_gas_to_salt") end)
        end
        if HT:getSetting("cook_auto_flare_breaker") then
            DelayedCalls:Add("ht_auto_flare_breaker", 0.6, function()
                ht_cook_interact("place_flare")
                ht_cook_interact("circuit_breaker")
                ht_cook_interact("transformer_box")
            end)
        end
    end

    if not HT.Mission._add_unit_backup then
        HT.Mission._add_unit_backup = ObjectInteractionManager.add_unit
    end
    function ObjectInteractionManager:add_unit(unit, ...)
        HT.Mission._add_unit_backup(self, unit, ...)
        if not HT.Mission.autoCookEnabled then return end
        if not unit then return end
        local interaction = unit and unit.interaction and unit:interaction()
        if not interaction then return end
        if interaction.tweak_data == "taking_meth" or interaction.tweak_data == "taking_meth_huge" then
            local bag_type = interaction.tweak_data == "taking_meth_huge" and "meth_half" or "meth"
            local pos = interaction:interact_position()
            local drop_pos = Vector3(pos.x, pos.y, pos.z + 40)
            DelayedCalls:Add("ht_take_meth", 2.5, function()
                ht_drop_bag()
                ht_cook_interact(interaction.tweak_data)
                DelayedCalls:Add("ht_drop_meth", 0.3, function()
                    if Network:is_server() then
                        local carry = tweak_data.carry[bag_type]
                        if carry then
                            managers.player:server_drop_carry(bag_type, 1,
                                false, false, 1, drop_pos,
                                Vector3(math.random(-180,180), math.random(-180,180), 0),
                                Vector3(0,0,1), 100, nil,
                                managers.network:session():local_peer())
                        end
                    else
                        managers.network:session():send_to_host("server_drop_carry",
                            bag_type, 1, false, false, 1, drop_pos,
                            Vector3(math.random(-180,180), math.random(-180,180), 0),
                            Vector3(0,0,1), 100, nil)
                    end
                    pcall(function() managers.player:clear_carry() end)
                end)
            end)
        end
    end

    HT:addAlert("Auto cook ON", HT.colors.success, false)
end

function HT.Mission:disableAutoCook()
    HT.Mission.autoCookEnabled = false
    if HT.Mission._dialog_backup then
        DialogManager.queue_dialog = HT.Mission._dialog_backup
        HT.Mission._dialog_backup = nil
    end
    if HT.Mission._add_unit_backup then
        ObjectInteractionManager.add_unit = HT.Mission._add_unit_backup
        HT.Mission._add_unit_backup = nil
    end
    if HT.Mission._cook_interact_backup and _G.UseInteractionExt then
        UseInteractionExt.interact = HT.Mission._cook_interact_backup
        HT.Mission._cook_interact_backup = nil
    end
    HT.Mission.expectedChemical = nil
    HT:addAlert("Auto cook OFF", HT.colors.success, false)
end

function HT.Mission:autoCook()
    if not HT:isInHeist() then
        HT:addAlert("ut_alert_in_heist_only_feature", HT.colors.warning)
        return
    end
    if not HT:isHost() then
        HT:addAlert("ut_alert_host_only_feature", HT.colors.warning)
        return
    end
    if HT.Mission.autoCookEnabled then
        HT.Mission:disableAutoCook()
    else
        HT.Mission:enableAutoCook()
    end
end

function HT.Mission:killAllEnemies()
    local player = managers.player and managers.player:player_unit()
    for _, data in pairs(managers.enemy:all_enemies()) do
        local unit = data.unit
        if alive(unit) then
            pcall(function()
                local dmg = unit:character_damage()
                if dmg and not dmg:dead() then
                    dmg:death({ attacker_unit = player, variant = "bullet" })
                end
            end)
        end
    end
    HT:addAlert("All enemies killed", HT.colors.success, false)
end

function HT.Mission:setFreezeCivilians(value)
    HT:setSetting("enable_freeze_civilians", value)
    HT:updateJsonValue("mission", "ut_item_mission_toggle_freeze_civilians", value)
    HT.Mission.enableFreezeCivilians = value
    if not value then
        for _, data in pairs(managers.enemy:all_civilians()) do
            if alive(data.unit) then
                pcall(function() data.unit:brain():set_active(true) end)
            end
        end
    end
    if value then
        HT:addAlert("Civilians frozen", HT.colors.success, false)
    else
        HT:addAlert("Civilians unfrozen", HT.colors.success, false)
    end
end

function HT.Mission:freezeAllCivs()
    for _, data in pairs(managers.enemy:all_civilians()) do
        if data.unit and alive(data.unit) and data.unit:brain():is_active() then
            data.unit:brain():set_active(false)
        end
    end
end

function HT.Mission:openAllDoors()
    local player = managers.player and managers.player:player_unit()
    if not player then return end
    local objects = {}
    for _, v in pairs(managers.interaction._interactive_units) do
        if alive(v) and v.interaction and v:interaction() then
            pcall(function()
                local has_door = false
                local unit = v:interaction()._unit
                if unit and alive(unit) and unit.door and unit:door() then
                    has_door = true
                end
                local td = v:interaction().tweak_data
                if td and (td:find("door") or td:find("lock") or td:find("gate") or td:find("keycard")) then
                    has_door = true
                end
                if has_door then
                    table.insert(objects, v:interaction())
                end
            end)
        end
    end
    for _, v in ipairs(objects) do
        HT:interactExtension(v, true)
    end
    HT:addAlert("All doors opened", HT.colors.success, false)
end
