2024-07-22 05:44:50 +00:00
|
|
|
using Dalamud.Plugin.Services;
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
2024-07-22 07:44:30 +00:00
|
|
|
using Lumina.Excel.GeneratedSheets;
|
2024-07-22 05:44:50 +00:00
|
|
|
|
|
|
|
namespace OrangeGuidanceTomestone.Util;
|
|
|
|
|
|
|
|
internal class ActorManager : IDisposable {
|
|
|
|
private Plugin Plugin { get; }
|
|
|
|
private uint? _idx;
|
2024-07-22 06:22:54 +00:00
|
|
|
private readonly Queue<BaseActorAction> _tasks = [];
|
2024-07-22 05:44:50 +00:00
|
|
|
|
|
|
|
internal ActorManager(Plugin plugin) {
|
|
|
|
this.Plugin = plugin;
|
|
|
|
this.Plugin.Framework.Update += this.OnFramework;
|
2024-07-22 18:33:02 +00:00
|
|
|
this.Plugin.ClientState.TerritoryChanged += this.OnTerritoryChange;
|
2024-07-22 05:44:50 +00:00
|
|
|
this.Plugin.Ui.Viewer.View += this.OnView;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Dispose() {
|
|
|
|
this.Plugin.Ui.Viewer.View -= this.OnView;
|
2024-07-22 18:33:02 +00:00
|
|
|
this.Plugin.ClientState.TerritoryChanged -= this.OnTerritoryChange;
|
2024-07-22 05:44:50 +00:00
|
|
|
this.Plugin.Framework.Update -= this.OnFramework;
|
2024-07-22 09:49:51 +00:00
|
|
|
|
|
|
|
if (this._idx != null) {
|
|
|
|
unsafe {
|
|
|
|
var objMan = ClientObjectManager.Instance();
|
|
|
|
new DisableAction().Run(this, objMan);
|
|
|
|
new DeleteAction().Run(this, objMan);
|
|
|
|
}
|
|
|
|
}
|
2024-07-22 05:44:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private unsafe void OnFramework(IFramework framework) {
|
2024-07-22 06:22:54 +00:00
|
|
|
if (!this._tasks.TryPeek(out var actorAction)) {
|
2024-07-22 05:44:50 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var objMan = ClientObjectManager.Instance();
|
2024-07-22 06:22:54 +00:00
|
|
|
var success = false;
|
2024-07-22 05:44:50 +00:00
|
|
|
|
2024-07-22 06:22:54 +00:00
|
|
|
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");
|
2024-07-22 05:44:50 +00:00
|
|
|
}
|
2024-07-22 06:22:54 +00:00
|
|
|
} else {
|
|
|
|
Plugin.Log.Warning("too many retries, skipping");
|
|
|
|
success = true;
|
2024-07-22 05:44:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (success) {
|
2024-07-22 06:22:54 +00:00
|
|
|
this._tasks.Dequeue();
|
2024-07-22 05:44:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-22 18:33:02 +00:00
|
|
|
private void OnTerritoryChange(ushort obj) {
|
|
|
|
this._idx = null;
|
|
|
|
}
|
|
|
|
|
2024-07-22 05:44:50 +00:00
|
|
|
private void OnView(Message? message) {
|
2024-07-22 06:07:05 +00:00
|
|
|
var msg = message == null ? "null" : "not null";
|
|
|
|
Plugin.Log.Debug($"OnView message is {msg}");
|
2024-07-22 05:44:50 +00:00
|
|
|
this.Despawn();
|
|
|
|
|
2024-07-22 09:49:51 +00:00
|
|
|
if (this.Plugin.Config.ShowEmotes && message?.Emote != null) {
|
2024-07-22 05:44:50 +00:00
|
|
|
this.Spawn(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-22 06:44:22 +00:00
|
|
|
internal void Spawn(Message message) {
|
|
|
|
this._tasks.Enqueue(new SpawnAction(message));
|
2024-07-22 06:22:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
internal void Despawn() {
|
|
|
|
if (this._idx == null) {
|
2024-07-22 05:44:50 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-07-22 06:22:54 +00:00
|
|
|
this._tasks.Enqueue(new DisableAction());
|
|
|
|
this._tasks.Enqueue(new DeleteAction());
|
|
|
|
}
|
|
|
|
|
2024-07-22 06:44:22 +00:00
|
|
|
private abstract unsafe class BaseActorAction {
|
2024-07-22 06:22:54 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Run this action.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns>true if the action is finished, false if it should be run again</returns>
|
|
|
|
public abstract bool Run(ActorManager manager, ClientObjectManager* objMan);
|
|
|
|
|
|
|
|
public int Tries { get; set; }
|
|
|
|
|
|
|
|
protected bool TryGetBattleChara(
|
|
|
|
ActorManager manager,
|
|
|
|
ClientObjectManager* objMan,
|
|
|
|
out BattleChara* chara
|
|
|
|
) {
|
|
|
|
chara = null;
|
|
|
|
|
|
|
|
if (manager._idx is not { } idx) {
|
|
|
|
Plugin.Log.Warning("tried to get battlechara but idx was null");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
var obj = objMan->GetObjectByIndex((ushort) idx);
|
|
|
|
if (obj == null) {
|
|
|
|
Plugin.Log.Warning("tried to get battlechara but it was null");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
chara = (BattleChara*) obj;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private unsafe class SpawnAction(Message message) : BaseActorAction {
|
|
|
|
public override bool Run(ActorManager manager, ClientObjectManager* objMan) {
|
|
|
|
if (manager._idx != null) {
|
|
|
|
Plugin.Log.Warning("refusing to spawn a second actor");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-07-22 07:44:30 +00:00
|
|
|
if (message.Emote == null) {
|
|
|
|
Plugin.Log.Warning("refusing to spawn an actor for a message without an emote");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-07-22 06:22:54 +00:00
|
|
|
var idx = objMan->CreateBattleCharacter();
|
|
|
|
if (idx == 0xFFFFFFFF) {
|
|
|
|
Plugin.Log.Debug("actor could not be spawned");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
manager._idx = idx;
|
2024-07-22 07:44:30 +00:00
|
|
|
var emote = message.Emote;
|
2024-07-22 09:49:51 +00:00
|
|
|
var emoteRow = manager.GetValidEmote(emote.Id);
|
2024-07-22 05:44:50 +00:00
|
|
|
|
2024-07-22 06:22:54 +00:00
|
|
|
var chara = (BattleChara*) objMan->GetObjectByIndex((ushort) idx);
|
2024-07-22 05:44:50 +00:00
|
|
|
|
2024-07-22 06:22:54 +00:00
|
|
|
chara->ObjectKind = ObjectKind.BattleNpc;
|
2024-07-22 06:44:22 +00:00
|
|
|
chara->TargetableStatus = 0;
|
2024-07-22 06:22:54 +00:00
|
|
|
chara->Position = message.Position;
|
|
|
|
chara->Rotation = message.Yaw;
|
|
|
|
var drawData = &chara->DrawData;
|
2024-07-22 07:44:30 +00:00
|
|
|
|
2024-07-22 09:49:51 +00:00
|
|
|
var maxLen = Math.Min(sizeof(CustomizeData), emote.Customise.Count);
|
2024-07-22 07:44:30 +00:00
|
|
|
var rawCustomise = (byte*) &drawData->CustomizeData;
|
|
|
|
for (var i = 0; i < maxLen; i++) {
|
|
|
|
rawCustomise[i] = emote.Customise[i];
|
|
|
|
}
|
|
|
|
|
2024-07-22 18:33:02 +00:00
|
|
|
// 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
|
2024-07-22 07:44:30 +00:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-07-22 09:49:51 +00:00
|
|
|
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 {
|
2024-07-22 07:44:30 +00:00
|
|
|
Id = weapon.ModelId.Id,
|
|
|
|
Type = weapon.ModelId.Kind,
|
|
|
|
Variant = weapon.ModelId.Variant,
|
|
|
|
Stain0 = weapon.ModelId.Stain0,
|
|
|
|
Stain1 = weapon.ModelId.Stain1,
|
2024-07-22 09:49:51 +00:00
|
|
|
};
|
|
|
|
drawData->Weapon((DrawDataContainer.WeaponSlot) i).Flags1 = weapon.Flags1;
|
|
|
|
drawData->Weapon((DrawDataContainer.WeaponSlot) i).Flags2 = weapon.Flags2;
|
|
|
|
drawData->Weapon((DrawDataContainer.WeaponSlot) i).State = weapon.State;
|
|
|
|
}
|
2024-07-22 07:44:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
drawData->IsHatHidden = emote.HatHidden;
|
|
|
|
drawData->IsVisorToggled = emote.VisorToggled;
|
|
|
|
drawData->IsWeaponHidden = emote.WeaponHidden;
|
|
|
|
|
|
|
|
drawData->SetGlasses(0, (ushort) emote.Glasses);
|
2024-07-22 05:44:50 +00:00
|
|
|
|
2024-07-22 18:33:02 +00:00
|
|
|
chara->Alpha = Math.Clamp(manager.Plugin.Config.EmoteAlpha / 100, 0, 1);
|
2024-07-22 06:22:54 +00:00
|
|
|
chara->SetMode(CharacterModes.AnimLock, 0);
|
2024-07-22 09:49:51 +00:00
|
|
|
if (emoteRow != null) {
|
|
|
|
chara->Timeline.BaseOverride = (ushort) emoteRow.ActionTimeline[0].Row;
|
2024-07-22 07:44:30 +00:00
|
|
|
}
|
2024-07-22 05:44:50 +00:00
|
|
|
|
2024-07-22 06:22:54 +00:00
|
|
|
manager._tasks.Enqueue(new EnableAction());
|
|
|
|
return true;
|
|
|
|
}
|
2024-07-22 05:44:50 +00:00
|
|
|
}
|
|
|
|
|
2024-07-22 09:49:51 +00:00
|
|
|
private Emote? GetValidEmote(uint rowId) {
|
|
|
|
var emote = this.Plugin.DataManager.GetExcelSheet<Emote>()?.GetRow(rowId);
|
|
|
|
if (emote == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return emote.TextCommand.Row == 0 ? null : emote;
|
|
|
|
}
|
|
|
|
|
2024-07-22 06:22:54 +00:00
|
|
|
private unsafe class EnableAction : BaseActorAction {
|
|
|
|
public override bool Run(ActorManager manager, ClientObjectManager* objMan) {
|
|
|
|
if (!this.TryGetBattleChara(manager, objMan, out var chara)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!chara->IsReadyToDraw()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
chara->EnableDraw();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private unsafe class DisableAction : BaseActorAction {
|
|
|
|
public override bool Run(ActorManager manager, ClientObjectManager* objMan) {
|
|
|
|
if (!this.TryGetBattleChara(manager, objMan, out var chara)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
chara->DisableDraw();
|
|
|
|
return true;
|
2024-07-22 06:07:05 +00:00
|
|
|
}
|
2024-07-22 06:22:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private unsafe class DeleteAction : BaseActorAction {
|
|
|
|
public override bool Run(ActorManager manager, ClientObjectManager* objMan) {
|
|
|
|
if (manager._idx is not { } idx) {
|
|
|
|
Plugin.Log.Warning("delete action but idx was null");
|
|
|
|
return true;
|
|
|
|
}
|
2024-07-22 06:07:05 +00:00
|
|
|
|
2024-07-22 06:22:54 +00:00
|
|
|
if (objMan->GetObjectByIndex((ushort) idx) == null) {
|
|
|
|
Plugin.Log.Warning("delete action but object at idx was null");
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-07-22 06:44:22 +00:00
|
|
|
manager._idx = null;
|
2024-07-22 06:22:54 +00:00
|
|
|
return true;
|
|
|
|
}
|
2024-07-22 05:44:50 +00:00
|
|
|
}
|
|
|
|
}
|