UI changes and stuff

This commit is contained in:
Asriel Camora
2023-06-23 03:02:33 -07:00
parent 37156c7f33
commit a5d1b18840
6 changed files with 445 additions and 172 deletions
+169
View File
@@ -0,0 +1,169 @@
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace Craftimizer.Plugin.Utils;
// https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17
public static class Chat
{
private static class Signatures
{
internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9";
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D";
}
private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
private static ProcessChatBoxDelegate? ProcessChatBox { get; }
private static readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString = null!;
static Chat()
{
if (Service.SigScanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr))
{
ProcessChatBox = Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(processChatBoxPtr);
}
unsafe
{
if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr))
{
_sanitiseString = (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sanitisePtr;
}
}
}
/// <summary>
/// <para>
/// Send a given message to the chat box. <b>This can send chat to the server.</b>
/// </para>
/// <para>
/// <b>This method is unsafe.</b> This method does no checking on your input and
/// may send content to the server that the normal client could not. You must
/// verify what you're sending and handle content and length to properly use
/// this.
/// </para>
/// </summary>
/// <param name="message">Message to send</param>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public static unsafe void SendMessageUnsafe(byte[] message)
{
if (ProcessChatBox == null)
{
throw new InvalidOperationException("Could not find signature for chat sending");
}
var uiModule = (IntPtr)Framework.Instance()->GetUiModule();
using var payload = new ChatPayload(message);
var mem1 = Marshal.AllocHGlobal(400);
Marshal.StructureToPtr(payload, mem1, false);
ProcessChatBox(uiModule, mem1, IntPtr.Zero, 0);
Marshal.FreeHGlobal(mem1);
}
/// <summary>
/// <para>
/// Send a given message to the chat box. <b>This can send chat to the server.</b>
/// </para>
/// <para>
/// This method is slightly less unsafe than <see cref="SendMessageUnsafe"/>. It
/// will throw exceptions for certain inputs that the client can't normally send,
/// but it is still possible to make mistakes. Use with caution.
/// </para>
/// </summary>
/// <param name="message">message to send</param>
/// <exception cref="ArgumentException">If <paramref name="message"/> is empty, longer than 500 bytes in UTF-8, or contains invalid characters.</exception>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public static void SendMessage(string message)
{
var bytes = Encoding.UTF8.GetBytes(message);
if (bytes.Length == 0)
{
throw new ArgumentException("message is empty", nameof(message));
}
if (bytes.Length > 500)
{
throw new ArgumentException("message is longer than 500 bytes", nameof(message));
}
if (message.Length != SanitiseText(message).Length)
{
throw new ArgumentException("message contained invalid characters", nameof(message));
}
SendMessageUnsafe(bytes);
}
/// <summary>
/// <para>
/// Sanitises a string by removing any invalid input.
/// </para>
/// <para>
/// The result of this method is safe to use with
/// <see cref="SendMessage"/>, provided that it is not empty or too
/// long.
/// </para>
/// </summary>
/// <param name="text">text to sanitise</param>
/// <returns>sanitised text</returns>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
public static unsafe string SanitiseText(string text)
{
if (_sanitiseString == null)
{
throw new InvalidOperationException("Could not find signature for chat sanitisation");
}
var uText = Utf8String.FromString(text);
_sanitiseString(uText, 0x27F, IntPtr.Zero);
var sanitised = uText->ToString();
uText->Dtor();
IMemorySpace.Free(uText);
return sanitised;
}
[StructLayout(LayoutKind.Explicit)]
private readonly struct ChatPayload : IDisposable
{
[FieldOffset(0)]
private readonly IntPtr textPtr;
[FieldOffset(16)]
private readonly ulong textLen;
[FieldOffset(8)]
private readonly ulong unk1;
[FieldOffset(24)]
private readonly ulong unk2;
internal ChatPayload(byte[] stringBytes)
{
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
this.textLen = (ulong)(stringBytes.Length + 1);
this.unk1 = 64;
this.unk2 = 0;
}
public void Dispose()
{
Marshal.FreeHGlobal(this.textPtr);
}
}
}
+191
View File
@@ -0,0 +1,191 @@
using Craftimizer.Simulator;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Linq;
namespace Craftimizer.Plugin.Utils;
internal static unsafe class Gearsets
{
private static readonly (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) BaseStats = (180, 0, 0, false, false);
private const int ParamCP = 11;
private const int ParamCraftsmanship = 70;
private const int ParamControl = 71;
public static CharacterStats CalculateCharacterStats(InventoryContainer* container, int characterLevel, bool canUseManipulation)
{
var stats = CalculateGearsetStats(container);
return new CharacterStats
{
CP = stats.CP,
Craftsmanship = stats.Craftsmanship,
Control = stats.Control,
Level = characterLevel,
CanUseManipulation = canUseManipulation,
HasSplendorousBuff = stats.HasSplendorous,
IsSpecialist = stats.HasSpecialist,
CLvl = CalculateCLvl(characterLevel),
};
}
public static CharacterStats CalculateCharacterStats(RaptureGearsetModule.GearsetEntry* entry, int characterLevel, bool canUseManipulation)
{
var stats = CalculateGearsetStats(entry);
return new CharacterStats
{
CP = stats.CP,
Craftsmanship = stats.Craftsmanship,
Control = stats.Control,
Level = characterLevel,
CanUseManipulation = canUseManipulation,
HasSplendorousBuff = stats.HasSplendorous,
IsSpecialist = stats.HasSpecialist,
CLvl = CalculateCLvl(characterLevel),
};
}
private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetStats(InventoryContainer* container)
{
var stats = BaseStats;
for (var i = 0; i < container->Size; ++i)
{
var itemStats = CalculateGearsetItemStats(container->Items[i]);
stats.CP += itemStats.CP;
stats.Craftsmanship += itemStats.Craftsmanship;
stats.Control += itemStats.Control;
stats.HasSplendorous = stats.HasSplendorous || itemStats.HasSplendorous;
stats.HasSpecialist = stats.HasSpecialist || itemStats.HasSpecialist;
}
return stats;
}
private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetStats(RaptureGearsetModule.GearsetEntry* entry)
{
var stats = new[]
{
BaseStats,
CalculateGearsetItemStats(entry->MainHand),
CalculateGearsetItemStats(entry->OffHand),
CalculateGearsetItemStats(entry->Head),
CalculateGearsetItemStats(entry->Body),
CalculateGearsetItemStats(entry->Hands),
// CalculateGearsetItemStats(entry->Belt),
CalculateGearsetItemStats(entry->Legs),
CalculateGearsetItemStats(entry->Feet),
CalculateGearsetItemStats(entry->Ears),
CalculateGearsetItemStats(entry->Neck),
CalculateGearsetItemStats(entry->Wrists),
CalculateGearsetItemStats(entry->RingRight),
CalculateGearsetItemStats(entry->RightLeft),
CalculateGearsetItemStats(entry->SoulStone),
};
return stats.Aggregate((a, b) => (a.CP + b.CP, a.Craftsmanship + b.Craftsmanship, a.Control + b.Control, a.HasSplendorous || b.HasSplendorous, a.HasSpecialist || b.HasSpecialist));
}
private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetItemStats(InventoryItem item) =>
CalculateGearsetItemStats(item.ItemID, item.Flags.HasFlag(InventoryItem.ItemFlags.HQ), item.Materia, item.MateriaGrade);
private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetItemStats(RaptureGearsetModule.GearsetItem item) =>
CalculateGearsetItemStats(item.ItemID % 1000000, item.ItemID > 1000000, item.Materia, item.MateriaGrade);
private static (int CP, int Craftsmanship, int Control, bool HasSplendorous, bool HasSpecialist) CalculateGearsetItemStats(uint itemId, bool isHq, ushort* materiaTypes, byte* materiaGrades)
{
var item = LuminaSheets.ItemSheet.GetRow(itemId)!;
int cp = 0, craftsmanship = 0, control = 0;
void IncreaseStat(int baseParam, int amount)
{
if (baseParam == ParamCP)
cp += amount;
else if (baseParam == ParamCraftsmanship)
craftsmanship += amount;
else if (baseParam == ParamControl)
control += amount;
}
foreach (var statIncrease in item.UnkData59)
IncreaseStat(statIncrease.BaseParam, statIncrease.BaseParamValue);
if (isHq)
{
foreach (var statIncrease in item.UnkData73)
IncreaseStat(statIncrease.BaseParamSpecial, statIncrease.BaseParamValueSpecial);
}
for (var i = 0; i < 5; ++i)
{
if (materiaTypes[i] == 0)
continue;
var materia = LuminaSheets.MateriaSheet.GetRow(materiaTypes[i])!;
IncreaseStat((int)materia.BaseParam.Row, materia.Value[materiaGrades[i]]);
}
cp = Math.Min(cp, CalculateParamCap(item, ParamCP));
craftsmanship = Math.Min(craftsmanship, CalculateParamCap(item, ParamCraftsmanship));
control = Math.Min(control, CalculateParamCap(item, ParamControl));
return (cp, craftsmanship, control, IsSpecialistSoulCrystal(item), IsSplendorousTool(itemId));
}
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/client/src/app/modules/gearsets/materia.service.ts#L265
private static int CalculateParamCap(Item item, int paramId)
{
var ilvl = item.LevelItem.Value!;
var param = LuminaSheets.BaseParamSheet.GetRow((uint)paramId)!;
var baseValue = paramId switch
{
ParamCP => ilvl.CP,
ParamCraftsmanship => ilvl.Craftsmanship,
ParamControl => ilvl.Control,
_ => 0
};
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/data-extraction/src/extractors/items.extractor.ts#L6
var slotMod = item.EquipSlotCategory.Row switch
{
1 => param.oneHWpnPct,
2 => param.OHPct,
3 => param.HeadPct,
4 => param.ChestPct,
5 => param.HandsPct,
6 => param.WaistPct,
7 => param.LegsPct,
8 => param.FeetPct,
9 => param.EarringPct,
10 => param.NecklacePct,
11 => param.BraceletPct,
12 => param.RingPct,
13 => param.twoHWpnPct,
14 => param.oneHWpnPct,
15 => param.ChestHeadPct,
16 => param.ChestHeadLegsFeetPct,
18 => param.LegsFeetPct,
19 => param.HeadChestHandsLegsFeetPct,
20 => param.ChestLegsGlovesPct,
21 => param.ChestLegsFeetPct,
_ => 0
};
var roleMod = param.MeldParam[item.BaseParamModifier];
// https://github.com/Caraxi/SimpleTweaksPlugin/pull/595
var cap = (int)Math.Round((float)baseValue * slotMod / (roleMod * 10f), MidpointRounding.AwayFromZero);
return cap == 0 ? int.MaxValue : cap;
}
private static bool IsSpecialistSoulCrystal(Item item) =>
// Soul Crystal ItemUICategory DoH Category
item.ItemUICategory.Row != 62 && item.ClassJobUse.Value!.ClassJobCategory.Row == 33;
private static bool IsSplendorousTool(uint itemId) =>
LuminaSheets.ItemSheetEnglish.GetRow(itemId)!.Description.ToDalamudString().TextValue.Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal);
// 38737 <= itemId && itemId <= 38744;
public static int CalculateCLvl(int characterLevel) =>
characterLevel <= 80
? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel
: (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId;
}