using System; using System.Numerics; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.NativeWrapper; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel; using Lumina.Excel.Sheets; namespace QuestionableCompanion.Services; public class DeathHandlerService : IDisposable { private readonly ICondition condition; private readonly IPluginLog log; private readonly IClientState clientState; private readonly ICommandManager commandManager; private readonly IFramework framework; private readonly Configuration config; private readonly IGameGui gameGui; private readonly IDataManager dataManager; private bool wasDead; private Vector3 deathPosition = Vector3.Zero; private uint deathTerritoryId; private DateTime deathTime = DateTime.MinValue; private bool hasClickedYesNo; private bool needsTeleportBack; private bool isRotationActive; public bool IsDead { get; private set; } public TimeSpan TimeSinceDeath { get { if (!IsDead || !(deathTime != DateTime.MinValue)) { return TimeSpan.Zero; } return DateTime.Now - deathTime; } } public DeathHandlerService(ICondition condition, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework, Configuration config, IGameGui gameGui, IDataManager dataManager) { this.condition = condition; this.log = log; this.clientState = clientState; this.commandManager = commandManager; this.framework = framework; this.config = config; this.gameGui = gameGui; this.dataManager = dataManager; log.Information("[DeathHandler] Service initialized"); } public void SetRotationActive(bool active) { isRotationActive = active; } public unsafe void Update() { if (clientState.LocalPlayer == null || !clientState.IsLoggedIn || !config.EnableDeathHandling) { return; } IPlayerCharacter player = clientState.LocalPlayer; uint currentHp = player.CurrentHp; if (currentHp == 0 && !wasDead) { IsDead = true; wasDead = true; deathTime = DateTime.Now; deathPosition = player.Position; deathTerritoryId = clientState.TerritoryType; hasClickedYesNo = false; needsTeleportBack = false; string territoryName = GetTerritoryName(deathTerritoryId); log.Warning("[DeathHandler] ========================================"); log.Warning("[DeathHandler] === PLAYER DIED (0% HP) ==="); log.Warning("[DeathHandler] ========================================"); log.Information($"[DeathHandler] Death Position: {deathPosition}"); log.Information($"[DeathHandler] Death Territory: {territoryName} ({deathTerritoryId})"); } if (IsDead && !hasClickedYesNo) { try { AtkUnitBasePtr addonPtr = gameGui.GetAddonByName("SelectYesno"); if (addonPtr != IntPtr.Zero) { log.Information("[DeathHandler] SelectYesNo addon detected - clicking YES"); AtkUnitBase* addon = (AtkUnitBase*)(nint)addonPtr; if (addon != null && addon->IsVisible) { AtkValue* values = stackalloc AtkValue[1]; *values = new AtkValue { Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int, Int = 0 }; addon->FireCallback(1u, values); hasClickedYesNo = true; log.Information("[DeathHandler] ✓ SelectYesNo callback fired (YES)"); log.Information("[DeathHandler] Waiting for respawn..."); } } } catch (Exception ex) { log.Error("[DeathHandler] Error clicking SelectYesNo: " + ex.Message); } } if (wasDead && currentHp != 0 && !player.IsDead) { log.Information("[DeathHandler] ========================================"); log.Information("[DeathHandler] === PLAYER RESPAWNED ==="); log.Information("[DeathHandler] ========================================"); IsDead = false; wasDead = false; needsTeleportBack = true; log.Information("[DeathHandler] Preparing to teleport back to death location..."); } if (needsTeleportBack && !IsDead && currentHp != 0 && (DateTime.Now - deathTime).TotalSeconds >= (double)config.DeathRespawnDelay) { TeleportBackToDeathLocation(); needsTeleportBack = false; } } private string GetTerritoryName(uint territoryId) { try { ExcelSheet territorySheet = dataManager.GetExcelSheet(); if (territorySheet == null) { return territoryId.ToString(); } if (!territorySheet.HasRow(territoryId)) { return territoryId.ToString(); } RowRef placeNameRef = territorySheet.GetRow(territoryId).PlaceName; if (placeNameRef.RowId == 0) { return territoryId.ToString(); } PlaceName? placeName = placeNameRef.ValueNullable; if (!placeName.HasValue) { return territoryId.ToString(); } string name = placeName.Value.Name.ExtractText(); if (!string.IsNullOrEmpty(name)) { return name; } } catch (Exception ex) { log.Error("[DeathHandler] Error getting territory name: " + ex.Message); } return territoryId.ToString(); } private void TeleportBackToDeathLocation() { try { string territoryName = GetTerritoryName(deathTerritoryId); log.Information("[DeathHandler] ========================================"); log.Information("[DeathHandler] === TELEPORTING BACK TO DEATH LOCATION ==="); log.Information("[DeathHandler] ========================================"); log.Information($"[DeathHandler] Target Territory: {territoryName} ({deathTerritoryId})"); log.Information($"[DeathHandler] Target Position: {deathPosition}"); if (clientState.TerritoryType == deathTerritoryId) { log.Information("[DeathHandler] Already in death territory - using /li to teleport to position"); framework.RunOnFrameworkThread(delegate { try { string text = $"/li {deathPosition.X:F2}, {deathPosition.Y:F2}, {deathPosition.Z:F2}"; commandManager.ProcessCommand(text); log.Information("[DeathHandler] ✓ Teleport to position: " + text); } catch (Exception ex2) { log.Error("[DeathHandler] Failed to send teleport command: " + ex2.Message); } }).Wait(); } else { log.Information("[DeathHandler] Different territory - teleporting to death territory"); framework.RunOnFrameworkThread(delegate { try { string text = "/li " + territoryName; commandManager.ProcessCommand(text); log.Information("[DeathHandler] ✓ Teleport to territory: " + text); } catch (Exception ex2) { log.Error("[DeathHandler] Failed to send teleport command: " + ex2.Message); } }).Wait(); } deathPosition = Vector3.Zero; deathTerritoryId = 0u; deathTime = DateTime.MinValue; log.Information("[DeathHandler] Death handling complete"); } catch (Exception ex) { log.Error("[DeathHandler] Error during teleport: " + ex.Message); } } public void Reset() { IsDead = false; wasDead = false; needsTeleportBack = false; hasClickedYesNo = false; deathPosition = Vector3.Zero; deathTerritoryId = 0u; deathTime = DateTime.MinValue; log.Information("[DeathHandler] State reset"); } public void Dispose() { } }