punish v6.8.18.0

This commit is contained in:
alydev 2025-10-09 07:47:19 +10:00
commit cfb4dea47e
316 changed files with 554088 additions and 0 deletions

View file

@ -0,0 +1,48 @@
namespace LLib.GameData;
public enum EClassJob : uint
{
Adventurer,
Gladiator,
Pugilist,
Marauder,
Lancer,
Archer,
Conjurer,
Thaumaturge,
Carpenter,
Blacksmith,
Armorer,
Goldsmith,
Leatherworker,
Weaver,
Alchemist,
Culinarian,
Miner,
Botanist,
Fisher,
Paladin,
Monk,
Warrior,
Dragoon,
Bard,
WhiteMage,
BlackMage,
Arcanist,
Summoner,
Scholar,
Rogue,
Ninja,
Machinist,
DarkKnight,
Astrologian,
Samurai,
RedMage,
BlueMage,
Gunbreaker,
Dancer,
Reaper,
Sage,
Viper,
Pictomancer
}

View file

@ -0,0 +1,188 @@
using System;
using System.Linq;
namespace LLib.GameData;
public static class EClassJobExtensions
{
public static bool IsClass(this EClassJob classJob)
{
bool flag;
switch (classJob)
{
case EClassJob.Gladiator:
case EClassJob.Pugilist:
case EClassJob.Marauder:
case EClassJob.Lancer:
case EClassJob.Archer:
case EClassJob.Conjurer:
case EClassJob.Thaumaturge:
case EClassJob.Arcanist:
case EClassJob.Rogue:
flag = true;
break;
default:
flag = false;
break;
}
if (!flag && !classJob.IsCrafter())
{
return classJob.IsGatherer();
}
return true;
}
public static bool HasBaseClass(this EClassJob classJob)
{
return (from x in Enum.GetValues<EClassJob>()
where x.IsClass()
select x).Any((EClassJob x) => x.AsJob() == classJob);
}
public static EClassJob AsJob(this EClassJob classJob)
{
return classJob switch
{
EClassJob.Gladiator => EClassJob.Paladin,
EClassJob.Marauder => EClassJob.Warrior,
EClassJob.Pugilist => EClassJob.Monk,
EClassJob.Lancer => EClassJob.Dragoon,
EClassJob.Rogue => EClassJob.Ninja,
EClassJob.Archer => EClassJob.Bard,
EClassJob.Conjurer => EClassJob.WhiteMage,
EClassJob.Thaumaturge => EClassJob.BlackMage,
EClassJob.Arcanist => EClassJob.Summoner,
_ => classJob,
};
}
public static bool IsTank(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Gladiator:
case EClassJob.Marauder:
case EClassJob.Paladin:
case EClassJob.Warrior:
case EClassJob.DarkKnight:
case EClassJob.Gunbreaker:
return true;
default:
return false;
}
}
public static bool IsHealer(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Conjurer:
case EClassJob.WhiteMage:
case EClassJob.Scholar:
case EClassJob.Astrologian:
case EClassJob.Sage:
return true;
default:
return false;
}
}
public static bool IsMelee(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Pugilist:
case EClassJob.Lancer:
case EClassJob.Monk:
case EClassJob.Dragoon:
case EClassJob.Rogue:
case EClassJob.Ninja:
case EClassJob.Samurai:
case EClassJob.Reaper:
case EClassJob.Viper:
return true;
default:
return false;
}
}
public static bool IsPhysicalRanged(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Archer:
case EClassJob.Bard:
case EClassJob.Machinist:
case EClassJob.Dancer:
return true;
default:
return false;
}
}
public static bool IsCaster(this EClassJob classJob)
{
switch (classJob)
{
case EClassJob.Thaumaturge:
case EClassJob.BlackMage:
case EClassJob.Arcanist:
case EClassJob.Summoner:
case EClassJob.RedMage:
case EClassJob.BlueMage:
case EClassJob.Pictomancer:
return true;
default:
return false;
}
}
public static bool DealsPhysicalDamage(this EClassJob classJob)
{
if (!classJob.IsTank() && !classJob.IsMelee())
{
return classJob.IsPhysicalRanged();
}
return true;
}
public static bool DealsMagicDamage(this EClassJob classJob)
{
if (!classJob.IsHealer())
{
return classJob.IsCaster();
}
return true;
}
public static bool IsCrafter(this EClassJob classJob)
{
if (classJob >= EClassJob.Carpenter)
{
return classJob <= EClassJob.Culinarian;
}
return false;
}
public static bool IsGatherer(this EClassJob classJob)
{
if (classJob >= EClassJob.Miner)
{
return classJob <= EClassJob.Fisher;
}
return false;
}
public static string ToFriendlyString(this EClassJob classJob)
{
return classJob switch
{
EClassJob.WhiteMage => "White Mage",
EClassJob.BlackMage => "Black Mage",
EClassJob.DarkKnight => "Dark Knight",
EClassJob.RedMage => "Red Mage",
EClassJob.BlueMage => "Blue Mage",
_ => classJob.ToString(),
};
}
}

View file

