using Microsoft.Extensions.Logging; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.PlayerData.Factories; using LightlessSync.Services; using LightlessSync.Services.ActorTracking; using LightlessSync.Interop.Ipc; using LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Handlers; internal sealed class OwnedObjectHandler { private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly GameObjectHandlerFactory _handlerFactory; private readonly IpcManager _ipc; private readonly ActorObjectService _actorObjectService; private const int _fullyLoadedTimeoutMsPlayer = 30000; private const int _fullyLoadedTimeoutMsOther = 5000; public OwnedObjectHandler( ILogger logger, DalamudUtilService dalamudUtil, GameObjectHandlerFactory handlerFactory, IpcManager ipc, ActorObjectService actorObjectService) { _logger = logger; _dalamudUtil = dalamudUtil; _handlerFactory = handlerFactory; _ipc = ipc; _actorObjectService = actorObjectService; } public async Task ApplyAsync( Guid applicationId, ObjectKind kind, HashSet changes, CharacterData data, GameObjectHandler playerHandler, Guid penumbraCollection, Dictionary customizeIds, CancellationToken token) { if (playerHandler.Address == nint.Zero) return false; var handler = await CreateHandlerAsync(kind, playerHandler, token).ConfigureAwait(false); if (handler is null || handler.Address == nint.Zero) return false; try { token.ThrowIfCancellationRequested(); bool hasFileReplacements = kind != ObjectKind.Player && data.FileReplacements.TryGetValue(kind, out var repls) && repls is { Count: > 0 }; bool shouldAssignCollection = kind != ObjectKind.Player && hasFileReplacements && penumbraCollection != Guid.Empty && _ipc.Penumbra.APIAvailable; bool isPlayerIpcOnly = kind == ObjectKind.Player && changes.Count > 0 && changes.All(c => c is PlayerChanges.Honorific or PlayerChanges.Moodles or PlayerChanges.PetNames or PlayerChanges.Heels); await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false); var drawTimeoutMs = handler.ObjectKind == ObjectKind.Player ? 30000 : 5000; var fullyLoadedTimeoutMs = handler.ObjectKind == ObjectKind.Player ? _fullyLoadedTimeoutMsPlayer : _fullyLoadedTimeoutMsOther; await _dalamudUtil .WaitWhileCharacterIsDrawing(_logger, handler, applicationId, drawTimeoutMs, token) .ConfigureAwait(false); if (handler.Address != nint.Zero) { var loaded = await _actorObjectService .WaitForFullyLoadedAsync(handler.Address, token, fullyLoadedTimeoutMs) .ConfigureAwait(false); if (!loaded) { _logger.LogTrace("[{appId}] {kind}: not fully loaded in time, skipping for now", applicationId, kind); return false; } } token.ThrowIfCancellationRequested(); if (shouldAssignCollection) { var objIndex = await _dalamudUtil .RunOnFrameworkThread(() => handler.GetGameObject()?.ObjectIndex) .ConfigureAwait(false); if (!objIndex.HasValue) { _logger.LogTrace("[{appId}] {kind}: ObjectIndex not available yet, cannot assign collection", applicationId, kind); return false; } await _ipc.Penumbra .AssignTemporaryCollectionAsync(_logger, penumbraCollection, objIndex.Value) .ConfigureAwait(false); } var tasks = new List(); foreach (var change in changes.OrderBy(c => (int)c)) { token.ThrowIfCancellationRequested(); switch (change) { case PlayerChanges.Customize: if (data.CustomizePlusData.TryGetValue(kind, out var customizeData) && !string.IsNullOrEmpty(customizeData)) tasks.Add(ApplyCustomizeAsync(handler.Address, customizeData, kind, customizeIds)); else if (customizeIds.TryGetValue(kind, out var existingId)) tasks.Add(RevertCustomizeAsync(existingId, kind, customizeIds)); break; case PlayerChanges.Glamourer: if (data.GlamourerData.TryGetValue(kind, out var glamourerData) && !string.IsNullOrEmpty(glamourerData)) tasks.Add(_ipc.Glamourer.ApplyAllAsync(_logger, handler, glamourerData, applicationId, token)); break; case PlayerChanges.Heels: if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.HeelsData)) tasks.Add(_ipc.Heels.SetOffsetForPlayerAsync(handler.Address, data.HeelsData)); break; case PlayerChanges.Honorific: if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.HonorificData)) tasks.Add(_ipc.Honorific.SetTitleAsync(handler.Address, data.HonorificData)); break; case PlayerChanges.Moodles: if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.MoodlesData)) tasks.Add(_ipc.Moodles.SetStatusAsync(handler.Address, data.MoodlesData)); break; case PlayerChanges.PetNames: if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.PetNamesData)) tasks.Add(_ipc.PetNames.SetPlayerData(handler.Address, data.PetNamesData)); break; case PlayerChanges.ModFiles: case PlayerChanges.ModManip: case PlayerChanges.ForcedRedraw: default: break; } } if (tasks.Count > 0) await Task.WhenAll(tasks).ConfigureAwait(false); token.ThrowIfCancellationRequested(); bool needsRedraw = _ipc.Penumbra.APIAvailable && ( shouldAssignCollection || changes.Contains(PlayerChanges.ForcedRedraw) || changes.Contains(PlayerChanges.ModFiles) || changes.Contains(PlayerChanges.ModManip) || changes.Contains(PlayerChanges.Glamourer) || changes.Contains(PlayerChanges.Customize) ); if (isPlayerIpcOnly) needsRedraw = false; if (needsRedraw && _ipc.Penumbra.APIAvailable) { _logger.LogWarning( "[{appId}] {kind}: Redrawing ownedTarget={isOwned} (needsRedraw={needsRedraw})", applicationId, kind, kind != ObjectKind.Player, needsRedraw); await _ipc.Penumbra .RedrawAsync(_logger, handler, applicationId, token) .ConfigureAwait(false); } return true; } finally { if (!ReferenceEquals(handler, playerHandler)) handler.Dispose(); } } private async Task CreateHandlerAsync(ObjectKind kind, GameObjectHandler playerHandler, CancellationToken token) { if (kind == ObjectKind.Player) return playerHandler; var playerPtr = playerHandler.Address; nint ownedPtr = kind switch { ObjectKind.Companion => await _dalamudUtil.GetCompanionAsync(playerPtr).ConfigureAwait(false), ObjectKind.MinionOrMount => await _dalamudUtil.GetMinionOrMountAsync(playerPtr).ConfigureAwait(false), ObjectKind.Pet => await _dalamudUtil.GetPetAsync(playerPtr).ConfigureAwait(false), _ => nint.Zero }; if (ownedPtr == nint.Zero) return null; return await _handlerFactory.Create(kind, () => ownedPtr, isWatched: false).ConfigureAwait(false); } private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind, Dictionary customizeIds) { customizeIds[kind] = await _ipc.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false); } private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind, Dictionary customizeIds) { if (!customizeId.HasValue) return; await _ipc.CustomizePlus.RevertByIdAsync(customizeId.Value).ConfigureAwait(false); customizeIds.Remove(kind); } }