From eceb8a11823b74fff917a38e628214ea1ad216a2 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 15 Nov 2023 00:15:36 -0800 Subject: [PATCH] Add most of SynthHelper backend --- Craftimizer/Plugin.cs | 3 + Craftimizer/Utils/SynthesisValues.cs | 90 ++++++++++ Craftimizer/Windows/SynthHelper.cs | 260 +++++++++++++++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 Craftimizer/Utils/SynthesisValues.cs create mode 100644 Craftimizer/Windows/SynthHelper.cs diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index 414f2b6..e3db6c0 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -26,6 +26,7 @@ public sealed class Plugin : IDalamudPlugin public WindowSystem WindowSystem { get; } public Settings SettingsWindow { get; } public RecipeNote RecipeNoteWindow { get; } + public SynthHelper SynthHelperWindow { get; } public MacroList ListWindow { get; private set; } public MacroEditor? EditorWindow { get; private set; } public MacroClipboard? ClipboardWindow { get; private set; } @@ -51,6 +52,7 @@ public sealed class Plugin : IDalamudPlugin SettingsWindow = new(); RecipeNoteWindow = new(); + SynthHelperWindow = new(); ListWindow = new(); // Trigger static constructors so a huge hitch doesn't occur on first RecipeNote frame. @@ -157,6 +159,7 @@ public sealed class Plugin : IDalamudPlugin Service.CommandManager.RemoveHandler("/crafteditor"); SettingsWindow.Dispose(); RecipeNoteWindow.Dispose(); + SynthHelperWindow.Dispose(); ListWindow.Dispose(); EditorWindow?.Dispose(); ClipboardWindow?.Dispose(); diff --git a/Craftimizer/Utils/SynthesisValues.cs b/Craftimizer/Utils/SynthesisValues.cs new file mode 100644 index 0000000..dfb9d78 --- /dev/null +++ b/Craftimizer/Utils/SynthesisValues.cs @@ -0,0 +1,90 @@ +using Craftimizer.Simulator; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using System; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Craftimizer.Utils; + +internal sealed unsafe class SynthesisValues +{ + private AddonSynthesis* Addon { get; } + + public SynthesisValues(AddonSynthesis* addon) + { + Addon = addon; + } + + private ReadOnlySpan Values => new(Addon->AtkUnitBase.AtkValues, Addon->AtkUnitBase.AtkValuesCount); + + // Always 0? + private uint Unk0 => GetUInt(0); + private bool IsTrialSynthesis => TryGetBool(1) ?? false; + public SeString ItemName => GetString(2); + public uint ItemIconId => GetUInt(3); + public uint ItemCount => GetUInt(4); + public uint Progress => GetUInt(5); + public uint MaxProgress => GetUInt(6); + public uint Durability => GetUInt(7); + public uint MaxDurability => GetUInt(8); + public uint Quality => GetUInt(9); + public uint HQChance => GetUInt(10); + private uint IsShowingCollectibleInfoValue => GetUInt(11); + private uint ConditionValue => GetUInt(12); + public SeString ConditionName => GetString(13); + public SeString ConditionNameAndTooltip => GetString(14); + public uint StepCount => GetUInt(15); + public uint ResultItemId => GetUInt(16); + public uint MaxQuality => GetUInt(17); + public uint RequiredQuality => GetUInt(18); + private uint IsCollectibleValue => GetUInt(19); + public uint Collectability => GetUInt(20); + public uint MaxCollectability => GetUInt(21); + public uint CollectabilityCheckpoint1 => GetUInt(22); + public uint CollectabilityCheckpoint2 => GetUInt(23); + public uint CollectabilityCheckpoint3 => GetUInt(24); + public bool IsExpertRecipe => GetBool(25); + + public bool IsShowingCollectibleInfo => IsShowingCollectibleInfoValue != 0; + public Condition Condition => (Condition)(1 << (int)ConditionValue); + public bool IsCollectible => IsCollectibleValue != 0; + + private uint? TryGetUInt(int i) + { + var value = Values[i]; + return value.Type == ValueType.UInt ? + value.UInt : + null; + } + + private bool? TryGetBool(int i) + { + var value = Values[i]; + return value.Type == ValueType.Bool ? + value.Byte != 0 : + null; + } + + private SeString? TryGetString(int i) + { + var value = Values[i]; + return value.Type switch + { + ValueType.AllocatedString or + ValueType.String => + MemoryHelper.ReadSeStringNullTerminated((nint)value.String), + _ => null + }; + } + + private uint GetUInt(int i) => + TryGetUInt(i) ?? throw new ArgumentException($"Value {i} is not a uint", nameof(i)); + + private bool GetBool(int i) => + TryGetBool(i) ?? throw new ArgumentException($"Value {i} is not a boolean", nameof(i)); + + private SeString GetString(int i) => + TryGetString(i) ?? throw new ArgumentException($"Value {i} is not a string", nameof(i)); +} diff --git a/Craftimizer/Windows/SynthHelper.cs b/Craftimizer/Windows/SynthHelper.cs new file mode 100644 index 0000000..40c8680 --- /dev/null +++ b/Craftimizer/Windows/SynthHelper.cs @@ -0,0 +1,260 @@ +using Craftimizer.Plugin; +using Craftimizer.Plugin.Utils; +using Craftimizer.Simulator; +using Dalamud.Interface.Windowing; +using FFXIVClientStructs.FFXIV.Client.UI; +using ImGuiNET; +using System; +using System.Threading; +using Craftimizer.Utils; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game; +using ActionType = Craftimizer.Simulator.Actions.ActionType; +using Dalamud.Interface.Utility; +using System.Numerics; +using Dalamud.Game.ClientState.Conditions; + +namespace Craftimizer.Windows; + +public sealed unsafe class SynthHelper : Window, IDisposable +{ + private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoNavFocus; + + public AddonSynthesis* Addon { get; private set; } + public RecipeData? RecipeData { get; private set; } + public CharacterStats? CharacterStats { get; private set; } + public SimulationInput? SimulationInput { get; private set; } + + public bool IsCrafting { get; private set; } + private int CurrentActionCount { get; set; } + private ActionStates CurrentActionStates { get; set; } + private SimulationState CurrentState + { + get => currentState; + set + { + if (currentState != value) + { + currentState = value; + OnStateUpdated(); + } + } + } + private SimulationState currentState; + + private CancellationTokenSource? HelperTaskTokenSource { get; set; } + private Exception? HelperTaskException { get; set; } + + public SynthHelper() : base("Craftimizer SynthHelper", WindowFlags) + { + Service.Plugin.Hooks.OnActionUsed += OnUseAction; + + RespectCloseHotkey = false; + DisableWindowSounds = true; + ShowCloseButton = false; + IsOpen = true; + + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new(-1), + MaximumSize = new(10000, 10000) + }; + + Service.WindowSystem.AddWindow(this); + } + + private bool wasInCraftAction; + public override void Update() + { + Addon = (AddonSynthesis*)Service.GameGui.GetAddonByName("Synthesis"); + + if (Addon != null) + { + var agent = AgentRecipeNote.Instance(); + var recipeId = (ushort)agent->ActiveCraftRecipeId; + + if (agent->ActiveCraftRecipeId == 0) + IsCrafting = false; + else if (!IsCrafting) + { + IsCrafting = true; + OnStartCrafting(recipeId); + } + } + else + IsCrafting = false; + + var isInCraftAction = Service.Condition[ConditionFlag.Crafting40]; + if (!isInCraftAction && wasInCraftAction) + OnFinishedUsingAction(); + wasInCraftAction = isInCraftAction; + } + + private bool wasOpen; + public override bool DrawConditions() + { + var isOpen = ShouldDraw(); + if (isOpen != wasOpen) + { + if (wasOpen) + HelperTaskTokenSource?.Cancel(); + } + + wasOpen = isOpen; + return isOpen; + } + + private bool ShouldDraw() + { + if (Service.ClientState.LocalPlayer == null) + return false; + + if (Addon == null) + return false; + + if (!IsCrafting) + return false; + + // Check if Synthesis addon is visible + if (Addon->AtkUnitBase.WindowNode == null) + return false; + + return true; + } + + public override void PreDraw() + { + ref var unit = ref Addon->AtkUnitBase; + var scale = unit.Scale; + var pos = new Vector2(unit.X, unit.Y); + var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale; + + var node = unit.GetNodeById(46); + + Position = ImGuiHelpers.MainViewport.Pos + pos + new Vector2(size.X, node->Y * scale); + } + + public override void Draw() + { + ImGui.Text($"{IsCrafting} {CurrentState.Progress} {CurrentState.ActionCount} {CurrentState.Condition}"); + } + + private void OnStartCrafting(ushort recipeId) + { + var shouldUpdateInput = false; + if (recipeId != RecipeData?.RecipeId) + { + RecipeData = new(recipeId); + shouldUpdateInput = true; + } + + { + var gearStats = Gearsets.CalculateGearsetCurrentStats(); + + var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); + if (container == null) + throw new InvalidOperationException("Could not get inventory container"); + + var gearItems = Gearsets.GetGearsetItems(container); + + var characterStats = Gearsets.CalculateCharacterStats(gearStats, gearItems, RecipeData.ClassJob.GetPlayerLevel(), RecipeData.ClassJob.CanPlayerUseManipulation()); + if (characterStats != CharacterStats) + { + CharacterStats = characterStats; + shouldUpdateInput = true; + } + } + + if (shouldUpdateInput) + SimulationInput = new(CharacterStats, RecipeData.RecipeInfo); + + CurrentActionCount = 0; + CurrentActionStates = new(); + CurrentState = GetCurrentState(); + } + + private void OnUseAction(ActionType action) + { + if (!IsCrafting) + return; + + (_, CurrentState) = new SimulatorNoRandom().Execute(GetCurrentState(), action); + CurrentActionCount = CurrentState.ActionCount; + CurrentActionStates = CurrentState.ActionStates; + } + + private void OnFinishedUsingAction() + { + if (!IsCrafting) + return; + + CurrentState = GetCurrentState(); + } + + private SimulationState GetCurrentState() + { + var player = Service.ClientState.LocalPlayer!; + var values = new SynthesisValues(Addon); + var statusManager = ((Character*)player.Address)->GetStatusManager(); + + byte GetEffectStack(ushort id) + { + foreach (var status in statusManager->StatusSpan) + if (status.StatusID == id) + return status.StackCount; + return 0; + } + bool HasEffect(ushort id) + { + foreach (var status in statusManager->StatusSpan) + if (status.StatusID == id) + return true; + return false; + } + + return new(SimulationInput!) + { + ActionCount = CurrentActionCount, + StepCount = (int)values.StepCount - 1, + Progress = (int)values.Progress, + Quality = (int)values.Quality, + Durability = (int)values.Durability, + CP = (int)player.CurrentCp, + Condition = values.Condition, + ActiveEffects = new() + { + InnerQuiet = GetEffectStack((ushort)EffectType.InnerQuiet.StatusId()), + WasteNot = GetEffectStack((ushort)EffectType.WasteNot.StatusId()), + Veneration = GetEffectStack((ushort)EffectType.Veneration.StatusId()), + GreatStrides = GetEffectStack((ushort)EffectType.GreatStrides.StatusId()), + Innovation = GetEffectStack((ushort)EffectType.Innovation.StatusId()), + FinalAppraisal = GetEffectStack((ushort)EffectType.FinalAppraisal.StatusId()), + WasteNot2 = GetEffectStack((ushort)EffectType.WasteNot2.StatusId()), + MuscleMemory = GetEffectStack((ushort)EffectType.MuscleMemory.StatusId()), + Manipulation = GetEffectStack((ushort)EffectType.Manipulation.StatusId()), + HeartAndSoul = HasEffect((ushort)EffectType.HeartAndSoul.StatusId()), + }, + ActionStates = CurrentActionStates + }; + } + + private void OnStateUpdated() + { + if (!IsCrafting) + return; + + Log.Debug("state updated!"); + } + + public void Dispose() + { + Service.Plugin.Hooks.OnActionUsed -= OnUseAction; + + Service.WindowSystem.RemoveWindow(this); + } +}