@ -0,0 +1,60 @@
namespace LLib.GameData;
public enum ETerritoryIntendedUse : byte
{
CityArea = 0,
OpenWorld = 1,
Inn = 2,
Dungeon = 3,
VariantDungeon = 4,
Gaol = 5,
StartingArea = 6,
QuestArea = 7,
AllianceRaid = 8,
QuestBattle = 9,
Trial = 10,
QuestArea2 = 12,
ResidentialArea = 13,
HousingInstances = 14,
QuestArea3 = 15,
Raid = 16,
Raid2 = 17,
Frontline = 18,
ChocoboSquare = 20,
RestorationEvent = 21,
Sanctum = 22,
GoldSaucer = 23,
LordOfVerminion = 25,
Diadem = 26,
HallOfTheNovice = 27,
CrystallineConflict = 28,
QuestBattle2 = 29,
Barracks = 30,
DeepDungeon = 31,
SeasonalEvent = 32,
TreasureMapDuty = 33,
SeasonalEventDuty = 34,
Battlehall = 35,
CrystallineConflict2 = 37,
Diadem2 = 38,
RivalWings = 39,
Unknown1 = 40,
Eureka = 41,
SeasonalEvent2 = 43,
LeapOfFaith = 44,
MaskedCarnivale = 45,
OceanFishing = 46,
Diadem3 = 47,
Bozja = 48,
IslandSanctuary = 49,
Battlehall2 = 50,
Battlehall3 = 51,
LargeScaleRaid = 52,
LargeScaleSavageRaid = 53,
QuestArea4 = 54,
TribalInstance = 56,
CriterionDuty = 57,
CriterionSavageDuty = 58,
Blunderville = 59,
CosmicExploration = 60
}

View file

@ -0,0 +1,53 @@
using System;
using System.Linq;
using Dalamud.Game.NativeWrapper;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace LLib.GameUI;
public static class LAddon
{
private const int UnitListCount = 18;
public unsafe static AtkUnitBase* GetAddonById(uint id)
{
AtkUnitList* ptr = &AtkStage.Instance()->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList;
for (int i = 0; i < 18; i++)
{
AtkUnitList* ptr2 = ptr + i;
foreach (int item in Enumerable.Range(0, Math.Min(ptr2->Count, ptr2->Entries.Length)))
{
AtkUnitBase* value = ptr2->Entries[item].Value;
if (value != null && value->Id == id)
{
return value;
}
}
}
return null;
}
public unsafe static bool TryGetAddonByName<T>(this IGameGui gameGui, string addonName, out T* addonPtr) where T : unmanaged
{
ArgumentNullException.ThrowIfNull(gameGui, "gameGui");
ArgumentException.ThrowIfNullOrEmpty(addonName, "addonName");
AtkUnitBasePtr addonByName = gameGui.GetAddonByName(addonName);
if (!addonByName.IsNull)
{
addonPtr = (T*)addonByName.Address;
return true;
}
addonPtr = null;
return false;
}
public unsafe static bool IsAddonReady(AtkUnitBase* addon)
{
if (addon->IsVisible)
{
return addon->UldManager.LoadedState == AtkLoadState.Loaded;
}
return false;
}
}

View file

@ -0,0 +1,21 @@
using System;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace LLib.GameUI;
public static class LAtkValue
{
public unsafe static string? ReadAtkString(this AtkValue atkValue)
{
if (atkValue.Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Undefined)
{
return null;
}
if (atkValue.String.HasValue)
{
return MemoryHelper.ReadSeStringNullTerminated(new IntPtr((byte*)atkValue.String)).WithCertainMacroCodeReplacements();
}
return null;
}
}

View file

@ -0,0 +1,16 @@
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Text.ReadOnly;
namespace LLib.GameUI;
public static class SeStringExtensions
{
public static string WithCertainMacroCodeReplacements(this SeString? str)
{
if (str == null)
{
return string.Empty;
}
return new ReadOnlySeString(str.Encode()).WithCertainMacroCodeReplacements();
}
}

View file

@ -0,0 +1,28 @@
namespace LLib.Gear;
public enum EBaseParam : byte
{
None = 0,
Strength = 1,
Dexterity = 2,
Vitality = 3,
Intelligence = 4,
Mind = 5,
Piety = 6,
GP = 10,
CP = 11,
DamagePhys = 12,
DamageMag = 13,
DefensePhys = 21,
DefenseMag = 24,
Tenacity = 19,
Crit = 27,
DirectHit = 22,
Determination = 44,
SpellSpeed = 46,
SkillSpeed = 45,
Craftsmanship = 70,
Control = 71,
Gathering = 72,
Perception = 73
}

