Add most of SynthHelper backend

This commit is contained in:
Asriel Camora
2023-11-15 00:15:36 -08:00
parent ca92a78197
commit eceb8a1182
3 changed files with 353 additions and 0 deletions
+3
View File
@@ -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();
+90
View File
@@ -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));
}
+260
View File
@@ -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);
}
}