using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.Interop; using Lumina.Excel.GeneratedSheets; namespace OrangeGuidanceTomestone.Util; internal class ActorManager : IDisposable { private Plugin Plugin { get; } private readonly Stack _idx = []; private readonly Queue _tasks = []; internal ActorManager(Plugin plugin) { this.Plugin = plugin; this.Plugin.Framework.Update += this.OnFramework; this.Plugin.ClientState.TerritoryChanged += this.OnTerritoryChange; this.Plugin.Ui.Viewer.View += this.OnView; } public void Dispose() { this.Plugin.Ui.Viewer.View -= this.OnView; this.Plugin.ClientState.TerritoryChanged -= this.OnTerritoryChange; this.Plugin.Framework.Update -= this.OnFramework; if (this._idx.Count > 0) { unsafe { var objMan = ClientObjectManager.Instance(); new DeleteAction().Run(this, objMan); } } } private unsafe void OnFramework(IFramework framework) { if (!this._tasks.TryPeek(out var actorAction)) { return; } var objMan = ClientObjectManager.Instance(); var success = false; if (actorAction.Tries < 10) { try { actorAction.Tries += 1; success = actorAction.Run(this, objMan); } catch (Exception ex) { Plugin.Log.Error(ex, "Error in actor action queue"); } } else { Plugin.Log.Warning("too many retries, skipping"); success = true; } if (success) { this._tasks.Dequeue(); } } private void OnTerritoryChange(ushort obj) { this._idx.Clear(); } private void OnView(Message? message) { var msg = message == null ? "null" : "not null"; Plugin.Log.Debug($"OnView message is {msg}"); this.Despawn(); if (this.Plugin.Config.ShowEmotes && message?.Emote != null) { this.Spawn(message); } } internal void Spawn(Message message) { this._tasks.Enqueue(new SpawnAction(message)); } internal void Despawn() { this._tasks.Enqueue(new DeleteAction()); } private abstract unsafe class BaseActorAction { /// /// Run this action. /// /// true if the action is finished, false if it should be run again public abstract bool Run(ActorManager manager, ClientObjectManager* objMan); public int Tries { get; set; } protected IEnumerable> GetBattleCharas( ActorManager manager, Pointer objMan ) { foreach (var idx in manager._idx) { Pointer ptr; unsafe { var obj = (BattleChara*) objMan.Value->GetObjectByIndex((ushort) idx); if (obj == null) { continue; } ptr = obj; } yield return ptr; } } } private unsafe class SpawnAction(Message message) : BaseActorAction { public override bool Run(ActorManager manager, ClientObjectManager* objMan) { if (message.Emote == null) { Plugin.Log.Warning("refusing to spawn an actor for a message without an emote"); return true; } var idx = objMan->CreateBattleCharacter(); if (idx == 0xFFFFFFFF) { Plugin.Log.Debug("actor could not be spawned"); return true; } manager._idx.Push(idx); var emote = message.Emote; var emoteRow = manager.GetValidEmote(emote.Id); var chara = (BattleChara*) objMan->GetObjectByIndex((ushort) idx); chara->ObjectKind = ObjectKind.BattleNpc; chara->TargetableStatus = 0; chara->Position = message.Position; chara->Rotation = message.Yaw; var drawData = &chara->DrawData; var maxLen = Math.Min(sizeof(CustomizeData), emote.Customise.Count); var rawCustomise = (byte*) &drawData->CustomizeData; for (var i = 0; i < maxLen; i++) { rawCustomise[i] = emote.Customise[i]; } // check if data is valid to prevent crashes if (!(&drawData->CustomizeData)->NormalizeCustomizeData(&drawData->CustomizeData)) { drawData->CustomizeData = new CustomizeData(); } // weapon and equipment values don't cause crashes, just transparent body parts for (var i = 0; i < Math.Min(drawData->EquipmentModelIds.Length, emote.Equipment.Length); i++) { var equip = emote.Equipment[i]; drawData->Equipment((DrawDataContainer.EquipmentSlot) i) = new EquipmentModelId { Id = equip.Id, Variant = equip.Variant, Stain0 = equip.Stain0, Stain1 = equip.Stain1, }; } if (emoteRow is { DrawsWeapon: true }) { for (var i = 0; i < Math.Min(drawData->WeaponData.Length, emote.Weapon.Length); i++) { var weapon = emote.Weapon[i]; drawData->Weapon((DrawDataContainer.WeaponSlot) i).ModelId = new FFXIVClientStructs.FFXIV.Client.Game.Character.WeaponModelId { Id = weapon.ModelId.Id, Type = weapon.ModelId.Kind, Variant = weapon.ModelId.Variant, Stain0 = weapon.ModelId.Stain0, Stain1 = weapon.ModelId.Stain1, }; drawData->Weapon((DrawDataContainer.WeaponSlot) i).Flags1 = weapon.Flags1; drawData->Weapon((DrawDataContainer.WeaponSlot) i).Flags2 = weapon.Flags2; drawData->Weapon((DrawDataContainer.WeaponSlot) i).State = weapon.State; } } drawData->IsHatHidden = emote.HatHidden; drawData->IsVisorToggled = emote.VisorToggled; drawData->IsWeaponHidden = emote.WeaponHidden; drawData->SetGlasses(0, (ushort) emote.Glasses); chara->Alpha = Math.Clamp(manager.Plugin.Config.EmoteAlpha / 100, 0, 1); chara->SetMode(CharacterModes.AnimLock, 0); if (emoteRow != null) { chara->Timeline.BaseOverride = (ushort) emoteRow.ActionTimeline[0].Row; } manager._tasks.Enqueue(new EnableAction()); return true; } } private Emote? GetValidEmote(uint rowId) { var emote = this.Plugin.DataManager.GetExcelSheet()?.GetRow(rowId); if (emote == null) { return null; } return emote.TextCommand.Row == 0 ? null : emote; } private unsafe class EnableAction : BaseActorAction { public override bool Run(ActorManager manager, ClientObjectManager* objMan) { var allReady = true; foreach (var chara in this.GetBattleCharas(manager, objMan)) { if (!chara.Value->IsReadyToDraw()) { allReady = false; continue; } chara.Value->EnableDraw(); } return allReady; } } private unsafe class DeleteAction : BaseActorAction { public override bool Run(ActorManager manager, ClientObjectManager* objMan) { foreach (var wrapper in this.GetBattleCharas(manager, objMan)) { wrapper.Value->DisableDraw(); var idx = objMan->GetIndexByObject((GameObject*) wrapper.Value); objMan->DeleteObjectByIndex((ushort) idx, 0); } manager._idx.Clear(); return true; } } }