View file

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace LLib.Gear;
public sealed record EquipmentStats(Dictionary<EBaseParam, StatInfo> Stats, byte MateriaCount)
{
private sealed class KeyValuePairComparer : IEqualityComparer<KeyValuePair<EBaseParam, StatInfo>>
{
public bool Equals(KeyValuePair<EBaseParam, StatInfo> x, KeyValuePair<EBaseParam, StatInfo> y)
{
if (x.Key == y.Key)
{
return object.Equals(x.Value, y.Value);
}
return false;
}
public int GetHashCode(KeyValuePair<EBaseParam, StatInfo> obj)
{
return HashCode.Combine((int)obj.Key, obj.Value);
}
}
public short Get(EBaseParam param)
{
return (short)(GetEquipment(param) + GetMateria(param));
}
public short GetEquipment(EBaseParam param)
{
Stats.TryGetValue(param, out StatInfo value);
return value?.EquipmentValue ?? 0;
}
public short GetMateria(EBaseParam param)
{
Stats.TryGetValue(param, out StatInfo value);
return value?.MateriaValue ?? 0;
}
public bool IsOvercapped(EBaseParam param)
{
Stats.TryGetValue(param, out StatInfo value);
return value?.Overcapped ?? false;
}
public bool Has(EBaseParam substat)
{
return Stats.ContainsKey(substat);
}
public bool HasMateria()
{
return Stats.Values.Any((StatInfo x) => x.MateriaValue > 0);
}
public bool Equals(EquipmentStats? other)
{
if (other != null && MateriaCount == other.MateriaCount)
{
return Stats.SequenceEqual(other.Stats, new KeyValuePairComparer());
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(MateriaCount, Stats);
}
}

View file

@ -0,0 +1,37 @@
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace LLib.Gear;
[Sheet("BaseParam")]
public readonly struct ExtendedBaseParam : IExcelRow<ExtendedBaseParam>
{
private const int ParamCount = 23;
public uint RowId => _003Crow_003EP;
public BaseParam BaseParam => new BaseParam(_003Cpage_003EP, _003Coffset_003EP, _003Crow_003EP);
public unsafe Collection<ushort> EquipSlotCategoryPct => new Collection<ushort>(_003Cpage_003EP, _003Coffset_003EP, _003Coffset_003EP, (delegate*<ExcelPage, uint, uint, uint, ushort>)(&EquipSlotCategoryPctCtor), 23);
public ExtendedBaseParam(ExcelPage page, uint offset, uint row)
{
_003Cpage_003EP = page;
_003Coffset_003EP = offset;
_003Crow_003EP = row;
}
private static ushort EquipSlotCategoryPctCtor(ExcelPage page, uint parentOffset, uint offset, uint i)
{
if (i != 0)
{
return page.ReadUInt16(offset + 8 + (i - 1) * 2);
}
return 0;
}
public static ExtendedBaseParam Create(ExcelPage page, uint offset, uint row)
{
return new ExtendedBaseParam(page, offset, row);
}
}

View file

@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace LLib.Gear;
public sealed class GearStatsCalculator
{
private sealed record MateriaInfo(EBaseParam BaseParam, Collection<short> Values, bool HasItem);
private const uint EternityRingItemId = 8575u;
private static readonly uint[] CanHaveOffhand = new uint[14]
{
2u, 6u, 8u, 12u, 14u, 16u, 18u, 20u, 22u, 24u,
26u, 28u, 30u, 32u
};
private readonly ExcelSheet<Item> _itemSheet;
private readonly Dictionary<(uint ItemLevel, EBaseParam BaseParam), ushort> _itemLevelStatCaps = new Dictionary<(uint, EBaseParam), ushort>();
private readonly Dictionary<(EBaseParam BaseParam, int EquipSlotCategory), ushort> _equipSlotCategoryPct;
private readonly Dictionary<uint, MateriaInfo> _materiaStats;
public GearStatsCalculator(IDataManager? dataManager)
: this(dataManager?.GetExcelSheet<ItemLevel>() ?? throw new ArgumentNullException("dataManager"), dataManager.GetExcelSheet<ExtendedBaseParam>(), dataManager.GetExcelSheet<Materia>(), dataManager.GetExcelSheet<Item>())
{
}
public GearStatsCalculator(ExcelSheet<ItemLevel> itemLevelSheet, ExcelSheet<ExtendedBaseParam> baseParamSheet, ExcelSheet<Materia> materiaSheet, ExcelSheet<Item> itemSheet)
{
ArgumentNullException.ThrowIfNull(itemLevelSheet, "itemLevelSheet");
ArgumentNullException.ThrowIfNull(baseParamSheet, "baseParamSheet");
ArgumentNullException.ThrowIfNull(materiaSheet, "materiaSheet");
ArgumentNullException.ThrowIfNull(itemSheet, "itemSheet");
_itemSheet = itemSheet;
foreach (ItemLevel item in itemLevelSheet)
{
_itemLevelStatCaps[(item.RowId, EBaseParam.Strength)] = item.Strength;
_itemLevelStatCaps[(item.RowId, EBaseParam.Dexterity)] = item.Dexterity;
_itemLevelStatCaps[(item.RowId, EBaseParam.Vitality)] = item.Vitality;
_itemLevelStatCaps[(item.RowId, EBaseParam.Intelligence)] = item.Intelligence;
_itemLevelStatCaps[(item.RowId, EBaseParam.Mind)] = item.Mind;
_itemLevelStatCaps[(item.RowId, EBaseParam.Piety)] = item.Piety;
_itemLevelStatCaps[(item.RowId, EBaseParam.GP)] = item.GP;
_itemLevelStatCaps[(item.RowId, EBaseParam.CP)] = item.CP;
_itemLevelStatCaps[(item.RowId, EBaseParam.DamagePhys)] = item.PhysicalDamage;
_itemLevelStatCaps[(item.RowId, EBaseParam.DamageMag)] = item.MagicalDamage;
_itemLevelStatCaps[(item.RowId, EBaseParam.DefensePhys)] = item.Defense;
_itemLevelStatCaps[(item.RowId, EBaseParam.DefenseMag)] = item.MagicDefense;
_itemLevelStatCaps[(item.RowId, EBaseParam.Tenacity)] = item.Tenacity;
_itemLevelStatCaps[(item.RowId, EBaseParam.Crit)] = item.CriticalHit;
_itemLevelStatCaps[(item.RowId, EBaseParam.DirectHit)] = item.DirectHitRate;
_itemLevelStatCaps[(item.RowId, EBaseParam.Determination)] = item.Determination;
_itemLevelStatCaps[(item.RowId, EBaseParam.SpellSpeed)] = item.SpellSpeed;
_itemLevelStatCaps[(item.RowId, EBaseParam.SkillSpeed)] = item.SkillSpeed;
_itemLevelStatCaps[(item.RowId, EBaseParam.Gathering)] = item.Gathering;
_itemLevelStatCaps[(item.RowId, EBaseParam.Perception)] = item.Perception;
_itemLevelStatCaps[(item.RowId, EBaseParam.Craftsmanship)] = item.Craftsmanship;
_itemLevelStatCaps[(item.RowId, EBaseParam.Control)] = item.Control;
}
_equipSlotCategoryPct = baseParamSheet.SelectMany((ExtendedBaseParam x) => from y in Enumerable.Range(0, x.EquipSlotCategoryPct.Count)
select ((EBaseParam)x.RowId, y: y, x.EquipSlotCategoryPct[y])).ToDictionary(((EBaseParam, int y, ushort) x) => (x.Item1, x.y), ((EBaseParam, int y, ushort) x) => x.Item3);
_materiaStats = materiaSheet.Where((Materia x) => x.RowId != 0 && x.BaseParam.RowId != 0).ToDictionary((Materia x) => x.RowId, (Materia x) => new MateriaInfo((EBaseParam)x.BaseParam.RowId, x.Value, x.Item[0].RowId != 0));
}
public unsafe EquipmentStats CalculateGearStats(InventoryItem* item)
{
List<(uint, byte)> list = new List<(uint, byte)>();
byte b = 0;
if (item->ItemId != 8575)
{
for (int i = 0; i < 5; i++)
{
ushort num = item->Materia[i];
if (num != 0)
{
b++;
list.Add((num, item->MateriaGrades[i]));
}
}
}
return CalculateGearStats(_itemSheet.GetRow(item->ItemId), item->Flags.HasFlag(InventoryItem.ItemFlags.HighQuality), list)with
{
MateriaCount = b
};
}
public EquipmentStats CalculateGearStats(Item item, bool highQuality, IReadOnlyList<(uint MateriaId, byte Grade)> materias)
{
ArgumentNullException.ThrowIfNull(materias, "materias");
Dictionary<EBaseParam, StatInfo> dictionary = new Dictionary<EBaseParam, StatInfo>();
for (int i = 0; i < item.BaseParam.Count; i++)
{
AddEquipmentStat(dictionary, item.BaseParam[i], item.BaseParamValue[i]);
}
if (highQuality)
{
for (int j = 0; j < item.BaseParamSpecial.Count; j++)
{
AddEquipmentStat(dictionary, item.BaseParamSpecial[j], item.BaseParamValueSpecial[j]);
}
}
foreach (var materia in materias)
{
if (_materiaStats.TryGetValue(materia.MateriaId, out MateriaInfo value))
{
AddMateriaStat(item, dictionary, value, materia.Grade);
}
}
return new EquipmentStats(dictionary, 0);
}
private static void AddEquipmentStat(Dictionary<EBaseParam, StatInfo> result, RowRef<BaseParam> baseParam, short value)
{
if (baseParam.RowId != 0)
{
if (result.TryGetValue((EBaseParam)baseParam.RowId, out StatInfo value2))
{
result[(EBaseParam)baseParam.RowId] = value2 with
{
EquipmentValue = (short)(value2.EquipmentValue + value)
};
}
else
{
result[(EBaseParam)baseParam.RowId] = new StatInfo(value, 0, Overcapped: false);
}
}
}
private void AddMateriaStat(Item item, Dictionary<EBaseParam, StatInfo> result, MateriaInfo materiaInfo, short grade)
{
if (!result.TryGetValue(materiaInfo.BaseParam, out StatInfo value))
{
value = (result[materiaInfo.BaseParam] = new StatInfo(0, 0, Overcapped: false));
}
if (materiaInfo.HasItem)
{
short num = (short)(GetMaximumStatValue(item, materiaInfo.BaseParam) - value.EquipmentValue);
if (value.MateriaValue + materiaInfo.Values[grade] > num)
{
result[materiaInfo.BaseParam] = value with
{
MateriaValue = num,
Overcapped = true
};
}
else
{
result[materiaInfo.BaseParam] = value with
{
MateriaValue = (short)(value.MateriaValue + materiaInfo.Values[grade])
};
}
}
else
{
result[materiaInfo.BaseParam] = value with
{
MateriaValue = (short)(value.MateriaValue + materiaInfo.Values[grade])
};
}
}
public short GetMaximumStatValue(Item item, EBaseParam baseParamValue)
{
if (_itemLevelStatCaps.TryGetValue((item.LevelItem.RowId, baseParamValue), out var value))
{
return (short)Math.Round((float)(value * _equipSlotCategoryPct[(baseParamValue, (int)item.EquipSlotCategory.RowId)]) / 1000f, MidpointRounding.AwayFromZero);
}
return 0;
}
public unsafe short CalculateAverageItemLevel(InventoryContainer* container)
{
uint num = 0u;
int num2 = 12;
for (int i = 0; i < 13; i++)
{
if (i == 5)
{
continue;
}
InventoryItem* inventorySlot = container->GetInventorySlot(i);
if (inventorySlot == null || inventorySlot->ItemId == 0)
{
continue;
}
Item? rowOrDefault = _itemSheet.GetRowOrDefault(inventorySlot->ItemId);
if (!rowOrDefault.HasValue)
{
continue;
}
if (rowOrDefault.Value.ItemUICategory.RowId == 105)
{
if (i == 0)
{
num2--;
}
num2--;
continue;
}
if (i == 0 && !CanHaveOffhand.Contains(rowOrDefault.Value.ItemUICategory.RowId))
{
num += rowOrDefault.Value.LevelItem.RowId;
i++;
}
num += rowOrDefault.Value.LevelItem.RowId;
}
return (short)(num / num2);
}
}

View file

@ -0,0 +1,6 @@
namespace LLib.Gear;
public sealed record StatInfo(short EquipmentValue, short MateriaValue, bool Overcapped)
{
public short TotalValue => (short)(EquipmentValue + MateriaValue);
}

View file

@ -0,0 +1,14 @@
namespace LLib.ImGui;
public interface IPersistableWindowConfig
{
WindowConfig? WindowConfig { get; }
void SaveWindowConfig();
}
public interface IPersistableWindowConfig<out T> : IPersistableWindowConfig where T : WindowConfig
{
new T? WindowConfig { get; }
WindowConfig? IPersistableWindowConfig.WindowConfig => WindowConfig;
}

188
LLib/LLib.ImGui/LWindow.cs Normal file
View file

@ -0,0 +1,188 @@
using System.Runtime.CompilerServices;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Windowing;
namespace LLib.ImGui;
public abstract class LWindow : Window
{
private bool _initializedConfig;
private bool _wasCollapsedLastFrame;
protected bool ClickedHeaderLastFrame { get; private set; }
protected bool ClickedHeaderCurrentFrame { get; private set; }
protected bool UncollapseNextFrame { get; set; }
public bool IsOpenAndUncollapsed
{
get
{
if (base.IsOpen)
{
return !_wasCollapsedLastFrame;
}
return false;
}
set
{
base.IsOpen = value;
UncollapseNextFrame = value;
}
}
protected bool IsPinned
{
get
{
return InternalIsPinned(this);
}
set
{
InternalIsPinned(this) = value;
}
}
protected bool IsClickthrough
{
get
{
return InternalIsClickthrough(this);
}
set
{
InternalIsClickthrough(this) = value;
}
}
protected int? Alpha
{
get
{
return (int?)(100000f * InternalAlpha(this));
}
set
{
InternalAlpha(this) = (float?)value / 100000f;
}
}
protected LWindow(string windowName, ImGuiWindowFlags flags = ImGuiWindowFlags.None, bool forceMainWindow = false)
: base(windowName, flags, forceMainWindow)
{
}
private void LoadWindowConfig()
{
if (!(this is IPersistableWindowConfig persistableWindowConfig))
{
return;
}
WindowConfig windowConfig = persistableWindowConfig.WindowConfig;
if (windowConfig != null)
{
if (base.AllowPinning)
{
IsPinned = windowConfig.IsPinned;
}
if (base.AllowClickthrough)
{
IsClickthrough = windowConfig.IsClickthrough;
}
Alpha = windowConfig.Alpha;
}
_initializedConfig = true;
}
private void UpdateWindowConfig()
{
if (!(this is IPersistableWindowConfig persistableWindowConfig) || Dalamud.Bindings.ImGui.ImGui.IsAnyMouseDown())
{
return;
}
WindowConfig windowConfig = persistableWindowConfig.WindowConfig;
if (windowConfig != null)
{
bool flag = false;
if (base.AllowPinning && windowConfig.IsPinned != IsPinned)
{
windowConfig.IsPinned = IsPinned;
flag = true;
}
if (base.AllowClickthrough && windowConfig.IsClickthrough != IsClickthrough)
{
windowConfig.IsClickthrough = IsClickthrough;
flag = true;
}
if (windowConfig.Alpha != Alpha)
{
windowConfig.Alpha = Alpha;
flag = true;
}
if (flag)
{
persistableWindowConfig.SaveWindowConfig();
}
}
}
public void ToggleOrUncollapse()
{
IsOpenAndUncollapsed = !IsOpenAndUncollapsed;
}
public override void OnOpen()
{
UncollapseNextFrame = true;
base.OnOpen();
}
public override void Update()
{
_wasCollapsedLastFrame = true;
}
public override void PreDraw()
{
if (!_initializedConfig)
{
LoadWindowConfig();
}
if (UncollapseNextFrame)
{
Dalamud.Bindings.ImGui.ImGui.SetNextWindowCollapsed(collapsed: false);
UncollapseNextFrame = false;
}
base.PreDraw();
ClickedHeaderLastFrame = ClickedHeaderCurrentFrame;
ClickedHeaderCurrentFrame = false;
}
public sealed override void Draw()
{
_wasCollapsedLastFrame = false;
DrawContent();
}
public abstract void DrawContent();
public override void PostDraw()
{
base.PostDraw();
if (_initializedConfig)
{
UpdateWindowConfig();
}
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "internalIsPinned")]
private static extern ref bool InternalIsPinned(Window @this);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "internalIsClickthrough")]
private static extern ref bool InternalIsClickthrough(Window @this);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "internalAlpha")]
private static extern ref float? InternalAlpha(Window @this);
}

