Add most of SynthHelper backend
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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