Files
Craftimizer/Craftimizer/SimulatorUtils.cs
T
JonKazama-Hellion b598c03e9e Apply csharpier reflow across source tree
Reformats the entire Craftimizer source tree with dotnet csharpier 1.2.6
to match the Hellion Forge house style (matches what HellionChat enforces
in its pre-push pipeline). Pure whitespace + using-block sorting; no
semantic changes.

This is a one-time noisy commit. Future code edits in this fork should
land csharpier-clean because the pre-push hook (introduced in the next
commit) runs `dotnet csharpier check Craftimizer/` as Block C of the
preflight gate.

Trade-off acknowledged: this widens the merge gap with upstream
Craftimizer should Asriel ever resume maintenance. Given the upstream
has been dormant since FFXIV 7.4 and the fork is light-rename only
(internal namespaces unchanged), the marginal cost is acceptable.
2026-05-26 20:21:21 +02:00

419 lines
15 KiB
C#

using System;
using System.Linq;
using System.Numerics;
using System.Text;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Utils;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Lumina.Excel.Sheets;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
using Action = Lumina.Excel.Sheets.Action;
using ActionType = Craftimizer.Simulator.Actions.ActionType;
using ClassJob = Craftimizer.Simulator.ClassJob;
using Condition = Craftimizer.Simulator.Condition;
using Status = Lumina.Excel.Sheets.Status;
namespace Craftimizer.Plugin;
internal static class ActionUtils
{
private static readonly (CraftAction? CraftAction, Action? Action)[,] ActionRows;
static ActionUtils()
{
var actionTypes = Enum.GetValues<ActionType>();
var classJobs = Enum.GetValues<ClassJob>();
ActionRows = new (CraftAction? CraftAction, Action? Action)[
actionTypes.Length,
classJobs.Length
];
foreach (var actionType in actionTypes)
{
var actionId = actionType.Base().ActionId;
if (LuminaSheets.CraftActionSheet.GetRowOrDefault(actionId) is { } baseCraftAction)
{
foreach (var classJob in classJobs)
{
ActionRows[(int)actionType, (int)classJob] = (
classJob switch
{
ClassJob.Carpenter => baseCraftAction.CRP.Value,
ClassJob.Blacksmith => baseCraftAction.BSM.Value,
ClassJob.Armorer => baseCraftAction.ARM.Value,
ClassJob.Goldsmith => baseCraftAction.GSM.Value,
ClassJob.Leatherworker => baseCraftAction.LTW.Value,
ClassJob.Weaver => baseCraftAction.WVR.Value,
ClassJob.Alchemist => baseCraftAction.ALC.Value,
ClassJob.Culinarian => baseCraftAction.CUL.Value,
_ => baseCraftAction,
},
null
);
}
}
if (LuminaSheets.ActionSheet.GetRowOrDefault(actionId) is { } baseAction)
{
var possibleActions = LuminaSheets.ActionSheet.Where(r =>
r.Icon == baseAction.Icon
&& r.ActionCategory.RowId == baseAction.ActionCategory.RowId
&& r.Name.Equals(baseAction.Name)
);
foreach (var classJob in classJobs)
ActionRows[(int)actionType, (int)classJob] = (
null,
possibleActions.First(r =>
r.ClassJobCategory.ValueNullable?.IsClassJob(classJob) ?? false
)
);
}
}
}
public static void Initialize() { }
public static (CraftAction? CraftAction, Action? Action) GetActionRow(
this ActionType me,
ClassJob classJob
) => ActionRows[(int)me, (int)classJob];
public static uint GetId(this ActionType me, ClassJob classJob)
{
var (craftAction, action) = GetActionRow(me, classJob);
return craftAction?.RowId ?? action?.RowId ?? 0;
}
public static string GetName(this ActionType me, ClassJob classJob)
{
var (craftAction, action) = GetActionRow(me, classJob);
return (craftAction?.Name ?? action?.Name)?.AsSpan().ToString() ?? "Unknown";
}
public static ITextureIcon GetIcon(this ActionType me, ClassJob classJob)
{
var (craftAction, action) = GetActionRow(me, classJob);
// 1953 = Old "Steady Hand" action icon
return Service.IconManager.GetIconCached(craftAction?.Icon ?? action?.Icon ?? 1953);
}
public static ActionType? GetActionTypeFromId(
uint actionId,
ClassJob classJob,
bool isCraftAction
)
{
foreach (var action in Enum.GetValues<ActionType>())
{
var row = action.GetActionRow(classJob);
if (isCraftAction)
{
if (row.CraftAction?.RowId == actionId)
return action;
}
else
{
if (row.Action?.RowId == actionId)
return action;
}
}
return null;
}
}
internal static class ClassJobUtils
{
public static byte GetClassJobIndex(this ClassJob me) =>
me switch
{
ClassJob.Carpenter => 8,
ClassJob.Blacksmith => 9,
ClassJob.Armorer => 10,
ClassJob.Goldsmith => 11,
ClassJob.Leatherworker => 12,
ClassJob.Weaver => 13,
ClassJob.Alchemist => 14,
ClassJob.Culinarian => 15,
_ => 0,
};
public static ClassJob? GetClassJobFromIdx(byte classJobIdx) =>
classJobIdx switch
{
8 => ClassJob.Carpenter,
9 => ClassJob.Blacksmith,
10 => ClassJob.Armorer,
11 => ClassJob.Goldsmith,
12 => ClassJob.Leatherworker,
13 => ClassJob.Weaver,
14 => ClassJob.Alchemist,
15 => ClassJob.Culinarian,
_ => null,
};
public static sbyte GetExpArrayIdx(this ClassJob me) =>
LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!.ExpArrayIndex;
public static unsafe short GetPlayerLevel(this ClassJob me) =>
PlayerState.Instance()->ClassJobLevels[me.GetExpArrayIdx()];
public static unsafe ushort GetWKSSyncedLevel(this ClassJob me)
{
var jobLevel = (ushort)me.GetPlayerLevel();
var handler = EventFramework.Instance()->GetCraftEventHandler();
if (handler != null)
{
for (var i = 0; i < 2; ++i)
{
if (handler->WKSClassJobs[i] == me.GetClassJobIndex())
return Math.Max(jobLevel, handler->WKSClassLevels[i]);
}
}
return jobLevel;
}
public static unsafe bool CanPlayerUseManipulation(this ClassJob me) =>
UIState
.Instance()
->IsUnlockLinkUnlockedOrQuestCompleted(
ActionType.Manipulation.GetActionRow(me).Action!.Value.UnlockLink.RowId
);
public static string GetName(this ClassJob me)
{
var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex());
return job.Name.ToString();
}
public static string GetNameArticle(this ClassJob me)
{
var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex());
if (LuminaSheets.ClassJobSheet.Language == Lumina.Data.Language.English)
{
if (me is ClassJob.Alchemist or ClassJob.Armorer)
return "an";
}
return "a";
}
public static string GetAbbreviation(this ClassJob me)
{
var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex());
return job.Abbreviation.ToString();
}
public static Quest GetUnlockQuest(this ClassJob me) =>
LuminaSheets.QuestSheet.GetRow(65720 + (uint)me);
public static ushort GetIconId(this ClassJob me) => (ushort)(62000 + me.GetClassJobIndex());
public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) =>
classJob switch
{
ClassJob.Carpenter => me.CRP,
ClassJob.Blacksmith => me.BSM,
ClassJob.Armorer => me.ARM,
ClassJob.Goldsmith => me.GSM,
ClassJob.Leatherworker => me.LTW,
ClassJob.Weaver => me.WVR,
ClassJob.Alchemist => me.ALC,
ClassJob.Culinarian => me.CUL,
_ => false,
};
}
internal static class ConditionUtils
{
private static (uint Name, uint Description) AddonIds(this Condition me) =>
me switch
{
Condition.Poor => (229, 14203),
Condition.Normal => (226, 14200),
Condition.Good => (227, 14201),
Condition.Excellent => (228, 14202),
Condition.Centered => (239, 14204),
Condition.Sturdy => (240, 14205),
Condition.Pliant => (241, 14206),
Condition.Malleable => (13455, 14208),
Condition.Primed => (13454, 14207),
Condition.GoodOmen => (14214, 14215),
_ => (226, 14200), // Unknown
};
private static Vector3 AddRGB(this Condition me) =>
me switch
{
Condition.Poor => new(-50, -50, -50),
Condition.Normal => new(32, 48, 64),
Condition.Good => new(80, -80, 0),
Condition.Excellent => Vector3.Zero, // All the other conditions are just a single lerp, this one is different
Condition.Centered => new(200, 200, 0),
Condition.Sturdy => new(-100, 45, 155),
Condition.Pliant => new(0, 250, 0),
Condition.Malleable => new(-80, -40, 180),
Condition.Primed => new(30, -155, 200),
Condition.GoodOmen => new(100, 20, 0),
_ => Vector3.Zero, // Unknown
};
private const float ConditionCyclePeriod = 19 / 30f;
// The real period of all condition color cycles are 0.633... (19/30) seconds
// Interp accepts 0-1
public static Vector4 GetColor(this Condition me, float interp)
{
//var baseColor = new Vector3(0.85f, 0.85f, 0.85f); // Middle-ish pixels of synthesis2_hr1.tex's condition circle
Vector3 addRgb;
// Excellent has 6 lerps and 1 ending constant
if (me == Condition.Excellent)
{
addRgb = interp switch
{
< 0.155f => Vector3.Lerp(new(128, 0, 0), new(128, 80, 0), (interp - 0) / 0.155f),
< 0.315f => Vector3.Lerp(
new(128, 80, 0),
new(128, 128, 0),
(interp - 0.155f) / 0.16f
),
< 0.475f => Vector3.Lerp(
new(128, 128, 0),
new(0, 64, 0),
(interp - 0.315f) / 0.16f
),
< 0.630f => Vector3.Lerp(
new(0, 64, 0),
new(0, 128, 128),
(interp - 0.475f) / 0.155f
),
< 0.790f => Vector3.Lerp(
new(0, 128, 128),
new(0, 0, 128),
(interp - 0.630f) / 0.16f
),
< 0.945f => Vector3.Lerp(
new(0, 0, 128),
new(64, 0, 64),
(interp - 0.790f) / 0.155f
),
_ => new(64, 0, 64),
};
}
// Period is twice as fast so we oscillate at twice that speed
else if (me == Condition.Malleable)
{
if (interp > .5f)
interp -= .5f;
if (interp > .25f)
interp = .25f - (interp - .25f);
interp *= 4;
addRgb = Vector3.Lerp(new(-80, -40, 180), new(-41, -1, 254), interp);
}
else
{
if (interp > .5f)
interp = .5f - (interp - .5f);
interp *= 2;
addRgb = me switch
{
Condition.Poor => Vector3.Lerp(new(-50, -50, -50), new(-1, -1, -1), interp),
Condition.Normal => Vector3.Lerp(new(32, 48, 64), new(63, 95, 127), interp),
Condition.Good => Vector3.Lerp(new(80, -80, 0), new(159, -1, 0), interp),
Condition.Centered => Vector3.Lerp(new(199, 199, 0), new(100, 100, 0), interp),
Condition.Sturdy => Vector3.Lerp(new(-100, 45, 155), new(-51, 89, 254), interp),
Condition.Pliant => Vector3.Lerp(new(0, 150, 0), new(0, 249, 0), interp),
Condition.Primed => Vector3.Lerp(new(-30, -255, 50), new(29, -156, 199), interp),
Condition.GoodOmen => Vector3.Lerp(new(100, 20, 0), new(100, 99, 99), interp),
_ => default,
};
}
return new(addRgb / 255, 1);
}
public static Vector4 GetColor(this Condition me, TimeSpan time)
{
return me.GetColor(
(float)(time.TotalSeconds % ConditionCyclePeriod / ConditionCyclePeriod)
);
}
public static string Name(this Condition me) =>
LuminaSheets.AddonSheet.GetRow(me.AddonIds().Name).Text.ToString();
public static string Description(this Condition me, bool isRelic)
{
var text = LuminaSheets.AddonSheet.GetRow(me.AddonIds().Description).Text;
if (!text.Any(p => p is { Type: ReadOnlySePayloadType.Macro, MacroCode: MacroCode.Float }))
return text.ToString();
ReadOnlySeString finalText = new();
foreach (var payload in text)
{
if (payload is { Type: ReadOnlySePayloadType.Macro, MacroCode: MacroCode.Float })
finalText += new ReadOnlySePayload(
ReadOnlySePayloadType.Text,
default,
Encoding.UTF8.GetBytes(isRelic ? "1.75" : "1.5")
);
else
finalText += payload;
}
return finalText.ToString();
}
}
internal static class EffectUtils
{
public static uint StatusId(this EffectType me) =>
me switch
{
EffectType.InnerQuiet => 251,
EffectType.WasteNot => 252,
EffectType.Veneration => 2226,
EffectType.GreatStrides => 254,
EffectType.Innovation => 2189,
EffectType.FinalAppraisal => 2190,
EffectType.WasteNot2 => 257,
EffectType.MuscleMemory => 2191,
EffectType.Manipulation => 1164,
EffectType.HeartAndSoul => 2665,
EffectType.Expedience => 3812,
EffectType.TrainedPerfection => 3813,
_ => throw new ArgumentOutOfRangeException(nameof(me)),
};
public static bool IsIndefinite(this EffectType me) =>
me is EffectType.InnerQuiet or EffectType.HeartAndSoul or EffectType.TrainedPerfection;
public static Status Status(this EffectType me) =>
LuminaSheets.StatusSheet.GetRow(me.StatusId())!;
public static uint GetIconId(this EffectType me, int strength)
{
var status = me.Status();
var iconId = status.Icon;
if (status.MaxStacks != 0)
iconId += (uint)Math.Clamp(strength, 1, status.MaxStacks) - 1;
return iconId;
}
public static ITextureIcon GetIcon(this EffectType me, int strength) =>
Service.IconManager.GetIconCached(me.GetIconId(strength));
public static string GetTooltip(this EffectType me, int strength, int duration)
{
var status = me.Status();
var name = new StringBuilder();
name.Append(status.Name.ToString());
if (status.MaxStacks != 0)
name.Append($" {strength}");
if (!status.IsPermanent)
name.Append($" > {duration}");
return name.ToString();
}
}