View file

@ -0,0 +1,10 @@
namespace LLib.ImGui;
public class WindowConfig
{
public bool IsPinned { get; set; }
public bool IsClickthrough { get; set; }
public int? Alpha { get; set; }
}

View file

@ -0,0 +1,14 @@
namespace LLib.Shop.Model;
public sealed class ItemForSale
{
public required int Position { get; init; }
public required uint ItemId { get; init; }
public required string? ItemName { get; init; }
public required uint Price { get; init; }
public required uint OwnedItems { get; init; }
}

View file

@ -0,0 +1,24 @@
using System;
namespace LLib.Shop.Model;
public sealed class PurchaseState
{
public int DesiredItems { get; }
public int OwnedItems { get; set; }
public int ItemsLeftToBuy => Math.Max(0, DesiredItems - OwnedItems);
public bool IsComplete => ItemsLeftToBuy == 0;
public bool IsAwaitingYesNo { get; set; }
public DateTime NextStep { get; set; } = DateTime.MinValue;
public PurchaseState(int desiredItems, int ownedItems)
{
DesiredItems = desiredItems;
OwnedItems = ownedItems;
}
}

View file

@ -0,0 +1,23 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace LLib.Shop;
public interface IShopWindow
{
bool IsEnabled { get; }
bool IsOpen { get; set; }
Vector2? Position { get; set; }
int GetCurrencyCount();
unsafe void UpdateShopStock(AtkUnitBase* addon);
unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow);
void SaveExternalPluginState();
void RestoreExternalPluginState();
}

