Add most of SynthHelper backend
This commit is contained in:
@@ -26,6 +26,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
public WindowSystem WindowSystem { get; }
|
public WindowSystem WindowSystem { get; }
|
||||||
public Settings SettingsWindow { get; }
|
public Settings SettingsWindow { get; }
|
||||||
public RecipeNote RecipeNoteWindow { get; }
|
public RecipeNote RecipeNoteWindow { get; }
|
||||||
|
public SynthHelper SynthHelperWindow { get; }
|
||||||
public MacroList ListWindow { get; private set; }
|
public MacroList ListWindow { get; private set; }
|
||||||
public MacroEditor? EditorWindow { get; private set; }
|
public MacroEditor? EditorWindow { get; private set; }
|
||||||
public MacroClipboard? ClipboardWindow { get; private set; }
|
public MacroClipboard? ClipboardWindow { get; private set; }
|
||||||
@@ -51,6 +52,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
SettingsWindow = new();
|
SettingsWindow = new();
|
||||||
RecipeNoteWindow = new();
|
RecipeNoteWindow = new();
|
||||||
|
SynthHelperWindow = new();
|
||||||
ListWindow = new();
|
ListWindow = new();
|
||||||
|
|
||||||
// Trigger static constructors so a huge hitch doesn't occur on first RecipeNote frame.
|
// 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");
|
Service.CommandManager.RemoveHandler("/crafteditor");
|
||||||
SettingsWindow.Dispose();
|
SettingsWindow.Dispose();
|
||||||
RecipeNoteWindow.Dispose();
|
RecipeNoteWindow.Dispose();
|
||||||
|
SynthHelperWindow.Dispose();
|
||||||
ListWindow.Dispose();
|
ListWindow.Dispose();
|
||||||
EditorWindow?.Dispose();
|
EditorWindow?.Dispose();
|
||||||
ClipboardWindow?.Dispose();
|
ClipboardWindow?.Dispose();
|
||||||
|
|||||||
@@ -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<AtkValue> 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));
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user