488 lines
21 KiB
Lua
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
|