View file

@ -0,0 +1,275 @@
using System;
using System.Numerics;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using LLib.Shop.Model;
namespace LLib.Shop;
public class RegularShopBase
{
private readonly IShopWindow _parentWindow;
private readonly string _addonName;
private readonly IPluginLog _pluginLog;
private readonly IGameGui _gameGui;
private readonly IAddonLifecycle _addonLifecycle;
public ItemForSale? ItemForSale { get; set; }
public PurchaseState? PurchaseState { get; private set; }
public bool AutoBuyEnabled => PurchaseState != null;
public bool IsAwaitingYesNo
{
get
{
return PurchaseState?.IsAwaitingYesNo ?? false;
}
set
{
PurchaseState.IsAwaitingYesNo = value;
}
}
public RegularShopBase(IShopWindow parentWindow, string addonName, IPluginLog pluginLog, IGameGui gameGui, IAddonLifecycle addonLifecycle)
{
_parentWindow = parentWindow;
_addonName = addonName;
_pluginLog = pluginLog;
_gameGui = gameGui;
_addonLifecycle = addonLifecycle;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize);
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate);
}
private unsafe void ShopPostSetup(AddonEvent type, AddonArgs args)
{
if (!_parentWindow.IsEnabled)
{
ItemForSale = null;
_parentWindow.IsOpen = false;
return;
}
_parentWindow.UpdateShopStock((AtkUnitBase*)args.Addon.Address);
PostUpdateShopStock();
if (ItemForSale != null)
{
_parentWindow.IsOpen = true;
}
}
private void ShopPreFinalize(AddonEvent type, AddonArgs args)
{
PurchaseState = null;
_parentWindow.RestoreExternalPluginState();
_parentWindow.IsOpen = false;
}
private unsafe void ShopPostUpdate(AddonEvent type, AddonArgs args)
{
if (!_parentWindow.IsEnabled)
{
ItemForSale = null;
_parentWindow.IsOpen = false;
return;
}
_parentWindow.UpdateShopStock((AtkUnitBase*)args.Addon.Address);
PostUpdateShopStock();
if (ItemForSale != null)
{
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
short num = 0;
short num2 = 0;
address->GetPosition(&num, &num2);
short num3 = 0;
short num4 = 0;
address->GetSize(&num3, &num4, scaled: true);
num += num3;
Vector2? position = _parentWindow.Position;
if (position.HasValue)
{
Vector2 valueOrDefault = position.GetValueOrDefault();
if ((short)valueOrDefault.X != num || (short)valueOrDefault.Y != num2)
{
_parentWindow.Position = new Vector2(num, num2);
}
}
_parentWindow.IsOpen = true;
}
else
{
_parentWindow.IsOpen = false;
}
}
private void PostUpdateShopStock()
{
if (ItemForSale != null && PurchaseState != null)
{
int ownedItems = (int)ItemForSale.OwnedItems;
if (PurchaseState.OwnedItems != ownedItems)
{
PurchaseState.OwnedItems = ownedItems;
PurchaseState.NextStep = DateTime.Now.AddSeconds(0.25);
}
}
}
public unsafe int GetItemCount(uint itemId)
{
return InventoryManager.Instance()->GetInventoryItemCount(itemId, isHq: false, checkEquipped: false, checkArmory: false, 0);
}
public int GetMaxItemsToPurchase()
{
if (ItemForSale == null)
{
return 0;
}
return (int)(_parentWindow.GetCurrencyCount() / ItemForSale.Price);
}
public void CancelAutoPurchase()
{
PurchaseState = null;
_parentWindow.RestoreExternalPluginState();
}
public void StartAutoPurchase(int toPurchase)
{
PurchaseState = new PurchaseState((int)ItemForSale.OwnedItems + toPurchase, (int)ItemForSale.OwnedItems);
_parentWindow.SaveExternalPluginState();
}
public unsafe void HandleNextPurchaseStep()
{
if (ItemForSale == null || PurchaseState == null)
{
return;
}
int num = DetermineMaxStackSize(ItemForSale.ItemId);
if (num == 0 && !HasFreeInventorySlot())
{
_pluginLog.Warning("No free inventory slots, can't buy more " + ItemForSale.ItemName);
PurchaseState = null;
_parentWindow.RestoreExternalPluginState();
}
else if (!PurchaseState.IsComplete)
{
if (PurchaseState.NextStep <= DateTime.Now && _gameGui.TryGetAddonByName<AtkUnitBase>(_addonName, out var addonPtr))
{
int num2 = Math.Min(PurchaseState.ItemsLeftToBuy, num);
_pluginLog.Information($"Buying {num2}x {ItemForSale.ItemName}");
_parentWindow.TriggerPurchase(addonPtr, num2);
PurchaseState.NextStep = DateTime.MaxValue;
PurchaseState.IsAwaitingYesNo = true;
}
}
else
{
_pluginLog.Information($"Stopping item purchase (desired = {PurchaseState.DesiredItems}, owned = {PurchaseState.OwnedItems})");
PurchaseState = null;
_parentWindow.RestoreExternalPluginState();
}
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize);
_addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate);
}
public bool HasFreeInventorySlot()
{
return CountFreeInventorySlots() > 0;
}
public unsafe int CountFreeInventorySlots()
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return 0;
}
int num = 0;
for (InventoryType inventoryType = InventoryType.Inventory1; inventoryType <= InventoryType.Inventory4; inventoryType++)
{
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(inventoryType);
for (int i = 0; i < inventoryContainer->Size; i++)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
if (inventorySlot == null || inventorySlot->ItemId == 0)
{
num++;
}
}
}
return num;
}
private unsafe int DetermineMaxStackSize(uint itemId)
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return 0;
}
int num = 0;
for (InventoryType inventoryType = InventoryType.Inventory1; inventoryType <= InventoryType.Inventory4; inventoryType++)
{
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(inventoryType);
for (int i = 0; i < inventoryContainer->Size; i++)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
if (inventorySlot == null || inventorySlot->ItemId == 0)
{
return 99;
}
if (inventorySlot->ItemId == itemId)
{
num += 999 - inventorySlot->Quantity;
if (num >= 99)
{
break;
}
}
}
}
return Math.Min(99, num);
}
public unsafe int CountInventorySlotsWithCondition(uint itemId, Predicate<int> predicate)
{
ArgumentNullException.ThrowIfNull(predicate, "predicate");
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return 0;
}
int num = 0;
for (InventoryType inventoryType = InventoryType.Inventory1; inventoryType <= InventoryType.Inventory4; inventoryType++)
{
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(inventoryType);
for (int i = 0; i < inventoryContainer->Size; i++)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
if (inventorySlot != null && inventorySlot->ItemId != 0 && inventorySlot->ItemId == itemId && predicate(inventorySlot->Quantity))
{
num++;
}
}
}
return num;
}
}

