diff --git a/client/Configuration.cs b/client/Configuration.cs index dd9bd9b..cba9978 100644 --- a/client/Configuration.cs +++ b/client/Configuration.cs @@ -18,6 +18,7 @@ public class Configuration : IPluginConfiguration { public bool LockViewer; public bool ClickThroughViewer; public bool HideTitlebar; + public bool ShowEmotes = true; public float ViewerOpacity = 100.0f; public int DefaultGlyph = 3; } diff --git a/client/Message.cs b/client/Message.cs index e13f7b6..7be684a 100644 --- a/client/Message.cs +++ b/client/Message.cs @@ -77,7 +77,7 @@ public class MessageWithTerritory { [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] public class EmoteData { public required uint Id { get; set; } - public required byte[] Customise { get; set; } + public required List Customise { get; set; } public required EquipmentData[] Equipment { get; set; } public required WeaponData[] Weapon { get; set; } public required uint Glasses { get; set; } @@ -91,7 +91,9 @@ public class EmoteData { public class EquipmentData { public required ushort Id { get; set; } public required byte Variant { get; set; } + [JsonProperty("stain_0")] public required byte Stain0 { get; set; } + [JsonProperty("stain_1")] public required byte Stain1 { get; set; } public required ulong Value { get; set; } } @@ -101,7 +103,9 @@ public class EquipmentData { public class WeaponData { public required WeaponModelId ModelId { get; set; } public required byte State { get; set; } + [JsonProperty("flags_1")] public required ushort Flags1 { get; set; } + [JsonProperty("flags_2")] public required byte Flags2 { get; set; } } @@ -111,7 +115,9 @@ public class WeaponModelId { public required ushort Id { get; set; } public required ushort Kind { get; set; } public required ushort Variant { get; set; } + [JsonProperty("stain_0")] public required byte Stain0 { get; set; } + [JsonProperty("stain_1")] public required byte Stain1 { get; set; } public required ulong Value { get; set; } } diff --git a/client/Ui/MainWindowTabs/Settings.cs b/client/Ui/MainWindowTabs/Settings.cs index 9e7195d..3919d51 100644 --- a/client/Ui/MainWindowTabs/Settings.cs +++ b/client/Ui/MainWindowTabs/Settings.cs @@ -121,6 +121,7 @@ internal class Settings : ITab { anyChanged |= vfx |= ImGui.Checkbox("Disable in cutscenes", ref this.Plugin.Config.DisableInCutscene); anyChanged |= vfx |= ImGui.Checkbox("Disable in /gpose", ref this.Plugin.Config.DisableInGpose); anyChanged |= vfx |= ImGui.Checkbox("Remove glow effect from signs", ref this.Plugin.Config.RemoveGlow); + anyChanged |= ImGui.Checkbox("Show player emotes", ref this.Plugin.Config.ShowEmotes); var tt = this.Plugin.DataManager.GetExcelSheet(); if (tt == null) { diff --git a/client/Ui/MainWindowTabs/Write.cs b/client/Ui/MainWindowTabs/Write.cs index 325c6d4..8ad7dfe 100644 --- a/client/Ui/MainWindowTabs/Write.cs +++ b/client/Ui/MainWindowTabs/Write.cs @@ -3,6 +3,7 @@ using System.Text; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Interface.Textures; +using Dalamud.Interface.Utility; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -28,6 +29,7 @@ internal class Write : ITab { private (int, int) _word2 = (-1, -1); private int _glyph; private int _emoteIdx = -1; + private string _emoteSearch = string.Empty; private const string Placeholder = "****"; private Pack? Pack => Pack.All.Get(this._pack); @@ -69,6 +71,7 @@ internal class Write : ITab { this.Emotes = this.Plugin.DataManager.GetExcelSheet()! .Skip(1) + .Where(emote => emote.TextCommand.Row != 0) .ToList(); this._glyph = this.Plugin.Config.DefaultGlyph; @@ -312,30 +315,50 @@ internal class Write : ITab { var emoteLabel = this._emoteIdx == -1 ? "None" : this.Emotes[this._emoteIdx].Name.ToDalamudString().TextValue; - if (ImGui.BeginCombo("Emote", emoteLabel)) { + if (ImGui.BeginCombo("Emote", emoteLabel, ImGuiComboFlags.HeightLarge)) { using var endCombo = new OnDispose(ImGui.EndCombo); - if (ImGui.Selectable("None##no-emote", this._emoteIdx == -1)) { - this._emoteIdx = -1; + if (ImGui.IsWindowAppearing()) { + ImGui.SetKeyboardFocusHere(); } - ImGui.Separator(); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("###emote-search", "Search...", ref this._emoteSearch, 100, ImGuiInputTextFlags.AutoSelectAll); - for (var i = 0; i < this.Emotes.Count; i++) { - var emote = this.Emotes[i]; - var name = emote.Name.ToDalamudString().TextValue; - - var unlocked = IsEmoteUnlocked(emote); - if (!unlocked) { - ImGui.BeginDisabled(); + using var endChild = new OnDispose(ImGui.EndChild); + if (ImGui.BeginChild("##emote-search-child", new Vector2(0, 150) * ImGuiHelpers.GlobalScale)) { + if (ImGui.Selectable("None##no-emote", this._emoteIdx == -1)) { + this._emoteIdx = -1; + ImGui.CloseCurrentPopup(); } - if (ImGui.Selectable($"{name}##emote-{emote.RowId}", this._emoteIdx == i)) { - this._emoteIdx = i; - } + ImGui.Separator(); - if (!unlocked) { - ImGui.EndDisabled(); + for (var i = 0; i < this.Emotes.Count; i++) { + var emote = this.Emotes[i]; + if (!string.IsNullOrEmpty(this._emoteSearch)) { + if (!emote.Name.ToDalamudString().TextValue.Contains(this._emoteSearch, StringComparison.InvariantCultureIgnoreCase)) { + if (!emote.TextCommand.Value!.Command.ToDalamudString().TextValue.Contains(this._emoteSearch, StringComparison.InvariantCultureIgnoreCase)) { + continue; + } + } + } + + var name = emote.Name.ToDalamudString().TextValue; + + var unlocked = IsEmoteUnlocked(emote); + if (!unlocked) { + ImGui.BeginDisabled(); + } + + if (ImGui.Selectable($"{name}##emote-{emote.RowId}", this._emoteIdx == i)) { + this._emoteIdx = i; + ImGui.CloseCurrentPopup(); + } + + if (!unlocked) { + ImGui.EndDisabled(); + } } } } @@ -416,12 +439,11 @@ internal class Write : ITab { } private unsafe EmoteData GetEmoteData(Emote emote, IPlayerCharacter player) { - var objMan = ClientObjectManager.Instance(); - var chara = (BattleChara*) objMan->GetObjectByIndex(player.ObjectIndex); + var chara = (Character*) GameObjectManager.Instance()->Objects.GetObjectByGameObjectId(player.GameObjectId); return new EmoteData { Id = emote.RowId, - Customise = player.Customize, + Customise = player.Customize.ToList(), Equipment = chara->DrawData.EquipmentModelIds .ToArray() .Select(equip => new EquipmentData { @@ -464,6 +486,8 @@ internal class Write : ITab { this._word1 = (-1, -1); this._word2 = (-1, -1); this._glyph = this.Plugin.Config.DefaultGlyph; + this._emoteIdx = -1; + this._emoteSearch = string.Empty; } private void ClearIfNecessary() { diff --git a/client/Util/ActorManager.cs b/client/Util/ActorManager.cs index 93d29b8..bf25eaa 100644 --- a/client/Util/ActorManager.cs +++ b/client/Util/ActorManager.cs @@ -19,6 +19,14 @@ internal class ActorManager : IDisposable { public void Dispose() { this.Plugin.Ui.Viewer.View -= this.OnView; this.Plugin.Framework.Update -= this.OnFramework; + + if (this._idx != null) { + unsafe { + var objMan = ClientObjectManager.Instance(); + new DisableAction().Run(this, objMan); + new DeleteAction().Run(this, objMan); + } + } } private unsafe void OnFramework(IFramework framework) { @@ -52,7 +60,7 @@ internal class ActorManager : IDisposable { Plugin.Log.Debug($"OnView message is {msg}"); this.Despawn(); - if (message?.Emote != null) { + if (this.Plugin.Config.ShowEmotes && message?.Emote != null) { this.Spawn(message); } } @@ -122,6 +130,7 @@ internal class ActorManager : IDisposable { manager._idx = idx; var emote = message.Emote; + var emoteRow = manager.GetValidEmote(emote.Id); var chara = (BattleChara*) objMan->GetObjectByIndex((ushort) idx); @@ -131,7 +140,7 @@ internal class ActorManager : IDisposable { chara->Rotation = message.Yaw; var drawData = &chara->DrawData; - var maxLen = Math.Min(sizeof(CustomizeData), emote.Customise.Length); + 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]; @@ -144,26 +153,23 @@ internal class ActorManager : IDisposable { Variant = equip.Variant, Stain0 = equip.Stain0, Stain1 = equip.Stain1, - Value = equip.Value, }; } - for (var i = 0; i < Math.Min(drawData->WeaponData.Length, emote.Weapon.Length); i++) { - var weapon = emote.Weapon[i]; - drawData->Weapon((DrawDataContainer.WeaponSlot) i) = new DrawObjectData { - ModelId = new FFXIVClientStructs.FFXIV.Client.Game.Character.WeaponModelId { + 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, - Value = weapon.ModelId.Value, - }, - DrawObject = chara->DrawObject, - Flags1 = weapon.Flags1, - Flags2 = weapon.Flags2, - State = weapon.State, - }; + }; + 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; @@ -174,8 +180,8 @@ internal class ActorManager : IDisposable { chara->Alpha = 0.25f; chara->SetMode(CharacterModes.AnimLock, 0); - if (manager.Plugin.DataManager.GetExcelSheet()?.GetRow(emote.Id) is { } row) { - chara->Timeline.BaseOverride = (ushort) row.ActionTimeline[0].Row; + if (emoteRow != null) { + chara->Timeline.BaseOverride = (ushort) emoteRow.ActionTimeline[0].Row; } manager._tasks.Enqueue(new EnableAction()); @@ -183,6 +189,15 @@ internal class ActorManager : IDisposable { } } + 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) { if (!this.TryGetBattleChara(manager, objMan, out var chara)) {