Files
darktide-AutoMark/AutoMark/scripts/mods/AutoMark/mark/servo_skull_mark.lua
T

488 lines
21 KiB
Lua

---@class AutoMarkMod:DMFMod
local mod = get_mod("AutoMark")
local context = mod.context
local mark_context = mod.mark_context
local TAG_NAMES = mod.TAG_NAMES
local mod_settings = mod.settings
local noospheric_command_breed_settings = mod.noospheric_command_breed_settings
local DISABLING_TYPES = mod.DISABLING_TYPES
-- Imports
local SpecialRulesSettings = require("scripts/settings/ability/special_rules_settings")
local CompanionServoSkullSettings = require("scripts/settings/companion/companion_servo_skull_settings")
local Hud = require("scripts/utilities/ui/hud")
local Health = require("scripts/utilities/health")
local special_rules = SpecialRulesSettings.special_rules
local servo_skull_states = CompanionServoSkullSettings.STATES
-- Global Cache
local RESOLUTION_LOOKUP = RESOLUTION_LOOKUP
local Managers = Managers
local callback = callback
local get_game_object_field = GameSession.game_object_field
local ScriptUnit_extension = ScriptUnit.extension
local HEALTH_ALIVE = HEALTH_ALIVE
local Unit_world_position = Unit.world_position
local Vector3_distance = Vector3.distance
local HACK_TAG_NAME = "hacking_over_here_companion"
local CANCEL_INTERVAL = 0.5
-- set_tag() for off-LOS units is silently rejected: as HOST, SmartTagSystem:set_tag() runs
-- _tagger_is_looking_at_target() (dot-0.75 aim cone + LOS sweep, from the game source) and
-- returns before creating the tag. Since we hold the full source of the accept-branch
-- (_generate_tag_id -> _create_tag_locally -> group limit check -> rpc to clients), the hook
-- below reproduces it verbatim, minus the look check, for our flagged auto-hack targets only.
local function ensure_automark_tag_hook()
if mod.omni_hack_tag_hooked then
return
end
local smart_tag_system = context.smart_tag_system
if not smart_tag_system then
return
end
local cls = getmetatable(smart_tag_system)
if type(cls) == "table" and rawget(cls, "set_tag") == nil then
cls = rawget(cls, "__index") or cls
end
if type(cls) == "table" and cls.set_tag ~= nil then
mod:hook(cls, "set_tag", function(previous_hook, self, template_name, tagger_unit, target_unit, target_location)
mod.omni_hack_hook_count = (mod.omni_hack_hook_count or 0) + 1
if template_name == HACK_TAG_NAME and mod_settings.debug_mode and not mod.omni_hack_hook_fired then
mod.omni_hack_hook_fired = true
local ext = target_unit and self._unit_extension_data[target_unit]
mod:diag("OMNI-HACK-DIAG hook FIRE is_server=", tostring(self._is_server), " flagged=", tostring(ext and ext._automark_hack_order), " has_ext=", tostring(ext ~= nil), " target=", tostring(target_unit), " loc=", tostring(target_location))
end
if template_name == HACK_TAG_NAME and target_unit and self._is_server then
local target_extension = self._unit_extension_data[target_unit]
if target_extension and target_extension._automark_hack_order then
local ok, err = pcall(function()
local tag_id = self:_generate_tag_id()
local tag = self:_create_tag_locally(tag_id, template_name, tagger_unit, target_unit, target_location)
self:_server_check_tag_group_limit(tagger_unit, tag:group())
local template_name_id = NetworkLookup.smart_tag_templates[template_name]
local tagger_game_object_id = Managers.state.unit_spawner:game_object_id(tagger_unit)
local target_game_object_id = Managers.state.unit_spawner:game_object_id(target_unit)
Managers.state.game_session:send_rpc_clients("rpc_set_smart_tag", tag_id, template_name_id, tagger_game_object_id, target_game_object_id, nil, target_location)
end)
if ok then
return
end
if mod_settings.debug_mode and not mod.omni_hack_taghook_err_logged then
mod.omni_hack_taghook_err_logged = true
mod:diag("OMNI-HACK-DIAG tag-branch replication FAILED:", tostring(err))
end
end
end
return previous_hook(self, template_name, tagger_unit, target_unit, target_location)
end)
mod.omni_hack_tag_hooked = true
mod.omni_hack_hooked_ptr = tostring(cls.set_tag)
pcall(function()
cls.set_tag.omni_hook_marker = true
end)
if mod_settings.debug_mode then
mod:diag("OMNI-HACK-DIAG set_tag hook INSTALLED cls_ptr=", tostring(cls.set_tag), " sys_ptr=", tostring(smart_tag_system.set_tag), " same_sys_obj=", tostring(smart_tag_system == context.smart_tag_system))
end
elseif mod_settings.debug_mode and not mod.omni_hack_taghook_fail_logged then
mod.omni_hack_taghook_fail_logged = true
mod:diag("OMNI-HACK-DIAG set_tag hook FAILED to locate class, mt=", tostring(cls))
end
end
local function servo_skull_mark_callback()
local smart_tag_system = context.smart_tag_system
if not smart_tag_system then
return
end
local target_unit = mod:find_target_unit()
if not target_unit then
return
end
local target_tag = smart_tag_system:unit_tag(target_unit)
local tag_name = TAG_NAMES.SERVO_SKULL_TAG
mod:set_manual_mark(tag_name, target_unit, target_tag)
end
local function enemy_mark_callback()
local smart_tag_system = context.smart_tag_system
if not smart_tag_system then
return
end
local target_unit = mod:find_target_unit()
if not target_unit then
return
end
local target_tag = smart_tag_system:unit_tag(target_unit)
if target_tag then
return
end
local tag_name = TAG_NAMES.ENEMY_TAG
mod:set_manual_mark(tag_name, target_unit, target_tag)
end
mod.servo_skull_mark = function()
if not context.mod_enabled or not context.game_mode_valid or context.class_name ~= "cryptic" or not context.has_servo_skull then
return
end
local companion_command_tap = context.companion_command_tap
if companion_command_tap == "double" then
local cb = callback(servo_skull_mark_callback)
Managers.state.game_mode:register_physics_safe_callback(cb)
elseif companion_command_tap == "single" then
local cb = callback(enemy_mark_callback)
Managers.state.game_mode:register_physics_safe_callback(cb)
end
end
local function hack_mark_callback()
local smart_tag_system = context.smart_tag_system
if not smart_tag_system then
return
end
local hud_element_smart_tagging = context.hud_element_smart_tagging
if not hud_element_smart_tagging then
return
end
local player = context.player
local player_unit = player and player.player_unit
if not player_unit then
return
end
local target_marker, _ = hud_element_smart_tagging:_find_world_marker_target()
local target_unit = target_marker and target_marker.unit
if not target_unit then
return
end
local target_extension = smart_tag_system._unit_extension_data[target_unit]
local template_name = target_extension and target_extension:_contextual_tag_template_name(player_unit, "companion_order")
if template_name ~= HACK_TAG_NAME then
return
end
local target_tag = smart_tag_system:unit_tag(target_unit)
local tagger_player = target_tag and target_tag._tagger_player
local tagger_player_unit = tagger_player and tagger_player.player_unit
if tagger_player_unit then
smart_tag_system:cancel_tag(target_tag._id, tagger_player_unit, true)
end
smart_tag_system:set_tag(HACK_TAG_NAME, player_unit, target_unit)
end
mod.hack_mark = function()
if not context.mod_enabled or not context.game_mode_valid or context.class_name ~= "cryptic" or not context.has_servo_skull then
return
end
local cb = callback(hack_mark_callback)
Managers.state.game_mode:register_physics_safe_callback(cb)
end
function mod:is_servo_skull_hacking()
local companion_spawner_extension = context.companion_spawner_extension
local companion_unit = companion_spawner_extension and companion_spawner_extension:spawned_unit_lookup(special_rules.cryptic_servo_skull_hack)
if not companion_unit then
return false
end
local game_session = Managers.state.game_session:game_session()
local game_object_id = Managers.state.unit_spawner:game_object_id(companion_unit)
local state = get_game_object_field(game_session, game_object_id, "state")
return state == servo_skull_states.hacking
end
function mod:is_noospheric_command_boost_breed_valid(target_unit)
local unit_data_extension = ScriptUnit_extension(target_unit, "unit_data_system")
local breed_data = unit_data_extension and unit_data_extension._breed
if not breed_data then
return false
end
local breed_name = breed_data.name
local breed_settings = noospheric_command_breed_settings[breed_name]
if breed_settings and breed_settings.override then
return breed_settings.toggle
end
if breed_data.is_boss then
return mod_settings.noospheric_command_boost_boss
elseif breed_data.tags.special then
return mod_settings.noospheric_command_boost_special
else
return mod_settings.noospheric_command_boost_elite
end
end
local find_hackable_target_unit = function()
local hud_element_smart_tagging = context.hud_element_smart_tagging
if not hud_element_smart_tagging then
return
end
local smart_tag_system = context.smart_tag_system
if not smart_tag_system then
return
end
Managers.event:trigger("request_world_markers_list", callback(hud_element_smart_tagging, "_cb_world_markers_list_request"))
local world_markers_list = hud_element_smart_tagging._world_markers_list
local hud_scale = Hud.hud_scale()
local selected_marker_distance = math.huge
local selected_marker_unit
local parent = hud_element_smart_tagging._parent
local player = parent:player()
local player_unit = player.player_unit
local resolution_width, resolution_height = RESOLUTION_LOOKUP.width, RESOLUTION_LOOKUP.height
local center_x, center_y = 0.5 * resolution_width, 0.5 * resolution_height
local radius = resolution_height / 8 * 3
local radius_squared = radius * radius
for i = 1, #world_markers_list do
local marker = world_markers_list[i]
local widget = marker.widget
if widget then
local distance = widget.content.distance
if hud_element_smart_tagging:_is_marker_valid_for_tagging(player_unit, marker, distance) then
local offset = widget.offset
local x = offset[1] * hud_scale
local y = offset[2] * hud_scale
local dx = x - center_x
local dy = y - center_y
if dx * dx + dy * dy <= radius_squared and distance < selected_marker_distance then
local marker_unit = marker.unit
local target_extension = smart_tag_system._unit_extension_data[marker_unit]
local template_name = target_extension and target_extension:_contextual_tag_template_name(player_unit, "companion_order")
if template_name == HACK_TAG_NAME then
selected_marker_unit = marker_unit
selected_marker_distance = distance
end
end
end
end
end
return selected_marker_unit, selected_marker_distance
end
local OMNI_HACK_RANGE = 45
local HACK_RETRY_DELAY = 30
mod._hack_blacklist = mod._hack_blacklist or {}
-- NOTE: the game's _contextual_tag_template_name() is forward-LOS gated (measured in live play:
-- hackable=0 all session for all 15 nearby smart-taggable units, even under the crosshair),
-- so we pick the target ourselves with the same enemy filter the marking path uses and let
-- auto_hack()'s set_tag() do the actual tagging - same call in-game smart tagging performs.
-- REAL SEMANTICS (per game source cryptic_double_tag_settings): double-ping on an enemy breed =
-- shooting tag (handled by the normal marking passes); double-ping on a "hack" smart-target
-- whose interactee is decoding = hacking_over_here_companion -> skull detaches, flies over,
-- auto-decrypts. So the omni scan must look for DECODING OBJECTS, not enemies.
local function find_hackable_target_unit_omni()
local smart_tag_system = context.smart_tag_system
local player = context.player
local player_unit = player and player.player_unit
if not smart_tag_system or not player_unit then
return
end
ensure_automark_tag_hook()
local player_position = Unit_world_position(player_unit, 1)
local selected_unit, selected_distance, selected_name, selected_extension
local diag_total
if mod_settings.debug_mode then
diag_total = 0
end
local diag_samples = mod_settings.debug_mode and {} or nil
for unit, target_extension in pairs(smart_tag_system._unit_extension_data) do
if unit ~= player_unit and target_extension._target_type == "hack" and not mod._hack_blacklist[unit] then
local interactee_extension = ScriptUnit_extension(unit, "interactee_system")
local decoding = interactee_extension ~= nil and interactee_extension:interaction_type() == "decoding" and interactee_extension:can_interact(unit, "decoding")
if decoding then
if diag_total then
diag_total = diag_total + 1
end
local distance = Vector3_distance(Unit_world_position(unit, 1), player_position)
if diag_samples and #diag_samples < 10 then
diag_samples[#diag_samples + 1] = tostring(distance)
end
if distance <= OMNI_HACK_RANGE and (not selected_distance or distance < selected_distance) then
selected_unit, selected_distance = unit, distance
selected_extension = target_extension
selected_name = "hack_object"
end
end
end
end
if diag_total then
mod.omni_hack_diag_counter = (mod.omni_hack_diag_counter or 0) + 1
if mod.omni_hack_diag_counter % 10 == 1 then
mod:diag(string.format("OMNI-HACK-DIAG decoding_objects=%d selected=%s dist=%s", diag_total, tostring(selected_unit), tostring(selected_distance)))
end
if not selected_unit and diag_total > 0 and mod.omni_hack_diag_counter % 50 == 1 then
mod:diag("OMNI-HACK-DIAG no-selection samples: " .. table.concat(diag_samples, " "))
end
end
if selected_unit then
-- don't retarget the same unit forever if the game refuses to accept the tag
mod._hack_blacklist[selected_unit] = { t = HACK_RETRY_DELAY, n = selected_name, ext = selected_extension }
if selected_extension then
selected_extension._automark_hack_order = true
end
end
return selected_unit, selected_distance
end
local hack_interval = 0
local hack_check = nil
function mod:auto_hack(dt, t, fixed_frame)
if not mod_settings.auto_hack or (mod_settings.disable_auto_hack_for_noospheric_command and context.has_noospheric_command) or context.class_name ~= "cryptic" or not context.has_servo_skull then
return
end
for unit, entry in pairs(mod._hack_blacklist) do
entry.t = entry.t - dt
if entry.t <= 0 then
if entry.ext then
entry.ext._automark_hack_order = nil
end
mod._hack_blacklist[unit] = nil
end
end
if hack_check then
hack_check.ttl = hack_check.ttl - dt
if hack_check.ttl <= 0 then
if mod_settings.debug_mode then
local smart_tag_system = context.smart_tag_system
mod:diag(string.format("OMNI-HACK-DIAG tagcheck %s alive=%s tag_applied=%s skull_hacking=%s hooked=%s hookcalls=%s", tostring(hack_check.n), tostring(HEALTH_ALIVE[hack_check.unit] == true), tostring(smart_tag_system and smart_tag_system:unit_tag(hack_check.unit) ~= nil), tostring(mod:is_servo_skull_hacking()), tostring(mod.omni_hack_tag_hooked), tostring(mod.omni_hack_hook_count)))
end
hack_check = nil
end
end
if hack_interval > 0 then
hack_interval = hack_interval - dt
return
end
if mod:is_servo_skull_hacking() then
return
end
local smart_tag_system = context.smart_tag_system
if not smart_tag_system then
return
end
local player = context.player
local player_unit = player and player.player_unit
if not player_unit then
return
end
local target_unit, _ = mod_settings.omni_mark and find_hackable_target_unit_omni() or find_hackable_target_unit()
if not target_unit then
return
end
if mod_settings.debug_mode and not mod.omni_hack_ptr_logged then
mod.omni_hack_ptr_logged = true
local marker
pcall(function()
marker = smart_tag_system.set_tag.omni_hook_marker
end)
mod:diag("OMNI-HACK-DIAG call-site marker_on_calling_fn=", tostring(marker), " hook_calls_before_this=", tostring(mod.omni_hack_hook_count))
end
local target_tag = smart_tag_system:unit_tag(target_unit)
local tagger_player = target_tag and target_tag._tagger_player
local tagger_player_unit = tagger_player and tagger_player.player_unit
if tagger_player_unit then
smart_tag_system:cancel_tag(target_tag._id, tagger_player_unit, true)
end
hack_interval = 1
smart_tag_system:set_tag(HACK_TAG_NAME, player_unit, target_unit)
hack_check = { unit = target_unit, ttl = 1.0, n = mod._hack_blacklist[target_unit] and mod._hack_blacklist[target_unit].n or "?" }
end
local cancel_interval = 0
function mod:auto_cancel_servo_skull_mark(dt, t, fixed_frame)
if not mod_settings.servo_skull_cancel_mark or context.class_name ~= "cryptic" or not context.has_servo_skull then
return
end
if cancel_interval > 0 then
cancel_interval = cancel_interval - dt
return
end
local disabled_character_state_component = context.disabled_character_state_component
if disabled_character_state_component and disabled_character_state_component.is_disabled and DISABLING_TYPES[disabled_character_state_component.disabling_type] then
return
end
local tag_context = mark_context[TAG_NAMES.SERVO_SKULL_TAG]
local marked_tag = tag_context.tag
local marked_unit = marked_tag and marked_tag._target_unit
if not marked_tag or not marked_unit then
return
end
if tag_context.is_manual then
return
end
local unit_data_extension = ScriptUnit_extension(marked_unit, "unit_data_system")
local breed_data = unit_data_extension and unit_data_extension._breed
local breed_name = breed_data and breed_data.name
local breed_settings = noospheric_command_breed_settings[breed_name]
local time_threshold, health_threshold
if breed_settings and breed_settings.override then
health_threshold = breed_settings.health_threshold or 0
time_threshold = breed_settings.time_threshold or 0
else
time_threshold = mod_settings.servo_skull_cancel_mark_time_threshold
health_threshold = mod_settings.servo_skull_cancel_mark_health_threshold
end
if time_threshold > 0 then
if mod:is_servo_skull_target_visible(marked_unit, fixed_frame, true) then
tag_context.servo_skull_lose_sight_time = nil
elseif not tag_context.servo_skull_lose_sight_time then
tag_context.servo_skull_lose_sight_time = t
end
if tag_context.servo_skull_lose_sight_time and t - tag_context.servo_skull_lose_sight_time > time_threshold then
mod:print_debug("cancel servo skull mark due to time threshold")
tag_context.canceled_units[marked_unit] = t + 0.3
cancel_interval = CANCEL_INTERVAL
mod:cancel_mark(marked_tag._id)
end
end
if health_threshold > 0 and tag_context.shoot_start_time then
local health_percent = Health.current_health_percent(marked_unit)
if health_percent < health_threshold then
mod:print_debug("cancel servo skull mark due to health threshold, health_percent:", health_percent)
tag_context.canceled_units[marked_unit] = t + 3
cancel_interval = CANCEL_INTERVAL
mod:cancel_mark(marked_tag._id)
return
end
end
end