35
LLib/LLib.csproj Normal file
View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>LLib</AssemblyName>
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
<TargetFramework>netcoreapp9.0</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup>
<LangVersion>12.0</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup />
<ItemGroup />
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
</Reference>
<Reference Include="Lumina">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
</Reference>
<Reference Include="Dalamud.Bindings.ImGui">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\Dalamud.Bindings.ImGui.dll</HintPath>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
</Reference>
<Reference Include="InteropGenerator.Runtime">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\dev\InteropGenerator.Runtime.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,100 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
namespace LLib;
public sealed class DalamudReflector : IDisposable
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IFramework _framework;
private readonly IPluginLog _pluginLog;
private readonly Dictionary<string, IDalamudPlugin> _pluginCache = new Dictionary<string, IDalamudPlugin>();
private bool _pluginsChanged;
public DalamudReflector(IDalamudPluginInterface pluginInterface, IFramework framework, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_framework = framework;
_pluginLog = pluginLog;
object pluginManager = GetPluginManager();
pluginManager.GetType().GetEvent("OnInstalledPluginsChanged").AddEventHandler(pluginManager, new Action(OnInstalledPluginsChanged));
_framework.Update += FrameworkUpdate;
}
public void Dispose()
{
_framework.Update -= FrameworkUpdate;
object pluginManager = GetPluginManager();
pluginManager.GetType().GetEvent("OnInstalledPluginsChanged").RemoveEventHandler(pluginManager, new Action(OnInstalledPluginsChanged));
}
private void FrameworkUpdate(IFramework framework)
{
if (_pluginsChanged)
{
_pluginsChanged = false;
_pluginCache.Clear();
}
}
private object GetPluginManager()
{
return _pluginInterface.GetType().Assembly.GetType("Dalamud.Service`1", throwOnError: true).MakeGenericType(_pluginInterface.GetType().Assembly.GetType("Dalamud.Plugin.Internal.PluginManager", throwOnError: true)).GetMethod("Get")
.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null);
}
public bool TryGetDalamudPlugin(string internalName, [MaybeNullWhen(false)] out IDalamudPlugin instance, bool suppressErrors = false, bool ignoreCache = false)
{
if (!ignoreCache && _pluginCache.TryGetValue(internalName, out instance))
{
return true;
}
try
{
object pluginManager = GetPluginManager();
foreach (object item in (IList)pluginManager.GetType().GetProperty("InstalledPlugins").GetValue(pluginManager))
{
if ((string)item.GetType().GetProperty("Name").GetValue(item) == internalName)
{
IDalamudPlugin dalamudPlugin = (IDalamudPlugin)((item.GetType().Name == "LocalDevPlugin") ? item.GetType().BaseType : item.GetType()).GetField("instance", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(item);
if (dalamudPlugin != null)
{
instance = dalamudPlugin;
_pluginCache[internalName] = dalamudPlugin;
return true;
}
if (!suppressErrors)
{
_pluginLog.Warning("[DalamudReflector] Found requested plugin " + internalName + " but it was null");
}
}
}
instance = null;
return false;
}
catch (Exception ex)
{
if (!suppressErrors)
{
_pluginLog.Error(ex, "Can't find " + internalName + " plugin: " + ex.Message);
}
instance = null;
return false;
}
}
private void OnInstalledPluginsChanged()
{
_pluginLog.Verbose("Installed plugins changed event fired");
_pluginsChanged = true;
}
}

