From 809f5e04b09e8bd1cbbbea0b9885a923d4327572 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Fri, 21 Jul 2023 16:02:16 +0400 Subject: [PATCH] Implement synthesis helper I feel like it's still super janky so far.. hopefully more improvements soon --- Craftimizer/Windows/Craft.cs | 349 ++++++++++++++++++++++++++++++++++- 1 file changed, 343 insertions(+), 6 deletions(-) diff --git a/Craftimizer/Windows/Craft.cs b/Craftimizer/Windows/Craft.cs index 55b8b56..74e1f2f 100644 --- a/Craftimizer/Windows/Craft.cs +++ b/Craftimizer/Windows/Craft.cs @@ -1,14 +1,30 @@ using Craftimizer.Plugin.Utils; using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; using Craftimizer.Utils; +using Dalamud.Game; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using System.Numerics; +using System.Threading; +using System.Threading.Tasks; +using ActionType = Craftimizer.Simulator.Actions.ActionType; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; namespace Craftimizer.Plugin.Windows; -public unsafe class Craft : Window +public sealed unsafe class Craft : Window, IDisposable { private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize @@ -16,22 +32,91 @@ public unsafe class Craft : Window | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus; - private RecipeNote RecipeUtils { get; } = new(); + private static Configuration Config => Service.Configuration; + + private static Random Random { get; } = new(); + private static RecipeNote RecipeUtils => Service.Plugin.RecipeNote; private bool WasOpen { get; set; } private CharacterStats CharacterStats { get; set; } = null!; + private SimulationInput Input { get; set; } = null!; + private int ActionCount { get; set; } + private ActionStates ActionStates { get; set; } + + // Set to true if we used an action, but it's not reflected in the addon yet + private bool IsIntermediate { get; set; } + private SimulationState IntermediateState { get; set; } + + private SimulationState? SolverState { get; set; } + private Task? SolverTask { get; set; } + private CancellationTokenSource? SolverTaskToken { get; set; } + private ConcurrentQueue SolverActionQueue { get; } = new(); + + // State is the state of the simulation *after* its corresponding action is executed. + private List<(ActionType Action, string Tooltip, ActionResponse Response, SimulationState State)> SolverActions { get; } = new(); + private SimulatorNoRandom SolverSim { get; set; } = null!; + + private SimulationState SolverLatestState => SolverActions.Count == 0 ? SolverState!.Value : SolverActions[^1].State; public Craft() : base("Craftimizer SynthesisHelper", WindowFlags, true) { Service.WindowSystem.AddWindow(this); + Service.Plugin.Hooks.OnActionUsed += OnActionUsed; IsOpen = true; } public override void Draw() { - ImGui.Text($"{CharacterStats.CP};{CharacterStats.Control};{CharacterStats.Craftsmanship}"); + while (SolverActionQueue.TryDequeue(out var poppedAction)) + AppendGeneratedAction(poppedAction); + + DrawActions(); + + ImGui.Dummy(default); + ImGui.BeginDisabled(!(SolverTask?.IsCompleted ?? true) || IsIntermediate); + if (ImGui.Button("Retry")) + QueueSolve(CreateSimulationState()); + ImGui.EndDisabled(); + } + + private void DrawActions() + { + var totalWidth = 300f; + var actionsPerRow = 5; + + var actionSize = new Vector2((totalWidth / actionsPerRow) - (ImGui.GetStyle().ItemSpacing.X * ((actionsPerRow + 1f) / actionsPerRow))); + ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); + + ImGui.Dummy(new(0, actionSize.Y)); + ImGui.SameLine(0, 0); + for (var i = 0; i < SolverActions.Count; ++i) + { + var (action, tooltip, _, state) = SolverActions[i]; + ImGui.PushID(i); + if (ImGui.ImageButton(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0)) + { + if (i == 0) + Chat.SendMessage($"/ac \"{action.GetName(RecipeUtils.ClassJob)}\""); + } + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text($"{action.GetName(RecipeUtils.ClassJob)}\n{tooltip}"); + Simulator.DrawAllProgressTooltips(state); + if (i == 0) + ImGui.Text("Click to Execute"); + ImGui.EndTooltip(); + } + ImGui.PopID(); + if (i % actionsPerRow != (actionsPerRow - 1)) + ImGui.SameLine(); + } + + ImGui.PopStyleColor(3); } public override void PreDraw() @@ -42,7 +127,7 @@ public unsafe class Craft : Window 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(5); + var node = unit.GetNodeById(79); Position = pos + new Vector2(size.X, node->Y * scale); SizeConstraints = new WindowSizeConstraints @@ -51,14 +136,29 @@ public unsafe class Craft : Window MaximumSize = new(10000, 10000) }; + if (Input == null) + return; + + var addonState = CreateSimulationState(); + if (IsIntermediate) + { + if (StatesEqualExceptSome(addonState, IntermediateState)) + return; + + IsIntermediate = false; + } + + if (SolverState != addonState) + QueueSolve(addonState); + base.PreDraw(); } private bool DrawConditionsInner() { - if (!RecipeUtils.Update(out _)) + if (!RecipeUtils.HasValidRecipe) return false; - return false; + if (RecipeUtils.AddonSynthesis == null) return false; @@ -66,11 +166,17 @@ public unsafe class Craft : Window if (RecipeUtils.AddonSynthesis->AtkUnitBase.WindowNode == null) return false; + if (RecipeUtils.AddonSynthesis->AtkUnitBase.GetNodeById(79) == null) + return false; + return base.DrawConditions(); } public override bool DrawConditions() { + if (!Config.EnableSynthesisHelper) + return false; + var ret = DrawConditionsInner(); if (ret && !WasOpen) ResetSimulation(); @@ -79,6 +185,64 @@ public unsafe class Craft : Window return ret; } + private void StopSolve() + { + if (SolverTask == null || SolverTaskToken == null) + return; + + if (!SolverTask.IsCompleted) + SolverTaskToken.Cancel(); + else + { + SolverTaskToken.Dispose(); + SolverTask.Dispose(); + + SolverTask = null; + SolverTaskToken = null; + } + } + + private void QueueSolve(SimulationState state) + { + StopSolve(); + + SolverActionQueue.Clear(); + SolverActions.Clear(); + SolverState = state; + SolverSim = new(state); + + SolverTaskToken = new(); + SolverTask = Task.Run(() => Config.SolverAlgorithm.Invoke(Config.SolverConfig, state, SolverActionQueue.Enqueue, SolverTaskToken.Token)); + } + + private void AppendGeneratedAction(ActionType action) + { + var actionBase = action.Base(); + if (actionBase is BaseComboAction comboActionBase) + { + AppendGeneratedAction(comboActionBase.ActionTypeA); + AppendGeneratedAction(comboActionBase.ActionTypeB); + } + else + { + if (SolverActions.Count >= Config.SynthesisHelperStepCount) + { + StopSolve(); + return; + } + + var tooltip = actionBase.GetTooltip(SolverSim, false); + var (response, state) = SolverSim.Execute(SolverLatestState, action); + SolverActions.Add((action, tooltip, response, state)); + + if (SolverActions.Count >= Config.SynthesisHelperStepCount) + { + StopSolve(); + return; + } + } + } + private void ResetSimulation() { var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); @@ -86,5 +250,178 @@ public unsafe class Craft : Window return; CharacterStats = Gearsets.CalculateCharacterStats(Gearsets.CalculateGearsetCurrentStats(), Gearsets.GetGearsetItems(container), RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation); + Input = new(CharacterStats, RecipeUtils.Info, 0, Random); + ActionCount = 0; + ActionStates = new(); + IsIntermediate = false; + } + + private sealed class AddonValues + { + public AddonSynthesis* Addon { get; } + public AtkValue* Values => Addon->AtkUnitBase.AtkValues; + public ushort ValueCount => Addon->AtkUnitBase.AtkValuesCount; + + public AddonValues(AddonSynthesis* addon) + { + Addon = addon; + if (ValueCount != 26) + throw new ArgumentException("AddonSynthesis must have 26 AtkValues", nameof(addon)); + } + + public unsafe AtkValue* this[int i] => Values + i; + + // Always 0? + private uint Unk0 => GetUInt(0); + // Always true? + private bool Unk1 => GetBool(1); + + 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 GetUInt(int i) + { + var value = this[i]; + return value->Type == ValueType.UInt ? + value->UInt : + throw new ArgumentException($"Value {i} is not a uint", nameof(i)); + } + + private bool GetBool(int i) + { + var value = this[i]; + return value->Type == ValueType.Bool ? + value->Byte != 0 : + throw new ArgumentException($"Value {i} is not a boolean", nameof(i)); + } + + private SeString GetString(int i) + { + var value = this[i]; + return value->Type switch + { + ValueType.AllocatedString or + ValueType.String => + MemoryHelper.ReadSeStringNullTerminated((nint)value->String), + _ => throw new ArgumentException($"Value {i} is not a string", nameof(i)) + }; + } + } + + private const ushort StatusInnerQuiet = 251; + private const ushort StatusWasteNot = 252; + private const ushort StatusVeneration = 2226; + private const ushort StatusGreatStrides = 254; + private const ushort StatusInnovation = 2189; + private const ushort StatusFinalAppraisal = 2190; + private const ushort StatusWasteNot2 = 257; + private const ushort StatusMuscleMemory = 2191; + private const ushort StatusManipulation = 1164; + private const ushort StatusHeartAndSoul = 2665; + + private SimulationState CreateSimulationState() + { + var player = Service.ClientState.LocalPlayer!; + var values = new AddonValues(RecipeUtils.AddonSynthesis); + 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(Input) + { + ActionCount = ActionCount, + 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(StatusInnerQuiet), + WasteNot = GetEffectStack(StatusWasteNot), + Veneration = GetEffectStack(StatusVeneration), + GreatStrides = GetEffectStack(StatusGreatStrides), + Innovation = GetEffectStack(StatusInnovation), + FinalAppraisal = GetEffectStack(StatusFinalAppraisal), + WasteNot2 = GetEffectStack(StatusWasteNot2), + MuscleMemory = GetEffectStack(StatusMuscleMemory), + Manipulation = GetEffectStack(StatusManipulation), + HeartAndSoul = HasEffect(StatusHeartAndSoul), + }, + ActionStates = ActionStates + }; + } + + private void OnActionUsed(ActionType action) + { + if (RecipeUtils.AddonSynthesis == null) + return; + var inGameState = CreateSimulationState(); + (_, var predictedState) = new SimulatorNoRandom(inGameState).Execute(inGameState, action); + QueueSolve(predictedState); + + ActionCount++; + var states = ActionStates; + states.MutateState(action.Base()); + ActionStates = states; + IsIntermediate = true; + IntermediateState = CreateSimulationState(); + } + + private static bool StatesEqualExceptSome(SimulationState a, SimulationState b) + { + b.CP = a.CP; + b.ActiveEffects = a.ActiveEffects; + return a == b; + } + + public void Dispose() + { + StopSolve(); + SolverTask?.Wait(); + SolverTask?.Dispose(); + SolverTaskToken?.Dispose(); + + Service.Plugin.Hooks.OnActionUsed -= OnActionUsed; } }