View file

@ -0,0 +1,104 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Plugin.Services;
using Lumina.Excel;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
namespace LLib;
public static class DataManagerExtensions
{
public static ReadOnlySeString? GetSeString<T>(this IDataManager dataManager, string key) where T : struct, IQuestDialogueText, IExcelRow<T>
{
ArgumentNullException.ThrowIfNull(dataManager, "dataManager");
return dataManager.GetExcelSheet<T>().Cast<T?>().SingleOrDefault((T? x) => x.Value.Key == key)?.Value;
}
public static string? GetString<T>(this IDataManager dataManager, string key, IPluginLog? pluginLog) where T : struct, IQuestDialogueText, IExcelRow<T>
{
string text = dataManager.GetSeString<T>(key)?.WithCertainMacroCodeReplacements();
pluginLog?.Verbose($"{typeof(T).Name}.{key} => {text}");
return text;
}
public static Regex? GetRegex<T>(this IDataManager dataManager, string key, IPluginLog? pluginLog) where T : struct, IQuestDialogueText, IExcelRow<T>
{
ReadOnlySeString? seString = dataManager.GetSeString<T>(key);
if (!seString.HasValue)
{
return null;
}
string text = string.Join("", seString.Select((ReadOnlySePayload payload) => (payload.Type == ReadOnlySePayloadType.Text) ? Regex.Escape(payload.ToString()) : "(.*)"));
pluginLog?.Verbose($"{typeof(T).Name}.{key} => /{text}/");
return new Regex(text);
}
public static ReadOnlySeString? GetSeString<T>(this IDataManager dataManager, uint rowId, Func<T, ReadOnlySeString?> mapper) where T : struct, IExcelRow<T>
{
ArgumentNullException.ThrowIfNull(dataManager, "dataManager");
ArgumentNullException.ThrowIfNull(mapper, "mapper");
T? rowOrDefault = dataManager.GetExcelSheet<T>().GetRowOrDefault(rowId);
if (!rowOrDefault.HasValue)
{
return null;
}
return mapper(rowOrDefault.Value);
}
public static string? GetString<T>(this IDataManager dataManager, uint rowId, Func<T, ReadOnlySeString?> mapper, IPluginLog? pluginLog = null) where T : struct, IExcelRow<T>
{
string text = dataManager.GetSeString(rowId, mapper)?.WithCertainMacroCodeReplacements();
pluginLog?.Verbose($"{typeof(T).Name}.{rowId} => {text}");
return text;
}
public static Regex? GetRegex<T>(this IDataManager dataManager, uint rowId, Func<T, ReadOnlySeString?> mapper, IPluginLog? pluginLog = null) where T : struct, IExcelRow<T>
{
ReadOnlySeString? seString = dataManager.GetSeString(rowId, mapper);
if (!seString.HasValue)
{
return null;
}
Regex regex = seString.ToRegex();
pluginLog?.Verbose($"{typeof(T).Name}.{rowId} => /{regex}/");
return regex;
}
public static Regex? GetRegex<T>(this T excelRow, Func<T, ReadOnlySeString?> mapper, IPluginLog? pluginLog) where T : struct, IExcelRow<T>
{
ArgumentNullException.ThrowIfNull(mapper, "mapper");
ReadOnlySeString? text = mapper(excelRow);
if (!text.HasValue)
{
return null;
}
Regex regex = text.ToRegex();
pluginLog?.Verbose($"{typeof(T).Name}.regex => /{regex}/");
return regex;
}
public static Regex ToRegex(this ReadOnlySeString? text)
{
ArgumentNullException.ThrowIfNull(text, "text");
return new Regex(string.Join("", text.Value.Select((ReadOnlySePayload payload) => (payload.Type == ReadOnlySePayloadType.Text) ? Regex.Escape(payload.ToString()) : "(.*)")));
}
public static string WithCertainMacroCodeReplacements(this ReadOnlySeString text)
{
return string.Join("", text.Select((ReadOnlySePayload payload) => payload.Type switch
{
ReadOnlySePayloadType.Text => payload.ToString(),
ReadOnlySePayloadType.Macro => payload.MacroCode switch
{
MacroCode.NewLine => "",
MacroCode.NonBreakingSpace => " ",
MacroCode.Hyphen => "-",
MacroCode.SoftHyphen => "",
_ => payload.ToString(),
},
_ => payload.ToString(),
}));
}
}

View file

@ -0,0 +1,10 @@
using Lumina.Text.ReadOnly;
namespace LLib;
public interface IQuestDialogueText
{
ReadOnlySeString Key { get; }
ReadOnlySeString Value { get; }
}

View file

@ -0,0 +1,26 @@
using Lumina.Excel;
using Lumina.Text.ReadOnly;
namespace LLib;
[Sheet("PleaseSpecifyTheSheetExplicitly")]
public readonly struct QuestDialogueText : IQuestDialogueText, IExcelRow<QuestDialogueText>
{
public uint RowId => _003Crow_003EP;
public ReadOnlySeString Key => _003Cpage_003EP.ReadString(_003Coffset_003EP, _003Coffset_003EP);
public ReadOnlySeString Value => _003Cpage_003EP.ReadString(_003Coffset_003EP + 4, _003Coffset_003EP);
public QuestDialogueText(ExcelPage page, uint offset, uint row)
{
_003Cpage_003EP = page;
_003Coffset_003EP = offset;
_003Crow_003EP = row;
}
static QuestDialogueText IExcelRow<QuestDialogueText>.Create(ExcelPage page, uint offset, uint row)
{
return new QuestDialogueText(page, offset, row);
}
}