Remove GameData, move a bunch of customization data to Penumbra.GameData and the rest to Glamourer, update accordingly. Some reformatting and cleanup.

This commit is contained in:
Ottermandias 2023-12-22 14:20:50 +01:00
parent e9d0e61b4c
commit 987c26a51d
83 changed files with 444 additions and 1620 deletions

View file

@ -1,11 +1,7 @@
using System.Buffers.Text;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Structs;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;

View file

@ -1,10 +1,10 @@
using System;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Interop.Structs;
using Glamourer.State;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Automation;
@ -74,7 +74,7 @@ public class AutoDesign
var ret = new JObject
{
["Gearset"] = GearsetIndex,
["JobGroup"] = Jobs.Id,
["JobGroup"] = Jobs.Id.Id,
};
return ret;

View file

@ -4,14 +4,12 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Unlocks;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
@ -209,7 +207,7 @@ public class AutoDesignApplier : IDisposable
if (!GetPlayerSet(id, out var set))
{
if (_state.TryGetValue(id, out var s))
s.LastJob = (byte)newJob.Id;
s.LastJob = newJob.Id;
return;
}

View file

@ -10,7 +10,6 @@ using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
@ -516,7 +515,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
var jobs = conditions["JobGroup"]?.ToObject<int>() ?? -1;
if (jobs >= 0)
{
if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup))
if (!_jobs.JobGroups.TryGetValue((JobGroupId)jobs, out var jobGroup))
{
Glamourer.Messager.NotificationMessage(
$"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.",

View file

@ -3,37 +3,30 @@ using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Designs;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Structs;
using Penumbra.String;
namespace Glamourer.Automation;
public class FixedDesignMigrator
public class FixedDesignMigrator(JobService jobs)
{
private readonly JobService _jobs;
private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData;
public FixedDesignMigrator(JobService jobs)
=> _jobs = jobs;
private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData;
public void ConsumeMigratedData(ActorManager actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager)
{
if (_migratedData == null)
return;
foreach (var data in _migratedData)
foreach (var (name, data) in _migratedData)
{
var allEnabled = true;
var name = data.Name;
if (autoManager.Any(d => name == d.Name))
continue;
var id = ActorIdentifier.Invalid;
if (ByteString.FromString(data.Name, out var byteString, false))
if (ByteString.FromString(name, out var byteString))
{
id = actors.CreatePlayer(byteString, ushort.MaxValue);
if (!id.IsValid)
@ -46,16 +39,15 @@ public class FixedDesignMigrator
id = actors.CreatePlayer(byteString, actors.Data.Worlds.First().Key);
if (!id.IsValid)
{
Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {data.Name}.", NotificationType.Error);
allEnabled = false;
Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {name}.", NotificationType.Error);
continue;
}
}
autoManager.AddDesignSet(name, id);
autoManager.SetState(autoManager.Count - 1, allEnabled);
autoManager.SetState(autoManager.Count - 1, true);
var set = autoManager[^1];
foreach (var design in data.Data.AsEnumerable().Reverse())
foreach (var design in data.AsEnumerable().Reverse())
{
if (!designFileSystem.Find(design.Item1, out var child) || child is not DesignFileSystem.Leaf leaf)
{
@ -96,7 +88,7 @@ public class FixedDesignMigrator
}
var job = obj["JobGroups"]?.ToObject<int>() ?? -1;
if (job < 0 || !_jobs.JobGroups.TryGetValue((ushort)job, out var group))
if (job < 0 || !jobs.JobGroups.TryGetValue((JobGroupId)job, out var group))
{
Glamourer.Messager.NotificationMessage("Could not semi-migrate fixed design: Invalid job group specified.",
NotificationType.Warning);

View file

@ -1,7 +1,6 @@
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
@ -85,12 +84,12 @@ public class DesignBase
internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant;
private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible;
public bool SetCustomize(CustomizationService customizationService, Customize customize)
public bool SetCustomize(CustomizationService customizationService, CustomizeArray customize)
{
if (customize.Equals(_designData.Customize))
return false;
_designData.Customize.Load(customize);
_designData.Customize = customize;
CustomizationSet = customizationService.Service.GetList(customize.Clan, customize.Gender);
return true;
}
@ -443,7 +442,7 @@ public class DesignBase
{
design._designData.ModelId = 0;
design._designData.IsHuman = true;
design.SetCustomize(customizations, Customize.Default);
design.SetCustomize(customizations, CustomizeArray.Default);
Glamourer.Messager.NotificationMessage("The loaded design does not contain any customization data, reset to default.",
NotificationType.Warning);
return;

View file

@ -1,7 +1,5 @@
using System;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using OtterGui;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
@ -62,7 +60,7 @@ public class DesignBase64Migration
data.SetHatVisible((bytes[90] & 0x01) == 0);
data.SetVisor((bytes[90] & 0x10) != 0);
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
break;
}
case 5:
@ -73,7 +71,7 @@ public class DesignBase64Migration
data.SetHatVisible((bytes[90] & 0x01) == 0);
data.SetVisor((bytes[90] & 0x10) != 0);
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
break;
default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}.");
}
@ -102,11 +100,11 @@ public class DesignBase64Migration
if (!humans.IsHuman(data.ModelId))
{
data.LoadNonHuman(data.ModelId, *(Customize*)(ptr + 4), (nint)eq);
data.LoadNonHuman(data.ModelId, *(CustomizeArray*)(ptr + 4), (nint)eq);
return data;
}
data.Customize.Load(*(Customize*)(ptr + 4));
data.Customize = *(CustomizeArray*)(ptr + 4);
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{
var mdl = eq[idx];
@ -187,7 +185,7 @@ public class DesignBase64Migration
| (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0)
| (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0)
| (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0));
save.Customize.Write((nint)data + 4);
save.Customize.Write(data + 4);
((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand));
((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand));
foreach (var slot in EquipSlotExtensions.EqdpSlots)

View file

@ -2,10 +2,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -165,7 +163,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
yield return (slot, item, armor.Stain);
}
var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant, FullEquipType.Unknown);
var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant);
if (!mh.Valid)
{
Glamourer.Log.Warning($"Appearance data {mainhand} for mainhand weapon invalid, item could not be identified.");

View file

@ -1,8 +1,6 @@
using System;
using System.Runtime.CompilerServices;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -12,30 +10,30 @@ namespace Glamourer.Designs;
public unsafe struct DesignData
{
private string _nameHead = string.Empty;
private string _nameBody = string.Empty;
private string _nameHands = string.Empty;
private string _nameLegs = string.Empty;
private string _nameFeet = string.Empty;
private string _nameEars = string.Empty;
private string _nameNeck = string.Empty;
private string _nameWrists = string.Empty;
private string _nameRFinger = string.Empty;
private string _nameLFinger = string.Empty;
private string _nameMainhand = string.Empty;
private string _nameOffhand = string.Empty;
private fixed uint _itemIds[12];
private fixed ushort _iconIds[12];
private fixed byte _equipmentBytes[48];
public Customize Customize = Customize.Default;
public uint ModelId;
public CrestFlag CrestVisibility;
private SecondaryId _secondaryMainhand;
private SecondaryId _secondaryOffhand;
private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand;
private byte _states;
public bool IsHuman = true;
private string _nameHead = string.Empty;
private string _nameBody = string.Empty;
private string _nameHands = string.Empty;
private string _nameLegs = string.Empty;
private string _nameFeet = string.Empty;
private string _nameEars = string.Empty;
private string _nameNeck = string.Empty;
private string _nameWrists = string.Empty;
private string _nameRFinger = string.Empty;
private string _nameLFinger = string.Empty;
private string _nameMainhand = string.Empty;
private string _nameOffhand = string.Empty;
private fixed uint _itemIds[12];
private fixed ushort _iconIds[12];
private fixed byte _equipmentBytes[48];
public CustomizeArray Customize = CustomizeArray.Default;
public uint ModelId;
public CrestFlag CrestVisibility;
private SecondaryId _secondaryMainhand;
private SecondaryId _secondaryOffhand;
private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand;
private byte _states;
public bool IsHuman = true;
public DesignData()
{ }
@ -255,11 +253,11 @@ public unsafe struct DesignData
}
public bool LoadNonHuman(uint modelId, Customize customize, nint equipData)
public bool LoadNonHuman(uint modelId, CustomizeArray customize, nint equipData)
{
ModelId = modelId;
IsHuman = false;
Customize.Load(customize);
Customize.Read(customize.Data);
fixed (byte* ptr = _equipmentBytes)
{
MemoryUtility.MemCpyUnchecked(ptr, (byte*)equipData, 40);
@ -294,7 +292,7 @@ public unsafe struct DesignData
public readonly byte[] GetCustomizeBytes()
{
var ret = new byte[CustomizeArray.Size];
fixed (byte* retPtr = ret, inPtr = Customize.Data.Data)
fixed (byte* retPtr = ret, inPtr = Customize.Data)
{
MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length);
}

View file

@ -3,12 +3,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Utility;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
@ -25,8 +23,8 @@ public class DesignManager
private readonly HumanModelList _humans;
private readonly SaveService _saveService;
private readonly DesignChanged _event;
private readonly List<Design> _designs = new();
private readonly Dictionary<Guid, DesignData> _undoStore = new();
private readonly List<Design> _designs = [];
private readonly Dictionary<Guid, DesignData> _undoStore = [];
public IReadOnlyList<Design> Designs
=> _designs;
@ -298,7 +296,7 @@ public class DesignManager
return;
case CustomizeIndex.Clan:
{
var customize = new Customize(design.DesignData.Customize.Data.Clone());
var customize = design.DesignData.Customize;
if (_customizations.ChangeClan(ref customize, (SubRace)value.Value) == 0)
return;
if (!design.SetCustomize(_customizations, customize))
@ -308,7 +306,7 @@ public class DesignManager
}
case CustomizeIndex.Gender:
{
var customize = new Customize(design.DesignData.Customize.Data.Clone());
var customize = design.DesignData.Customize;
if (_customizations.ChangeGender(ref customize, (Gender)(value.Value + 1)) == 0)
return;
if (!design.SetCustomize(_customizations, customize))

View file

@ -0,0 +1,128 @@
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.GameData;
/// <summary>
/// A custom version of CharaMakeParams that is easier to parse.
/// </summary>
[Sheet("CharaMakeParams")]
public class CharaMakeParams : ExcelRow
{
public const int NumMenus = 28;
public const int NumVoices = 12;
public const int NumGraphics = 10;
public const int MaxNumValues = 100;
public const int NumFaces = 8;
public const int NumFeatures = 7;
public const int NumEquip = 3;
public enum MenuType
{
ListSelector = 0,
IconSelector = 1,
ColorPicker = 2,
DoubleColorPicker = 3,
IconCheckmark = 4,
Percentage = 5,
Checkmark = 6, // custom
Nothing = 7, // custom
List1Selector = 8, // custom, 1-indexed lists
}
public struct Menu
{
public uint Id;
public byte InitVal;
public MenuType Type;
public byte Size;
public byte LookAt;
public uint Mask;
public uint Customize;
public uint[] Values;
public byte[] Graphic;
}
public struct FacialFeatures
{
public uint[] Icons;
}
public LazyRow<Race> Race { get; set; } = null!;
public LazyRow<Tribe> Tribe { get; set; } = null!;
public sbyte Gender { get; set; }
public Menu[] Menus { get; set; } = new Menu[NumMenus];
public byte[] Voices { get; set; } = new byte[NumVoices];
public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces];
public CharaMakeType.CharaMakeTypeUnkData3347Obj[] Equip { get; set; } = new CharaMakeType.CharaMakeTypeUnkData3347Obj[NumEquip];
public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language)
{
RowId = parser.RowId;
SubRowId = parser.SubRowId;
Race = new LazyRow<Race>(gameData, parser.ReadColumn<uint>(0), language);
Tribe = new LazyRow<Tribe>(gameData, parser.ReadColumn<uint>(1), language);
Gender = parser.ReadColumn<sbyte>(2);
int currentOffset;
for (var i = 0; i < NumMenus; ++i)
{
currentOffset = 3 + i;
Menus[i].Id = parser.ReadColumn<uint>(0 * NumMenus + currentOffset);
Menus[i].InitVal = parser.ReadColumn<byte>(1 * NumMenus + currentOffset);
Menus[i].Type = (MenuType)parser.ReadColumn<byte>(2 * NumMenus + currentOffset);
Menus[i].Size = parser.ReadColumn<byte>(3 * NumMenus + currentOffset);
Menus[i].LookAt = parser.ReadColumn<byte>(4 * NumMenus + currentOffset);
Menus[i].Mask = parser.ReadColumn<uint>(5 * NumMenus + currentOffset);
Menus[i].Customize = parser.ReadColumn<uint>(6 * NumMenus + currentOffset);
Menus[i].Values = new uint[Menus[i].Size];
switch (Menus[i].Type)
{
case MenuType.ColorPicker:
case MenuType.DoubleColorPicker:
case MenuType.Percentage:
break;
default:
currentOffset += 7 * NumMenus;
for (var j = 0; j < Menus[i].Size; ++j)
Menus[i].Values[j] = parser.ReadColumn<uint>(j * NumMenus + currentOffset);
break;
}
Menus[i].Graphic = new byte[NumGraphics];
currentOffset = 3 + (MaxNumValues + 7) * NumMenus + i;
for (var j = 0; j < NumGraphics; ++j)
Menus[i].Graphic[j] = parser.ReadColumn<byte>(j * NumMenus + currentOffset);
}
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus;
for (var i = 0; i < NumVoices; ++i)
Voices[i] = parser.ReadColumn<byte>(currentOffset++);
for (var i = 0; i < NumFaces; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + i;
FacialFeatureByFace[i].Icons = new uint[NumFeatures];
for (var j = 0; j < NumFeatures; ++j)
FacialFeatureByFace[i].Icons[j] = (uint)parser.ReadColumn<int>(j * NumFaces + currentOffset);
}
for (var i = 0; i < NumEquip; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7;
Equip[i] = new CharaMakeType.CharaMakeTypeUnkData3347Obj()
{
Helmet = parser.ReadColumn<ulong>(currentOffset + 0),
Top = parser.ReadColumn<ulong>(currentOffset + 1),
Gloves = parser.ReadColumn<ulong>(currentOffset + 2),
Legs = parser.ReadColumn<ulong>(currentOffset + 3),
Shoes = parser.ReadColumn<ulong>(currentOffset + 4),
Weapon = parser.ReadColumn<ulong>(currentOffset + 5),
SubWeapon = parser.ReadColumn<ulong>(currentOffset + 6),
};
}
}
}

View file

@ -0,0 +1,50 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Dalamud.Plugin.Services;
using Penumbra.String.Functions;
namespace Glamourer.GameData;
public class ColorParameters : IReadOnlyList<uint>
{
private readonly uint[] _rgbaColors;
public ReadOnlySpan<uint> GetSlice(int offset, int count)
=> _rgbaColors.AsSpan(offset, count);
public unsafe ColorParameters(IDataManager gameData, IPluginLog log)
{
try
{
var file = gameData.GetFile("chara/xls/charamake/human.cmp")!;
_rgbaColors = new uint[file.Data.Length >> 2];
fixed (byte* ptr1 = file.Data)
{
fixed (uint* ptr2 = _rgbaColors)
{
MemoryUtility.MemCpyUnchecked(ptr2, ptr1, file.Data.Length);
}
}
}
catch (Exception e)
{
log.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n"
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
_rgbaColors = Array.Empty<uint>();
}
}
public IEnumerator<uint> GetEnumerator()
=> (IEnumerator<uint>)_rgbaColors.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _rgbaColors.Length;
public uint this[int index]
=> _rgbaColors[index];
}

View file

@ -0,0 +1,38 @@
namespace Glamourer.GameData;
/// <summary> For localization from the game files directly. </summary>
public enum CustomName
{
MidlanderM,
HighlanderM,
WildwoodM,
DuskwightM,
PlainsfolkM,
DunesfolkM,
SeekerOfTheSunM,
KeeperOfTheMoonM,
SeawolfM,
HellsguardM,
RaenM,
XaelaM,
HelionM,
LostM,
RavaM,
VeenaM,
MidlanderF,
HighlanderF,
WildwoodF,
DuskwightF,
PlainsfolkF,
DunesfolkF,
SeekerOfTheSunF,
KeeperOfTheMoonF,
SeawolfF,
HellsguardF,
RaenF,
XaelaF,
HelionF,
LostF,
RavaF,
VeenaF,
}

View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
using Penumbra.GameData.Enums;
namespace Glamourer.GameData;
public class CustomizationManager : ICustomizationManager
{
private static CustomizationOptions? _options;
private CustomizationManager()
{ }
public static ICustomizationManager Create(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet)
{
_options ??= new CustomizationOptions(textures, gameData, log, npcCustomizeSet);
return new CustomizationManager();
}
public IReadOnlyList<Race> Races
=> CustomizationOptions.Races;
public IReadOnlyList<SubRace> Clans
=> CustomizationOptions.Clans;
public IReadOnlyList<Gender> Genders
=> CustomizationOptions.Genders;
public CustomizationSet GetList(SubRace clan, Gender gender)
=> _options!.GetList(clan, gender);
public IDalamudTextureWrap GetIcon(uint iconId)
=> _options!.GetIcon(iconId);
public string GetName(CustomName name)
=> _options!.GetName(name);
}

View file

@ -0,0 +1,56 @@
using Penumbra.GameData.Enums;
using System.Collections.Generic;
using System.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
public static class CustomizationNpcOptions
{
public static Dictionary<(SubRace, Gender), IReadOnlyList<(CustomizeIndex, CustomizeValue)>> CreateNpcData(CustomizationSet[] sets,
NpcCustomizeSet npcCustomizeSet)
{
var dict = new Dictionary<(SubRace, Gender), HashSet<(CustomizeIndex, CustomizeValue)>>();
var customizeIndices = new[]
{
CustomizeIndex.Face,
CustomizeIndex.Hairstyle,
CustomizeIndex.LipColor,
CustomizeIndex.SkinColor,
CustomizeIndex.FacePaintColor,
CustomizeIndex.HighlightsColor,
CustomizeIndex.HairColor,
CustomizeIndex.FacePaint,
CustomizeIndex.TattooColor,
CustomizeIndex.EyeColorLeft,
CustomizeIndex.EyeColorRight,
};
foreach (var customize in npcCustomizeSet.Select(s => s.Customize))
{
var set = sets[CustomizationOptions.ToIndex(customize.Clan, customize.Gender)];
foreach (var customizeIndex in customizeIndices)
{
var value = customize[customizeIndex];
if (value == CustomizeValue.Zero)
continue;
if (set.DataByValue(customizeIndex, value, out _, customize.Face) >= 0)
continue;
if (!dict.TryGetValue((set.Clan, set.Gender), out var npcSet))
{
npcSet = [(customizeIndex, value)];
dict.Add((set.Clan, set.Gender), npcSet);
}
else
{
npcSet.Add((customizeIndex, value));
}
}
}
return dict.ToDictionary(kvp => kvp.Key,
kvp => (IReadOnlyList<(CustomizeIndex, CustomizeValue)>)kvp.Value.OrderBy(p => p.Item1).ThenBy(p => p.Item2.Value).ToArray());
}
}

View file

@ -0,0 +1,530 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.GameData;
// Generate everything about customization per tribe and gender.
public partial class CustomizationOptions
{
// All races except for Unknown
internal static readonly Race[] Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
// All tribes except for Unknown
internal static readonly SubRace[] Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
// Two genders.
internal static readonly Gender[] Genders =
{
Gender.Male,
Gender.Female,
};
// Every tribe and gender has a separate set of available customizations.
internal CustomizationSet GetList(SubRace race, Gender gender)
=> _customizationSets[ToIndex(race, gender)];
// Get specific icons.
internal IDalamudTextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id)!;
private readonly IconStorage _icons;
private static readonly int ListSize = Clans.Length * Genders.Length;
private readonly CustomizationSet[] _customizationSets = new CustomizationSet[ListSize];
// Get the index for the given pair of tribe and gender.
internal static int ToIndex(SubRace race, Gender gender)
{
var idx = ((int)race - 1) * Genders.Length + (gender == Gender.Female ? 1 : 0);
if (idx < 0 || idx >= ListSize)
ThrowException(race, gender);
return idx;
}
private static void ThrowException(SubRace race, Gender gender)
=> throw new Exception($"Invalid customization requested for {race} {gender}.");
}
public partial class CustomizationOptions
{
public string GetName(CustomName name)
=> _names[(int)name];
internal CustomizationOptions(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet)
{
var tmp = new TemporaryData(gameData, this, log);
_icons = new IconStorage(textures, gameData);
SetNames(gameData);
foreach (var race in Clans)
{
foreach (var gender in Genders)
_customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender);
}
tmp.SetNpcData(_customizationSets, npcCustomizeSet);
}
// Obtain localized names of customization options and race names from the game data.
private readonly string[] _names = new string[Enum.GetValues<CustomName>().Length];
private void SetNames(IDataManager gameData)
{
var subRace = gameData.GetExcelSheet<Tribe>()!;
void Set(CustomName id, Lumina.Text.SeString? s, string def)
=> _names[(int)id] = s?.ToDalamudString().TextValue ?? def;
Set(CustomName.MidlanderM, subRace.GetRow((int)SubRace.Midlander)?.Masculine, SubRace.Midlander.ToName());
Set(CustomName.MidlanderF, subRace.GetRow((int)SubRace.Midlander)?.Feminine, SubRace.Midlander.ToName());
Set(CustomName.HighlanderM, subRace.GetRow((int)SubRace.Highlander)?.Masculine, SubRace.Highlander.ToName());
Set(CustomName.HighlanderF, subRace.GetRow((int)SubRace.Highlander)?.Feminine, SubRace.Highlander.ToName());
Set(CustomName.WildwoodM, subRace.GetRow((int)SubRace.Wildwood)?.Masculine, SubRace.Wildwood.ToName());
Set(CustomName.WildwoodF, subRace.GetRow((int)SubRace.Wildwood)?.Feminine, SubRace.Wildwood.ToName());
Set(CustomName.DuskwightM, subRace.GetRow((int)SubRace.Duskwight)?.Masculine, SubRace.Duskwight.ToName());
Set(CustomName.DuskwightF, subRace.GetRow((int)SubRace.Duskwight)?.Feminine, SubRace.Duskwight.ToName());
Set(CustomName.PlainsfolkM, subRace.GetRow((int)SubRace.Plainsfolk)?.Masculine, SubRace.Plainsfolk.ToName());
Set(CustomName.PlainsfolkF, subRace.GetRow((int)SubRace.Plainsfolk)?.Feminine, SubRace.Plainsfolk.ToName());
Set(CustomName.DunesfolkM, subRace.GetRow((int)SubRace.Dunesfolk)?.Masculine, SubRace.Dunesfolk.ToName());
Set(CustomName.DunesfolkF, subRace.GetRow((int)SubRace.Dunesfolk)?.Feminine, SubRace.Dunesfolk.ToName());
Set(CustomName.SeekerOfTheSunM, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Masculine, SubRace.SeekerOfTheSun.ToName());
Set(CustomName.SeekerOfTheSunF, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Feminine, SubRace.SeekerOfTheSun.ToName());
Set(CustomName.KeeperOfTheMoonM, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Masculine, SubRace.KeeperOfTheMoon.ToName());
Set(CustomName.KeeperOfTheMoonF, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Feminine, SubRace.KeeperOfTheMoon.ToName());
Set(CustomName.SeawolfM, subRace.GetRow((int)SubRace.Seawolf)?.Masculine, SubRace.Seawolf.ToName());
Set(CustomName.SeawolfF, subRace.GetRow((int)SubRace.Seawolf)?.Feminine, SubRace.Seawolf.ToName());
Set(CustomName.HellsguardM, subRace.GetRow((int)SubRace.Hellsguard)?.Masculine, SubRace.Hellsguard.ToName());
Set(CustomName.HellsguardF, subRace.GetRow((int)SubRace.Hellsguard)?.Feminine, SubRace.Hellsguard.ToName());
Set(CustomName.RaenM, subRace.GetRow((int)SubRace.Raen)?.Masculine, SubRace.Raen.ToName());
Set(CustomName.RaenF, subRace.GetRow((int)SubRace.Raen)?.Feminine, SubRace.Raen.ToName());
Set(CustomName.XaelaM, subRace.GetRow((int)SubRace.Xaela)?.Masculine, SubRace.Xaela.ToName());
Set(CustomName.XaelaF, subRace.GetRow((int)SubRace.Xaela)?.Feminine, SubRace.Xaela.ToName());
Set(CustomName.HelionM, subRace.GetRow((int)SubRace.Helion)?.Masculine, SubRace.Helion.ToName());
Set(CustomName.HelionF, subRace.GetRow((int)SubRace.Helion)?.Feminine, SubRace.Helion.ToName());
Set(CustomName.LostM, subRace.GetRow((int)SubRace.Lost)?.Masculine, SubRace.Lost.ToName());
Set(CustomName.LostF, subRace.GetRow((int)SubRace.Lost)?.Feminine, SubRace.Lost.ToName());
Set(CustomName.RavaM, subRace.GetRow((int)SubRace.Rava)?.Masculine, SubRace.Rava.ToName());
Set(CustomName.RavaF, subRace.GetRow((int)SubRace.Rava)?.Feminine, SubRace.Rava.ToName());
Set(CustomName.VeenaM, subRace.GetRow((int)SubRace.Veena)?.Masculine, SubRace.Veena.ToName());
Set(CustomName.VeenaF, subRace.GetRow((int)SubRace.Veena)?.Feminine, SubRace.Veena.ToName());
}
private class TemporaryData
{
public CustomizationSet GetSet(SubRace race, Gender gender)
{
var (skin, hair) = GetColors(race, gender);
var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var hrothgar = race.ToRace() == Race.Hrothgar;
// Create the initial set with all the easily accessible parameters available for anyone.
var set = new CustomizationSet(race, gender)
{
Voices = row.Voices,
HairStyles = GetHairStyles(race, gender),
HairColors = hair,
SkinColors = skin,
EyeColors = _eyeColorPicker,
HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker,
LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = hrothgar ? Array.Empty<CustomizeData>() : _lipColorPickerLight,
FacePaintColorsDark = _facePaintColorPickerDark,
FacePaintColorsLight = _facePaintColorPickerLight,
Faces = GetFaces(row),
NumEyebrows = GetListSize(row, CustomizeIndex.Eyebrows),
NumEyeShapes = GetListSize(row, CustomizeIndex.EyeShape),
NumNoseShapes = GetListSize(row, CustomizeIndex.Nose),
NumJawShapes = GetListSize(row, CustomizeIndex.Jaw),
NumMouthShapes = GetListSize(row, CustomizeIndex.Mouth),
FacePaints = GetFacePaints(race, gender),
TailEarShapes = GetTailEarShapes(row),
};
SetAvailability(set, row);
SetFacialFeatures(set, row);
SetHairByFace(set);
SetMenuTypes(set, row);
SetNames(set, row);
return set;
}
public void SetNpcData(CustomizationSet[] sets, NpcCustomizeSet npcCustomizeSet)
{
var data = CustomizationNpcOptions.CreateNpcData(sets, npcCustomizeSet);
foreach (var set in sets)
{
if (data.TryGetValue((set.Clan, set.Gender), out var npcData))
set.NpcOptions = npcData.ToArray();
}
}
public TemporaryData(IDataManager gameData, CustomizationOptions options, IPluginLog log)
{
_options = options;
_cmpFile = new ColorParameters(gameData, log);
_customizeSheet = gameData.GetExcelSheet<CharaMakeCustomize>(ClientLanguage.English)!;
Lobby = gameData.GetExcelSheet<Lobby>(ClientLanguage.English)!;
var tmp = gameData.Excel.GetType().GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
.MakeGenericMethod(typeof(CharaMakeParams)).Invoke(gameData.Excel, new object?[]
{
"charamaketype",
gameData.Language.ToLumina(),
null,
}) as ExcelSheet<CharaMakeParams>;
_listSheet = tmp!;
_hairSheet = gameData.GetExcelSheet<HairMakeType>()!;
_highlightPicker = CreateColorPicker(CustomizeIndex.HighlightsColor, 256, 192);
_lipColorPickerDark = CreateColorPicker(CustomizeIndex.LipColor, 512, 96);
_lipColorPickerLight = CreateColorPicker(CustomizeIndex.LipColor, 1024, 96, true);
_eyeColorPicker = CreateColorPicker(CustomizeIndex.EyeColorLeft, 0, 192);
_facePaintColorPickerDark = CreateColorPicker(CustomizeIndex.FacePaintColor, 640, 96);
_facePaintColorPickerLight = CreateColorPicker(CustomizeIndex.FacePaintColor, 1152, 96, true);
_tattooColorPicker = CreateColorPicker(CustomizeIndex.TattooColor, 0, 192);
}
// Required sheets.
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
private readonly ExcelSheet<CharaMakeParams> _listSheet;
private readonly ExcelSheet<HairMakeType> _hairSheet;
public readonly ExcelSheet<Lobby> Lobby;
private readonly ColorParameters _cmpFile;
// Those values are shared between all races.
private readonly CustomizeData[] _highlightPicker;
private readonly CustomizeData[] _eyeColorPicker;
private readonly CustomizeData[] _facePaintColorPickerDark;
private readonly CustomizeData[] _facePaintColorPickerLight;
private readonly CustomizeData[] _lipColorPickerDark;
private readonly CustomizeData[] _lipColorPickerLight;
private readonly CustomizeData[] _tattooColorPicker;
private readonly CustomizationOptions _options;
private CustomizeData[] CreateColorPicker(CustomizeIndex index, int offset, int num, bool light = false)
{
var ret = new CustomizeData[num];
var idx = 0;
foreach (var value in _cmpFile.GetSlice(offset, num))
{
ret[idx] = new CustomizeData(index, (CustomizeValue)(light ? 128 + idx : idx), value, (ushort)(offset + idx));
++idx;
}
return ret;
}
private void SetHairByFace(CustomizationSet set)
{
if (set.Race != Race.Hrothgar)
{
set.HairByFace = Enumerable.Repeat(set.HairStyles, set.Faces.Count + 1).ToArray();
return;
}
var tmp = new IReadOnlyList<CustomizeData>[set.Faces.Count + 1];
tmp[0] = set.HairStyles;
for (var i = 1; i <= set.Faces.Count; ++i)
{
bool Valid(CustomizeData c)
{
var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0;
return data == 0 || data == i + set.Faces.Count;
}
tmp[i] = set.HairStyles.Where(Valid).ToArray();
}
set.HairByFace = tmp;
}
private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row)
{
// Set up the menu types for all customizations.
set.Types = Enum.GetValues<CustomizeIndex>().Select(c =>
{
// Those types are not correctly given in the menu, so special case them to color pickers.
switch (c)
{
case CustomizeIndex.HighlightsColor:
case CustomizeIndex.EyeColorLeft:
case CustomizeIndex.EyeColorRight:
case CustomizeIndex.FacePaintColor:
return CharaMakeParams.MenuType.ColorPicker;
case CustomizeIndex.BodyType: return CharaMakeParams.MenuType.Nothing;
case CustomizeIndex.FacePaintReversed:
case CustomizeIndex.Highlights:
case CustomizeIndex.SmallIris:
case CustomizeIndex.Lipstick:
return CharaMakeParams.MenuType.Checkmark;
case CustomizeIndex.FacialFeature1:
case CustomizeIndex.FacialFeature2:
case CustomizeIndex.FacialFeature3:
case CustomizeIndex.FacialFeature4:
case CustomizeIndex.FacialFeature5:
case CustomizeIndex.FacialFeature6:
case CustomizeIndex.FacialFeature7:
case CustomizeIndex.LegacyTattoo:
return CharaMakeParams.MenuType.IconCheckmark;
}
var gameId = c.ToByteAndMask().ByteIdx;
// Otherwise find the first menu corresponding to the id.
// If there is none, assume a list.
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == gameId);
var ret = menu?.Type ?? CharaMakeParams.MenuType.ListSelector;
if (c is CustomizeIndex.TailShape && ret is CharaMakeParams.MenuType.ListSelector)
ret = CharaMakeParams.MenuType.List1Selector;
return ret;
}).ToArray();
set.Order = CustomizationSet.ComputeOrder(set);
}
// Set customizations available if they have any options.
private static void SetAvailability(CustomizationSet set, CharaMakeParams row)
{
if (set is { Race: Race.Hrothgar, Gender: Gender.Female })
return;
Set(true, CustomizeIndex.Height);
Set(set.Faces.Count > 0, CustomizeIndex.Face);
Set(true, CustomizeIndex.Hairstyle);
Set(true, CustomizeIndex.Highlights);
Set(true, CustomizeIndex.SkinColor);
Set(true, CustomizeIndex.EyeColorRight);
Set(true, CustomizeIndex.HairColor);
Set(true, CustomizeIndex.HighlightsColor);
Set(true, CustomizeIndex.TattooColor);
Set(set.NumEyebrows > 0, CustomizeIndex.Eyebrows);
Set(true, CustomizeIndex.EyeColorLeft);
Set(set.NumEyeShapes > 0, CustomizeIndex.EyeShape);
Set(set.NumNoseShapes > 0, CustomizeIndex.Nose);
Set(set.NumJawShapes > 0, CustomizeIndex.Jaw);
Set(set.NumMouthShapes > 0, CustomizeIndex.Mouth);
Set(set.LipColorsDark.Count > 0, CustomizeIndex.LipColor);
Set(GetListSize(row, CustomizeIndex.MuscleMass) > 0, CustomizeIndex.MuscleMass);
Set(set.TailEarShapes.Count > 0, CustomizeIndex.TailShape);
Set(GetListSize(row, CustomizeIndex.BustSize) > 0, CustomizeIndex.BustSize);
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaint);
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintColor);
Set(true, CustomizeIndex.FacialFeature1);
Set(true, CustomizeIndex.FacialFeature2);
Set(true, CustomizeIndex.FacialFeature3);
Set(true, CustomizeIndex.FacialFeature4);
Set(true, CustomizeIndex.FacialFeature5);
Set(true, CustomizeIndex.FacialFeature6);
Set(true, CustomizeIndex.FacialFeature7);
Set(true, CustomizeIndex.LegacyTattoo);
Set(true, CustomizeIndex.SmallIris);
Set(set.Race != Race.Hrothgar, CustomizeIndex.Lipstick);
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintReversed);
return;
void Set(bool available, CustomizeIndex flag)
{
if (available)
set.SetAvailable(flag);
}
}
// Create a list of lists of facial features and the legacy tattoo.
private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row)
{
var count = set.Faces.Count;
set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count);
set.LegacyTattoo = Create(CustomizeIndex.LegacyTattoo, 137905);
var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray();
for (var i = 0; i < count; ++i)
{
var data = row.FacialFeatureByFace[i].Icons;
tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, data[0]);
tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, data[1]);
tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, data[2]);
tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, data[3]);
tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, data[4]);
tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, data[5]);
tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, data[6]);
}
set.FacialFeature1 = tmp[0];
set.FacialFeature2 = tmp[1];
set.FacialFeature3 = tmp[2];
set.FacialFeature4 = tmp[3];
set.FacialFeature5 = tmp[4];
set.FacialFeature6 = tmp[5];
set.FacialFeature7 = tmp[6];
return;
static (CustomizeData, CustomizeData) Create(CustomizeIndex i, uint data)
=> (new CustomizeData(i, CustomizeValue.Zero, data), new CustomizeData(i, CustomizeValue.Max, data, 1));
}
// Set the names for the given set of parameters.
private void SetNames(CustomizationSet set, CharaMakeParams row)
{
var nameArray = Enum.GetValues<CustomizeIndex>().Select(c =>
{
// Find the first menu that corresponds to the Id.
var byteId = c.ToByteAndMask().ByteIdx;
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == byteId);
if (menu == null)
{
// If none exists and the id corresponds to highlights, set the Highlights name.
if (c == CustomizeIndex.Highlights)
return Lobby.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights";
// Otherwise there is an error and we use the default name.
return c.ToDefaultName();
}
// Otherwise all is normal, get the menu name or if it does not work the default name.
var textRow = Lobby.GetRow(menu.Value.Id);
return textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName();
}).ToArray();
// Add names for both eye colors.
nameArray[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorLeft.ToDefaultName();
nameArray[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.EyeColorRight.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature1] = CustomizeIndex.FacialFeature1.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature2] = CustomizeIndex.FacialFeature2.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature3] = CustomizeIndex.FacialFeature3.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature4] = CustomizeIndex.FacialFeature4.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature5] = CustomizeIndex.FacialFeature5.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature6] = CustomizeIndex.FacialFeature6.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature7] = CustomizeIndex.FacialFeature7.ToDefaultName();
nameArray[(int)CustomizeIndex.LegacyTattoo] = CustomizeIndex.LegacyTattoo.ToDefaultName();
nameArray[(int)CustomizeIndex.SmallIris] = CustomizeIndex.SmallIris.ToDefaultName();
nameArray[(int)CustomizeIndex.Lipstick] = CustomizeIndex.Lipstick.ToDefaultName();
nameArray[(int)CustomizeIndex.FacePaintReversed] = CustomizeIndex.FacePaintReversed.ToDefaultName();
set.OptionName = nameArray;
}
// Obtain available skin and hair colors for the given subrace and gender.
private (CustomizeData[], CustomizeData[]) GetColors(SubRace race, Gender gender)
{
if (race is > SubRace.Veena or SubRace.Unknown)
throw new ArgumentOutOfRangeException(nameof(race), race, null);
var gv = gender == Gender.Male ? 0 : 1;
var idx = ((int)race * 2 + gv) * 5 + 3;
return (CreateColorPicker(CustomizeIndex.SkinColor, idx << 8, 192),
CreateColorPicker(CustomizeIndex.HairColor, (idx + 1) << 8, 192));
}
// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender.
private CustomizeData[] GetHairStyles(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
// Unknown30 is the number of available hairstyles.
var hairList = new List<CustomizeData>(row.Unknown30);
// Hairstyles can be found starting at Unknown66.
for (var i = 0; i < row.Unknown30; ++i)
{
var name = $"Unknown{66 + i * 9}";
var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
// Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx);
if (hairRow == null)
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx));
else if (_options._icons.IconExists(hairRow.Icon))
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon,
(ushort)hairRow.RowId));
}
return hairList.OrderBy(h => h.Value.Value).ToArray();
}
// Get Features.
private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new CustomizeData(id, (CustomizeValue)(index + 1), value)
: new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId);
}
// Get List sizes.
private static int GetListSize(CharaMakeParams row, CustomizeIndex index)
{
var gameId = index.ToByteAndMask().ByteIdx;
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == gameId);
return menu?.Size ?? 0;
}
// Get face paints from the hair sheet via reflection.
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<CustomizeData>(row.Unknown37);
// Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i)
{
// Face paints start at Unknown73.
var name = $"Unknown{73 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
if (paintRow != null)
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
(ushort)paintRow.RowId));
else
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
}
return paintList.OrderBy(p => p.Value.Value).ToArray();
}
// Specific icons for tails or ears.
private CustomizeData[] GetTailEarShapes(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray()
?? Array.Empty<CustomizeData>();
// Specific icons for faces.
private CustomizeData[] GetFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx)
?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray()
?? Array.Empty<CustomizeData>();
// Specific icons for Hrothgar patterns.
private CustomizeData[] HrothgarFurPattern(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray()
?? Array.Empty<CustomizeData>();
}
}

View file

@ -0,0 +1,310 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
// Each Subrace and Gender combo has a customization set.
// This describes the available customizations, their types and their names.
public class CustomizationSet
{
internal CustomizationSet(SubRace clan, Gender gender)
{
Gender = gender;
Clan = clan;
Race = clan.ToRace();
SettingAvailable = 0;
}
public Gender Gender { get; }
public SubRace Clan { get; }
public Race Race { get; }
public CustomizeFlag SettingAvailable { get; internal set; }
internal void SetAvailable(CustomizeIndex index)
=> SettingAvailable |= index.ToFlag();
public bool IsAvailable(CustomizeIndex index)
=> SettingAvailable.HasFlag(index.ToFlag());
// Meta
public IReadOnlyList<string> OptionName { get; internal set; } = null!;
public string Option(CustomizeIndex index)
=> OptionName[(int)index];
public IReadOnlyList<byte> Voices { get; internal init; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizeIndex[]> Order { get; internal set; } = null!;
// Always list selector.
public int NumEyebrows { get; internal init; }
public int NumEyeShapes { get; internal init; }
public int NumNoseShapes { get; internal init; }
public int NumJawShapes { get; internal init; }
public int NumMouthShapes { get; internal init; }
// Always Icon Selector
public IReadOnlyList<CustomizeData> Faces { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> HairStyles { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<CustomizeData>> HairByFace { get; internal set; } = null!;
public IReadOnlyList<CustomizeData> TailEarShapes { get; internal init; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature1 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature2 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature3 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature4 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature5 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature6 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature7 { get; internal set; } = null!;
public (CustomizeData, CustomizeData) LegacyTattoo { get; internal set; }
public IReadOnlyList<CustomizeData> FacePaints { get; internal init; } = null!;
public IReadOnlyList<(CustomizeIndex Type, CustomizeValue Value)> NpcOptions { get; internal set; } =
Array.Empty<(CustomizeIndex Type, CustomizeValue Value)>();
// Always Color Selector
public IReadOnlyList<CustomizeData> SkinColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> HairColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> HighlightColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> EyeColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> TattooColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> FacePaintColorsLight { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> FacePaintColorsDark { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> LipColorsLight { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> LipColorsDark { get; internal init; } = null!;
public bool Validate(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face)
{
if (IsAvailable(index))
return DataByValue(index, value, out custom, face) >= 0
|| NpcOptions.Any(t => t.Type == index && t.Value == value);
custom = null;
return value == CustomizeValue.Zero;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int DataByValue(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face)
{
var type = Types[(int)index];
int GetInteger0(out CustomizeData? custom)
{
if (value < Count(index))
{
custom = new CustomizeData(index, value, 0, value.Value);
return value.Value;
}
custom = null;
return -1;
}
int GetInteger1(out CustomizeData? custom)
{
if (value > 0 && value < Count(index) + 1)
{
custom = new CustomizeData(index, value, 0, (ushort)(value.Value - 1));
return value.Value;
}
custom = null;
return -1;
}
static int GetBool(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom)
{
if (value == CustomizeValue.Zero)
{
custom = new CustomizeData(index, CustomizeValue.Zero, 0, 0);
return 0;
}
var (_, mask) = index.ToByteAndMask();
if (value.Value == mask)
{
custom = new CustomizeData(index, new CustomizeValue(mask), 0, 1);
return 1;
}
custom = null;
return -1;
}
static int Invalid(out CustomizeData? custom)
{
custom = null;
return -1;
}
int Get(IEnumerable<CustomizeData> list, CustomizeValue v, out CustomizeData? output)
{
var (val, idx) = list.Cast<CustomizeData?>().WithIndex().FirstOrDefault(p => p.Item1!.Value.Value == v);
if (val == null)
{
output = null;
return -1;
}
output = val;
return idx;
}
return type switch
{
CharaMakeParams.MenuType.ListSelector => GetInteger0(out custom),
CharaMakeParams.MenuType.List1Selector => GetInteger1(out custom),
CharaMakeParams.MenuType.IconSelector => index switch
{
CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom),
CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles,
value, out custom),
CustomizeIndex.TailShape => Get(TailEarShapes, value, out custom),
CustomizeIndex.FacePaint => Get(FacePaints, value, out custom),
CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.ColorPicker => index switch
{
CustomizeIndex.SkinColor => Get(SkinColors, value, out custom),
CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom),
CustomizeIndex.EyeColorRight => Get(EyeColors, value, out custom),
CustomizeIndex.HairColor => Get(HairColors, value, out custom),
CustomizeIndex.HighlightsColor => Get(HighlightColors, value, out custom),
CustomizeIndex.TattooColor => Get(TattooColors, value, out custom),
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.DoubleColorPicker => index switch
{
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.IconCheckmark => GetBool(index, value, out custom),
CharaMakeParams.MenuType.Percentage => GetInteger0(out custom),
CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom),
_ => Invalid(out custom),
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CustomizeData Data(CustomizeIndex index, int idx)
=> Data(index, idx, CustomizeValue.Zero);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CustomizeData Data(CustomizeIndex index, int idx, CustomizeValue face)
{
if (idx >= Count(index, face = HrothgarFaceHack(face)))
throw new IndexOutOfRangeException();
switch (Types[(int)index])
{
case CharaMakeParams.MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case CharaMakeParams.MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case CharaMakeParams.MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx);
case CharaMakeParams.MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx);
}
return index switch
{
CustomizeIndex.Face => Faces[idx],
CustomizeIndex.Hairstyle => face < HairByFace.Count ? HairByFace[face.Value][idx] : HairStyles[idx],
CustomizeIndex.TailShape => TailEarShapes[idx],
CustomizeIndex.FacePaint => FacePaints[idx],
CustomizeIndex.FacialFeature1 => idx == 0 ? FacialFeature1[face.Value].Item1 : FacialFeature1[face.Value].Item2,
CustomizeIndex.FacialFeature2 => idx == 0 ? FacialFeature2[face.Value].Item1 : FacialFeature2[face.Value].Item2,
CustomizeIndex.FacialFeature3 => idx == 0 ? FacialFeature3[face.Value].Item1 : FacialFeature3[face.Value].Item2,
CustomizeIndex.FacialFeature4 => idx == 0 ? FacialFeature4[face.Value].Item1 : FacialFeature4[face.Value].Item2,
CustomizeIndex.FacialFeature5 => idx == 0 ? FacialFeature5[face.Value].Item1 : FacialFeature5[face.Value].Item2,
CustomizeIndex.FacialFeature6 => idx == 0 ? FacialFeature6[face.Value].Item1 : FacialFeature6[face.Value].Item2,
CustomizeIndex.FacialFeature7 => idx == 0 ? FacialFeature7[face.Value].Item1 : FacialFeature7[face.Value].Item2,
CustomizeIndex.LegacyTattoo => idx == 0 ? LegacyTattoo.Item1 : LegacyTattoo.Item2,
CustomizeIndex.SkinColor => SkinColors[idx],
CustomizeIndex.EyeColorLeft => EyeColors[idx],
CustomizeIndex.EyeColorRight => EyeColors[idx],
CustomizeIndex.HairColor => HairColors[idx],
CustomizeIndex.HighlightsColor => HighlightColors[idx],
CustomizeIndex.TattooColor => TattooColors[idx],
CustomizeIndex.LipColor => idx < 96 ? LipColorsDark[idx] : LipColorsLight[idx - 96],
CustomizeIndex.FacePaintColor => idx < 96 ? FacePaintColorsDark[idx] : FacePaintColorsLight[idx - 96],
_ => new CustomizeData(0, CustomizeValue.Zero),
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CharaMakeParams.MenuType Type(CustomizeIndex index)
=> Types[(int)index];
internal static IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizeIndex[]> ComputeOrder(CustomizationSet set)
{
var ret = Enum.GetValues<CustomizeIndex>().ToArray();
ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft;
ret[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorRight;
ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor;
var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray());
foreach (var type in Enum.GetValues<CharaMakeParams.MenuType>())
dict.TryAdd(type, Array.Empty<CustomizeIndex>());
return dict;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int Count(CustomizeIndex index)
=> Count(index, CustomizeValue.Zero);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int Count(CustomizeIndex index, CustomizeValue face)
{
if (!IsAvailable(index))
return 0;
return Type(index) switch
{
CharaMakeParams.MenuType.Percentage => 101,
CharaMakeParams.MenuType.IconCheckmark => 2,
CharaMakeParams.MenuType.Checkmark => 2,
_ => index switch
{
CustomizeIndex.Face => Faces.Count,
CustomizeIndex.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count
? HairByFace[face.Value].Count
: HairStyles.Count,
CustomizeIndex.SkinColor => SkinColors.Count,
CustomizeIndex.EyeColorRight => EyeColors.Count,
CustomizeIndex.HairColor => HairColors.Count,
CustomizeIndex.HighlightsColor => HighlightColors.Count,
CustomizeIndex.TattooColor => TattooColors.Count,
CustomizeIndex.Eyebrows => NumEyebrows,
CustomizeIndex.EyeColorLeft => EyeColors.Count,
CustomizeIndex.EyeShape => NumEyeShapes,
CustomizeIndex.Nose => NumNoseShapes,
CustomizeIndex.Jaw => NumJawShapes,
CustomizeIndex.Mouth => NumMouthShapes,
CustomizeIndex.LipColor => LipColorsLight.Count + LipColorsDark.Count,
CustomizeIndex.TailShape => TailEarShapes.Count,
CustomizeIndex.FacePaint => FacePaints.Count,
CustomizeIndex.FacePaintColor => FacePaintColorsLight.Count + FacePaintColorsDark.Count,
_ => throw new ArgumentOutOfRangeException(nameof(index), index, null),
},
};
}
private CustomizeValue HrothgarFaceHack(CustomizeValue value)
=> Race == Race.Hrothgar && value.Value is > 4 and < 9 ? value - 4 : value;
}
public static class CustomizationSetExtensions
{
/// <summary> Return only the available customizations in this set and Clan or Gender. </summary>
public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizationSet set)
=> flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender);
}

View file

@ -0,0 +1,47 @@
using System;
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
// Any customization value can be represented in 8 bytes by its ID,
// a byte value, an optional value-id and an optional icon or color.
[StructLayout(LayoutKind.Explicit)]
public readonly struct CustomizeData : IEquatable<CustomizeData>
{
[FieldOffset(0)]
public readonly CustomizeIndex Index;
[FieldOffset(1)]
public readonly CustomizeValue Value;
[FieldOffset(2)]
public readonly ushort CustomizeId;
[FieldOffset(4)]
public readonly uint IconId;
[FieldOffset(4)]
public readonly uint Color;
public CustomizeData(CustomizeIndex index, CustomizeValue value, uint data = 0, ushort customizeId = 0)
{
Index = index;
Value = value;
IconId = data;
Color = data;
CustomizeId = customizeId;
}
public bool Equals(CustomizeData other)
=> Index == other.Index
&& Value.Value == other.Value.Value
&& CustomizeId == other.CustomizeId;
public override bool Equals(object? obj)
=> obj is CustomizeData other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)Index, Value.Value, CustomizeId);
}

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
using Dalamud.Interface.Internal;
using Penumbra.GameData.Enums;
namespace Glamourer.GameData;
public interface ICustomizationManager
{
public IReadOnlyList<Race> Races { get; }
public IReadOnlyList<SubRace> Clans { get; }
public IReadOnlyList<Gender> Genders { get; }
public CustomizationSet GetList(SubRace race, Gender gender);
public IDalamudTextureWrap GetIcon(uint iconId);
public string GetName(CustomName name);
}

View file

@ -0,0 +1,282 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Services;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
{
public string Name
=> nameof(NpcCustomizeSet);
private readonly List<NpcData> _data = [];
public long Time { get; private set; }
public long Memory { get; private set; }
public int TotalCount
=> _data.Count;
public Task Awaiter { get; }
public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{
var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter);
Awaiter = waitTask.ContinueWith(_ =>
{
var watch = Stopwatch.StartNew();
var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs));
var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames));
FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result);
Time = watch.ElapsedMilliseconds;
});
}
private static List<NpcData> CreateEnpcData(IDataManager data, DictENpc eNpcs)
{
var enpcSheet = data.GetExcelSheet<ENpcBase>()!;
var list = new List<NpcData>(eNpcs.Count);
foreach (var (id, name) in eNpcs)
{
var row = enpcSheet.GetRow(id.Id);
if (row == null || name.IsNullOrWhitespace())
continue;
var (valid, customize) = FromEnpcBase(row);
if (!valid)
continue;
var ret = new NpcData
{
Name = name,
Customize = customize,
Id = id,
Kind = ObjectKind.EventNpc,
};
if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip)
{
ApplyNpcEquip(ref ret, equip);
}
else
{
ret.Set(0, row.ModelHead | (row.DyeHead.Row << 24));
ret.Set(1, row.ModelBody | (row.DyeBody.Row << 24));
ret.Set(2, row.ModelHands | (row.DyeHands.Row << 24));
ret.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24));
ret.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24));
ret.Set(5, row.ModelEars | (row.DyeEars.Row << 24));
ret.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24));
ret.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24));
ret.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24));
ret.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24));
ret.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48));
ret.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48));
ret.VisorToggled = row.Visor;
}
list.Add(ret);
}
return list;
}
private static List<NpcData> CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{
var bnpcSheet = data.GetExcelSheet<BNpcBase>()!;
var list = new List<NpcData>((int)bnpcSheet.RowCount);
foreach (var baseRow in bnpcSheet)
{
if (baseRow.ModelChara.Value!.Type != 1)
continue;
var bnpcNameIds = bNpcNames[baseRow.RowId];
if (bnpcNameIds.Count == 0)
continue;
var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!);
if (!valid)
continue;
var equip = baseRow.NpcEquip.Value!;
var ret = new NpcData
{
Customize = customize,
Id = baseRow.RowId,
Kind = ObjectKind.BattleNpc,
};
ApplyNpcEquip(ref ret, equip);
foreach (var bnpcNameId in bnpcNameIds)
{
if (bNpcs.TryGetValue(bnpcNameId.Id, out var name) && !name.IsNullOrWhitespace())
list.Add(ret with { Name = name });
}
}
return list;
}
private void FilterAndOrderNpcData(List<NpcData> eNpcEquip, List<NpcData> bNpcEquip)
{
_data.Clear();
_data.EnsureCapacity(eNpcEquip.Count + bNpcEquip.Count);
var groups = eNpcEquip.Concat(bNpcEquip).GroupBy(d => d.Name).ToDictionary(g => g.Key, g => g.ToList());
foreach (var (name, duplicates) in groups.OrderBy(kvp => kvp.Key))
{
for (var i = 0; i < duplicates.Count; ++i)
{
var current = duplicates[i];
for (var j = 0; j < i; ++j)
{
if (current.DataEquals(duplicates[j]))
{
duplicates.RemoveAt(i--);
break;
}
}
}
if (duplicates.Count == 1)
{
_data.Add(duplicates[0]);
Memory += 96;
}
else
{
_data.AddRange(duplicates
.Select(duplicate => duplicate with
{
Name = $"{name} ({(duplicate.Kind is ObjectKind.BattleNpc ? 'B' : 'E')}{duplicate.Id})"
}));
Memory += 96 * duplicates.Count + duplicates.Sum(d => d.Name.Length * 2);
}
}
var lastWeird = _data.FindIndex(d => char.IsAsciiLetterOrDigit(d.Name[0]));
if (lastWeird != -1)
{
_data.AddRange(_data.Take(lastWeird));
_data.RemoveRange(0, lastWeird);
}
_data.TrimExcess();
}
private static void ApplyNpcEquip(ref NpcData data, NpcEquip row)
{
data.Set(0, row.ModelHead | (row.DyeHead.Row << 24));
data.Set(1, row.ModelBody | (row.DyeBody.Row << 24));
data.Set(2, row.ModelHands | (row.DyeHands.Row << 24));
data.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24));
data.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24));
data.Set(5, row.ModelEars | (row.DyeEars.Row << 24));
data.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24));
data.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24));
data.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24));
data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24));
data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48));
data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48));
data.VisorToggled = row.Visor;
}
private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize)
{
var customize = new CustomizeArray();
customize.SetByIndex(0, (CustomizeValue) (byte)bnpcCustomize.Race.Row);
customize.SetByIndex(1, (CustomizeValue) bnpcCustomize.Gender);
customize.SetByIndex(2, (CustomizeValue) bnpcCustomize.BodyType);
customize.SetByIndex(3, (CustomizeValue) bnpcCustomize.Height);
customize.SetByIndex(4, (CustomizeValue) (byte)bnpcCustomize.Tribe.Row);
customize.SetByIndex(5, (CustomizeValue) bnpcCustomize.Face);
customize.SetByIndex(6, (CustomizeValue) bnpcCustomize.HairStyle);
customize.SetByIndex(7, (CustomizeValue) bnpcCustomize.HairHighlight);
customize.SetByIndex(8, (CustomizeValue) bnpcCustomize.SkinColor);
customize.SetByIndex(9, (CustomizeValue) bnpcCustomize.EyeHeterochromia);
customize.SetByIndex(10, (CustomizeValue) bnpcCustomize.HairColor);
customize.SetByIndex(11, (CustomizeValue) bnpcCustomize.HairHighlightColor);
customize.SetByIndex(12, (CustomizeValue) bnpcCustomize.FacialFeature);
customize.SetByIndex(13, (CustomizeValue) bnpcCustomize.FacialFeatureColor);
customize.SetByIndex(14, (CustomizeValue) bnpcCustomize.Eyebrows);
customize.SetByIndex(15, (CustomizeValue) bnpcCustomize.EyeColor);
customize.SetByIndex(16, (CustomizeValue) bnpcCustomize.EyeShape);
customize.SetByIndex(17, (CustomizeValue) bnpcCustomize.Nose);
customize.SetByIndex(18, (CustomizeValue) bnpcCustomize.Jaw);
customize.SetByIndex(19, (CustomizeValue) bnpcCustomize.Mouth);
customize.SetByIndex(20, (CustomizeValue) bnpcCustomize.LipColor);
customize.SetByIndex(21, (CustomizeValue) bnpcCustomize.BustOrTone1);
customize.SetByIndex(22, (CustomizeValue) bnpcCustomize.ExtraFeature1);
customize.SetByIndex(23, (CustomizeValue) bnpcCustomize.ExtraFeature2OrBust);
customize.SetByIndex(24, (CustomizeValue) bnpcCustomize.FacePaint);
customize.SetByIndex(25, (CustomizeValue) bnpcCustomize.FacePaintColor);
if (customize.BodyType.Value != 1
|| !CustomizationOptions.Races.Contains(customize.Race)
|| !CustomizationOptions.Clans.Contains(customize.Clan)
|| !CustomizationOptions.Genders.Contains(customize.Gender))
return (false, CustomizeArray.Default);
return (true, customize);
}
private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase)
{
if (enpcBase.ModelChara.Value?.Type != 1)
return (false, CustomizeArray.Default);
var customize = new CustomizeArray();
customize.SetByIndex(0, (CustomizeValue) (byte)enpcBase.Race.Row);
customize.SetByIndex(1, (CustomizeValue) enpcBase.Gender);
customize.SetByIndex(2, (CustomizeValue) enpcBase.BodyType);
customize.SetByIndex(3, (CustomizeValue) enpcBase.Height);
customize.SetByIndex(4, (CustomizeValue) (byte)enpcBase.Tribe.Row);
customize.SetByIndex(5, (CustomizeValue) enpcBase.Face);
customize.SetByIndex(6, (CustomizeValue) enpcBase.HairStyle);
customize.SetByIndex(7, (CustomizeValue) enpcBase.HairHighlight);
customize.SetByIndex(8, (CustomizeValue) enpcBase.SkinColor);
customize.SetByIndex(9, (CustomizeValue) enpcBase.EyeHeterochromia);
customize.SetByIndex(10, (CustomizeValue) enpcBase.HairColor);
customize.SetByIndex(11, (CustomizeValue) enpcBase.HairHighlightColor);
customize.SetByIndex(12, (CustomizeValue) enpcBase.FacialFeature);
customize.SetByIndex(13, (CustomizeValue) enpcBase.FacialFeatureColor);
customize.SetByIndex(14, (CustomizeValue) enpcBase.Eyebrows);
customize.SetByIndex(15, (CustomizeValue) enpcBase.EyeColor);
customize.SetByIndex(16, (CustomizeValue) enpcBase.EyeShape);
customize.SetByIndex(17, (CustomizeValue) enpcBase.Nose);
customize.SetByIndex(18, (CustomizeValue) enpcBase.Jaw);
customize.SetByIndex(19, (CustomizeValue) enpcBase.Mouth);
customize.SetByIndex(20, (CustomizeValue) enpcBase.LipColor);
customize.SetByIndex(21, (CustomizeValue) enpcBase.BustOrTone1);
customize.SetByIndex(22, (CustomizeValue) enpcBase.ExtraFeature1);
customize.SetByIndex(23, (CustomizeValue) enpcBase.ExtraFeature2OrBust);
customize.SetByIndex(24, (CustomizeValue) enpcBase.FacePaint);
customize.SetByIndex(25, (CustomizeValue) enpcBase.FacePaintColor);
if (customize.BodyType.Value != 1
|| !CustomizationOptions.Races.Contains(customize.Race)
|| !CustomizationOptions.Clans.Contains(customize.Clan)
|| !CustomizationOptions.Genders.Contains(customize.Gender))
return (false, CustomizeArray.Default);
return (true, customize);
}
public IEnumerator<NpcData> GetEnumerator()
=> _data.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _data.Count;
public NpcData this[int index]
=> _data[index];
}

View file

@ -0,0 +1,89 @@
using System;
using System.Text;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
public unsafe struct NpcData
{
public string Name;
public CustomizeArray Customize;
private fixed byte _equip[40];
public CharacterWeapon Mainhand;
public CharacterWeapon Offhand;
public NpcId Id;
public bool VisorToggled;
public ObjectKind Kind;
public ReadOnlySpan<CharacterArmor> Equip
{
get
{
fixed (byte* ptr = _equip)
{
return new ReadOnlySpan<CharacterArmor>((CharacterArmor*)ptr, 10);
}
}
}
public string WriteGear()
{
var sb = new StringBuilder(128);
var span = Equip;
for (var i = 0; i < 10; ++i)
{
sb.Append(span[i].Set.Id.ToString("D4"));
sb.Append('-');
sb.Append(span[i].Variant.Id.ToString("D3"));
sb.Append('-');
sb.Append(span[i].Stain.Id.ToString("D3"));
sb.Append(", ");
}
sb.Append(Mainhand.Skeleton.Id.ToString("D4"));
sb.Append('-');
sb.Append(Mainhand.Weapon.Id.ToString("D4"));
sb.Append('-');
sb.Append(Mainhand.Variant.Id.ToString("D3"));
sb.Append('-');
sb.Append(Mainhand.Stain.Id.ToString("D4"));
sb.Append(", ");
sb.Append(Offhand.Skeleton.Id.ToString("D4"));
sb.Append('-');
sb.Append(Offhand.Weapon.Id.ToString("D4"));
sb.Append('-');
sb.Append(Offhand.Variant.Id.ToString("D3"));
sb.Append('-');
sb.Append(Offhand.Stain.Id.ToString("D3"));
return sb.ToString();
}
internal void Set(int idx, uint value)
{
fixed (byte* ptr = _equip)
{
((uint*)ptr)[idx] = value;
}
}
public bool DataEquals(in NpcData other)
{
if (VisorToggled != other.VisorToggled)
return false;
if (!Customize.Equals(other.Customize))
return false;
if (!Mainhand.Equals(other.Mainhand))
return false;
if (!Offhand.Equals(other.Offhand))
return false;
fixed (byte* ptr1 = _equip, ptr2 = other._equip)
{
return new ReadOnlySpan<byte>(ptr1, 40).SequenceEqual(new ReadOnlySpan<byte>(ptr2, 40));
}
}
}

View file

@ -84,7 +84,6 @@
<ItemGroup>
<ProjectReference Include="..\OtterGui\OtterGui.csproj" />
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.string.csproj" />
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
@ -109,8 +108,8 @@
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess"/>
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0"/>
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess" />
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0" />
</Exec>
<PropertyGroup>

View file

@ -1,11 +1,11 @@
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Glamourer.Customization;
using Glamourer.GameData;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Customization;
@ -15,12 +15,12 @@ public partial class CustomizationDrawer
private void DrawColorPicker(CustomizeIndex index)
{
using var _ = SetId(index);
using var id = SetId(index);
var (current, custom) = GetCurrentCustomization(index);
var color = ImGui.ColorConvertU32ToFloat4(current < 0 ? ImGui.GetColorU32(ImGuiCol.FrameBg) : custom.Color);
using (var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0))
{
if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.None, _framedIconSize))
ImGui.OpenPopup(ColorPickerPopupName);
@ -39,7 +39,7 @@ public partial class CustomizationDrawer
ImGui.SameLine();
using (var group = ImRaii.Group())
using (_ = ImRaii.Group())
{
DataInputInt(current, npc);
if (_withApply)
@ -89,7 +89,7 @@ public partial class CustomizationDrawer
{
var current = _set.DataByValue(index, _customize[index], out var custom, _customize.Face);
if (_set.IsAvailable(index) && current < 0)
return (current, new CustomizeData(index, _customize[index], 0, 0));
return (current, new CustomizeData(index, _customize[index]));
return (current, custom!.Value);
}

View file

@ -1,7 +1,7 @@
using System;
using System.Linq;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.GameData;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;

View file

@ -1,9 +1,11 @@
using System;
using System.Numerics;
using Glamourer.Customization;
using Glamourer.GameData;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization;
@ -13,7 +15,7 @@ public partial class CustomizationDrawer
private void DrawIconSelector(CustomizeIndex index)
{
using var _ = SetId(index);
using var id = SetId(index);
using var bigGroup = ImRaii.Group();
var label = _currentOption;
@ -28,7 +30,7 @@ public partial class CustomizationDrawer
}
var icon = _service.Service.GetIcon(custom!.Value.IconId);
using (var disabled = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw))
using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw))
{
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
ImGui.OpenPopup(IconSelectorPopup);
@ -37,7 +39,7 @@ public partial class CustomizationDrawer
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
ImGui.SameLine();
using (var group = ImRaii.Group())
using (_ = ImRaii.Group())
{
DataInputInt(current, npc);
if (_lockedRedraw && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
@ -118,16 +120,16 @@ public partial class CustomizationDrawer
ImGui.Dummy(new Vector2(ImGui.GetFrameHeight()));
}
var oldValue = _customize.Data.At(_currentIndex.ToByteAndMask().ByteIdx);
var tmp = (int)oldValue;
var oldValue = _customize.AtIndex(_currentIndex.ToByteAndMask().ByteIdx);
var tmp = (int)oldValue.Value;
ImGui.SetNextItemWidth(_inputIntSize);
if (ImGui.InputInt("##text", ref tmp, 1, 1))
{
tmp = Math.Clamp(tmp, 0, byte.MaxValue);
if (tmp != oldValue)
if (tmp != oldValue.Value)
{
_customize.Data.Set(_currentIndex.ToByteAndMask().ByteIdx, (byte)tmp);
var changes = (byte)tmp ^ oldValue;
_customize.SetByIndex(_currentIndex.ToByteAndMask().ByteIdx, (CustomizeValue)tmp);
var changes = (byte)tmp ^ oldValue.Value;
Changed |= ((changes & 0x01) == 0x01 ? CustomizeFlag.FacialFeature1 : 0)
| ((changes & 0x02) == 0x02 ? CustomizeFlag.FacialFeature2 : 0)
| ((changes & 0x04) == 0x04 ? CustomizeFlag.FacialFeature3 : 0)

View file

@ -1,10 +1,10 @@
using System;
using System.Numerics;
using Glamourer.Customization;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization;
@ -91,10 +91,10 @@ public partial class CustomizationDrawer
private void DrawListSelector(CustomizeIndex index, bool indexedBy1)
{
using var _ = SetId(index);
using var id = SetId(index);
using var bigGroup = ImRaii.Group();
using (var disabled = ImRaii.Disabled(_locked))
using (_ = ImRaii.Disabled(_locked))
{
if (indexedBy1)
{
@ -210,7 +210,7 @@ public partial class CustomizationDrawer
}
else
{
using (var disabled = ImRaii.Disabled(_locked))
using (_ = ImRaii.Disabled(_locked))
{
if (ImGui.Checkbox("##toggle", ref tmp))
{

View file

@ -4,7 +4,7 @@ using System.Reflection;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
@ -22,13 +22,12 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
private Exception? _terminate;
private Customize _customize = Customize.Default;
private CustomizeArray _customize = CustomizeArray.Default;
private CustomizationSet _set = null!;
public Customize Customize
public CustomizeArray Customize
=> _customize;
public CustomizeFlag CurrentFlag { get; private set; }
public CustomizeFlag Changed { get; private set; }
public CustomizeFlag ChangeApply { get; private set; }
@ -47,18 +46,16 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
public void Dispose()
=> _legacyTattoo?.Dispose();
public bool Draw(Customize current, bool locked, bool lockedRedraw)
public bool Draw(CustomizeArray current, bool locked, bool lockedRedraw)
{
CurrentFlag = CustomizeFlagExtensions.All;
_withApply = false;
Init(current, locked, lockedRedraw);
return DrawInternal();
}
public bool Draw(Customize current, CustomizeFlag apply, bool locked, bool lockedRedraw)
public bool Draw(CustomizeArray current, CustomizeFlag apply, bool locked, bool lockedRedraw)
{
CurrentFlag = CustomizeFlagExtensions.All;
ChangeApply = apply;
_initialApply = apply;
_withApply = !_config.HideApplyCheckmarks;
@ -66,12 +63,12 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
return DrawInternal();
}
private void Init(Customize current, bool locked, bool lockedRedraw)
private void Init(CustomizeArray current, bool locked, bool lockedRedraw)
{
UpdateSizes();
_terminate = null;
Changed = 0;
_customize.Load(current);
_terminate = null;
Changed = 0;
_customize = current;
_locked = locked;
_lockedRedraw = lockedRedraw;
}
@ -156,20 +153,20 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
for (var i = 0; i < CustomizeArray.Size; ++i)
{
using var id = ImRaii.PushId(i);
int value = _customize.Data.Data[i];
int value = _customize.Data[i];
ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt(string.Empty, ref value, 0, 0))
{
var newValue = (byte)Math.Clamp(value, 0, byte.MaxValue);
if (newValue != _customize.Data.Data[i])
if (newValue != _customize.Data[i])
foreach (var flag in Enum.GetValues<CustomizeIndex>())
{
var (j, mask) = flag.ToByteAndMask();
var (j, _) = flag.ToByteAndMask();
if (j == i)
Changed |= flag.ToFlag();
}
_customize.Data.Data[i] = newValue;
_customize.Data[i] = newValue;
}
}

View file

@ -4,7 +4,7 @@ using System.Linq;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Services;
@ -13,6 +13,7 @@ using OtterGui;
using OtterGui.Classes;
using OtterGui.Log;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui;

View file

@ -7,7 +7,6 @@ using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui.Raii;
using Penumbra.Api.Enums;
@ -76,7 +75,7 @@ public class PenumbraChangedItemTooltip : IDisposable
case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item):
break;
case EquipSlot.RFinger:
using (var tt = !openTooltip ? null : ImRaii.Tooltip())
using (_ = !openTooltip ? null : ImRaii.Tooltip())
{
ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor (Right Finger).");
ImGui.TextUnformatted($"{prefix}Shift + Right-Click to apply to current actor (Left Finger).");
@ -92,7 +91,7 @@ public class PenumbraChangedItemTooltip : IDisposable
break;
default:
using (var tt = !openTooltip ? null : ImRaii.Tooltip())
using (_ = !openTooltip ? null : ImRaii.Tooltip())
{
ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor.");
if (last.Valid)

View file

@ -6,7 +6,6 @@ using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui.Customization;
@ -14,7 +13,6 @@ using Glamourer.Gui.Equipment;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
@ -173,21 +171,21 @@ public class ActorPanel(
private void DrawEquipmentMetaToggles()
{
using (var _ = ImRaii.Group())
using (_ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.HatState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Head, _stateManager, _state!));
}
ImGui.SameLine();
using (var _ = ImRaii.Group())
using (_ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.VisorState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Body, _stateManager, _state!));
}
ImGui.SameLine();
using (var _ = ImRaii.Group())
using (_ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.WeaponState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!));
@ -199,7 +197,7 @@ public class ActorPanel(
var names = _modelChara[_state!.ModelData.ModelId];
var turnHuman = ImGui.Button("Turn Human");
ImGui.Separator();
using (var box = ImRaii.ListBox("##MonsterList",
using (_ = ImRaii.ListBox("##MonsterList",
new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetTextLineHeightWithSpacing())))
{
if (names.Count == 0)
@ -211,14 +209,14 @@ public class ActorPanel(
ImGui.Separator();
ImGui.TextUnformatted("Customization Data");
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
using (_ = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var b in _state.ModelData.Customize.Data)
foreach (var b in _state.ModelData.Customize)
{
using (var g = ImRaii.Group())
using (_ = ImRaii.Group())
{
ImGui.TextUnformatted($" {b:X2}");
ImGui.TextUnformatted($"{b,3}");
ImGui.TextUnformatted($" {b.Value:X2}");
ImGui.TextUnformatted($"{b.Value,3}");
}
ImGui.SameLine();
@ -232,11 +230,11 @@ public class ActorPanel(
ImGui.Separator();
ImGui.TextUnformatted("Equipment Data");
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
using (_ = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var b in _state.ModelData.GetEquipmentBytes())
{
using (var g = ImRaii.Group())
using (_ = ImRaii.Group())
{
ImGui.TextUnformatted($" {b:X2}");
ImGui.TextUnformatted($"{b,3}");
@ -298,8 +296,8 @@ public class ActorPanel(
BorderColor = ColorId.ActorUnavailable.Value(),
};
private string _newName = string.Empty;
private DesignBase? _newDesign = null;
private string _newName = string.Empty;
private DesignBase? _newDesign;
private void SaveDesignOpen()
{

View file

@ -6,10 +6,8 @@ using System.Text;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
@ -17,44 +15,29 @@ using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Action = System.Action;
using CustomizeIndex = Glamourer.Customization.CustomizeIndex;
namespace Glamourer.Gui.Tabs.AutomationTab;
public class SetPanel
public class SetPanel(
SetSelector _selector,
AutoDesignManager _manager,
JobService _jobs,
ItemUnlockManager _itemUnlocks,
RevertDesignCombo _designCombo,
CustomizeUnlockManager _customizeUnlocks,
CustomizationService _customizations,
IdentifierDrawer _identifierDrawer,
Configuration _config)
{
private readonly AutoDesignManager _manager;
private readonly SetSelector _selector;
private readonly ItemUnlockManager _itemUnlocks;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly CustomizationService _customizations;
private readonly Configuration _config;
private readonly RevertDesignCombo _designCombo;
private readonly JobGroupCombo _jobGroupCombo;
private readonly IdentifierDrawer _identifierDrawer;
private readonly JobGroupCombo _jobGroupCombo = new(_manager, _jobs, Glamourer.Log);
private string? _tempName;
private int _dragIndex = -1;
private Action? _endAction;
public SetPanel(SetSelector selector, AutoDesignManager manager, JobService jobs, ItemUnlockManager itemUnlocks,
RevertDesignCombo designCombo,
CustomizeUnlockManager customizeUnlocks, CustomizationService customizations, IdentifierDrawer identifierDrawer, Configuration config)
{
_selector = selector;
_manager = manager;
_itemUnlocks = itemUnlocks;
_customizeUnlocks = customizeUnlocks;
_customizations = customizations;
_identifierDrawer = identifierDrawer;
_config = config;
_designCombo = designCombo;
_jobGroupCombo = new JobGroupCombo(manager, jobs, Glamourer.Log);
}
private AutoDesignSet Selection
=> _selector.Selection!;
@ -77,7 +60,7 @@ public class SetPanel
var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y };
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
{
var enabled = Selection.Enabled;
if (ImGui.Checkbox("##Enabled", ref enabled))
@ -87,7 +70,7 @@ public class SetPanel
}
ImGui.SameLine();
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
{
var useGame = _selector.Selection!.BaseState is AutoDesignSet.Base.Game;
if (ImGui.Checkbox("##gameState", ref useGame))
@ -98,7 +81,7 @@ public class SetPanel
}
ImGui.SameLine();
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
{
var editing = _config.ShowAutomationSetEditing;
if (ImGui.Checkbox("##Show Editing", ref editing))
@ -230,7 +213,7 @@ public class SetPanel
if (_config.ShowUnlockedItemWarnings)
{
ImGui.TableNextColumn();
DrawWarnings(design, idx);
DrawWarnings(design);
}
}
@ -278,7 +261,7 @@ public class SetPanel
}
}
private void DrawWarnings(AutoDesign design, int idx)
private void DrawWarnings(AutoDesign design)
{
if (design.Revert)
return;
@ -301,27 +284,6 @@ public class SetPanel
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * ImGuiHelpers.GlobalScale, 0));
static void DrawWarning(StringBuilder sb, uint color, Vector2 size, string suffix, string good)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
if (sb.Length > 0)
{
sb.Append(suffix);
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, color);
}
ImGuiUtil.HoverTooltip(sb.ToString());
}
else
{
ImGuiUtil.DrawTextButton(string.Empty, size, 0);
ImGuiUtil.HoverTooltip(good);
}
}
var tt = _config.UnlockedItemMode
? "\nThese items will be skipped when applied automatically.\n\nTo change this, disable the Obtained Item Mode setting."
: string.Empty;
@ -355,6 +317,27 @@ public class SetPanel
: string.Empty;
DrawWarning(sb2, _config.UnlockedItemMode ? 0xA03030F0 : 0x0, size, tt, "All customizations to be applied are unlocked.");
ImGui.SameLine();
return;
static void DrawWarning(StringBuilder sb, uint color, Vector2 size, string suffix, string good)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
if (sb.Length > 0)
{
sb.Append(suffix);
using (_ = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, color);
}
ImGuiUtil.HoverTooltip(sb.ToString());
}
else
{
ImGuiUtil.DrawTextButton(string.Empty, size, 0);
ImGuiUtil.HoverTooltip(good);
}
}
}
private void DrawDragDrop(AutoDesignSet set, int index)
@ -394,7 +377,7 @@ public class SetPanel
var newType = design.ApplicationType;
var newTypeInt = (uint)newType;
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using (var c = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()))
using (_ = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()))
{
if (ImGui.CheckboxFlags("##all", ref newTypeInt, (uint)AutoDesign.Type.All))
newType = (AutoDesign.Type)newTypeInt;

View file

@ -2,7 +2,7 @@
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;

View file

@ -1,9 +1,10 @@
using System;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.DebugTab;

View file

@ -1,10 +1,10 @@
using System;
using System.Numerics;
using Glamourer.Customization;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.DebugTab;

View file

@ -1,10 +1,9 @@
using System.IO;
using System.Numerics;
using Glamourer.Customization;
using Glamourer.Interop;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
namespace Glamourer.Gui.Tabs.DebugTab;
@ -35,7 +34,7 @@ public class DatFilePanel(ImportService _importService) : IDebugTabTree
ImGui.TextUnformatted(_datFile.Value.Version.ToString());
ImGui.TextUnformatted(_datFile.Value.Time.LocalDateTime.ToString("g"));
ImGui.TextUnformatted(_datFile.Value.Voice.ToString());
ImGui.TextUnformatted(_datFile.Value.Customize.Data.ToString());
ImGui.TextUnformatted(_datFile.Value.Customize.ToString());
ImGui.TextUnformatted(_datFile.Value.Description);
}
}

View file

@ -1,9 +1,7 @@
using System;
using System.Linq;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;

View file

@ -1,10 +1,8 @@
using System;
using System.Linq;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Services;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
@ -85,7 +83,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe
DrawDesignData(_parse64);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
ImGui.TextUnformatted(_base64);
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 }))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 }))
{
foreach (var (c1, c2) in _restore.Zip(_base64))
{
@ -99,7 +97,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe
foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex())
{
using (var group = ImRaii.Group())
using (_ = ImRaii.Group())
{
ImGui.TextUnformatted(idx.ToString("D2"));
ImGui.TextUnformatted(b1.ToString("X2"));
@ -121,7 +119,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
foreach (var (b, idx) in _base64Bytes.WithIndex())
{
using (var group = ImRaii.Group())
using (_ = ImRaii.Group())
{
ImGui.TextUnformatted(idx.ToString("D2"));
ImGui.TextUnformatted(b.ToString("X2"));

View file

@ -32,7 +32,7 @@ public class JobPanel(JobService _jobs) : IDebugTabTree
foreach (var (id, job) in _jobs.Jobs)
{
ImGuiUtil.DrawTableColumn(id.ToString("D3"));
ImGuiUtil.DrawTableColumn(id.Id.ToString("D3"));
ImGuiUtil.DrawTableColumn(job.Name);
ImGuiUtil.DrawTableColumn(job.Abbreviation);
}
@ -68,7 +68,7 @@ public class JobPanel(JobService _jobs) : IDebugTabTree
foreach (var (id, group) in _jobs.JobGroups)
{
ImGuiUtil.DrawTableColumn(id.ToString("D3"));
ImGuiUtil.DrawTableColumn(id.Id.ToString("D3"));
ImGuiUtil.DrawTableColumn(group.Name);
ImGuiUtil.DrawTableColumn(group.Count.ToString());
}

View file

@ -1,10 +1,8 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
@ -126,15 +124,14 @@ public unsafe class ModelEvaluationPanel(
model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty);
}
private void DrawWeaponState(Actor actor, Model model)
private static void DrawWeaponState(Actor actor, Model model)
{
using var id = ImRaii.PushId("WeaponState");
ImGuiUtil.DrawTableColumn("Weapon State");
ImGuiUtil.DrawTableColumn(actor.IsCharacter
? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible"
: "No Character");
var text = string.Empty;
string text;
if (!model.IsHuman)
{
text = "No Model";
@ -146,19 +143,14 @@ public unsafe class ModelEvaluationPanel(
else
{
var weapon = (DrawObject*)model.AsDrawObject->Object.ChildObject;
if ((weapon->Flags & 0x09) == 0x09)
text = "Visible";
else
text = "Hidden";
text = (weapon->Flags & 0x09) == 0x09 ? "Visible" : "Hidden";
}
ImGuiUtil.DrawTableColumn(text);
ImGui.TableNextColumn();
if (!model.IsHuman)
return;
}
private void DrawWetness(Actor actor, Model model)
private static void DrawWetness(Actor actor, Model model)
{
using var id = ImRaii.PushId("Wetness");
ImGuiUtil.DrawTableColumn("Wetness");
@ -212,12 +204,12 @@ public unsafe class ModelEvaluationPanel(
private void DrawCustomize(Actor actor, Model model)
{
using var id = ImRaii.PushId("Customize");
var actorCustomize = new Customize(actor.IsCharacter
var actorCustomize = actor.IsCharacter
? *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData
: new CustomizeArray());
var modelCustomize = new Customize(model.IsHuman
: new CustomizeArray();
var modelCustomize = model.IsHuman
? *(CustomizeArray*)model.AsHuman->Customize.Data
: new CustomizeArray());
: new CustomizeArray();
foreach (var type in Enum.GetValues<CustomizeIndex>())
{
using var id2 = ImRaii.PushId((int)type);
@ -235,7 +227,7 @@ public unsafe class ModelEvaluationPanel(
var shift = BitOperations.TrailingZeroCount(mask);
var newValue = value + (1 << shift);
modelCustomize.Set(type, (CustomizeValue)newValue);
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
_changeCustomizeService.UpdateCustomize(model, modelCustomize);
}
ImGui.SameLine();
@ -246,14 +238,14 @@ public unsafe class ModelEvaluationPanel(
var shift = BitOperations.TrailingZeroCount(mask);
var newValue = value - (1 << shift);
modelCustomize.Set(type, (CustomizeValue)newValue);
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
_changeCustomizeService.UpdateCustomize(model, modelCustomize);
}
ImGui.SameLine();
if (ImGui.SmallButton("Reset"))
{
modelCustomize.Set(type, actorCustomize[type]);
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
_changeCustomizeService.UpdateCustomize(model, modelCustomize);
}
}
}

View file

@ -3,14 +3,15 @@ using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Glamourer.Gui.Tabs.DebugTab;
@ -24,7 +25,7 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
=> false;
private string _npcFilter = string.Empty;
private bool _customizeOrGear = false;
private bool _customizeOrGear;
public void Draw()
{
@ -48,19 +49,17 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
ImGui.TableNextRow();
var idx = 0;
var remainder = ImGuiClip.FilteredClippedDraw(_npcCombo.Items, skips,
d => d.Name.Contains(_npcFilter, StringComparison.OrdinalIgnoreCase), Draw);
d => d.Name.Contains(_npcFilter, StringComparison.OrdinalIgnoreCase), DrawData);
ImGui.TableNextColumn();
ImGuiClip.DrawEndDummy(remainder, ImGui.GetFrameHeightWithSpacing());
return;
void Draw(NpcData data)
void DrawData(NpcData data)
{
using var id = ImRaii.PushId(idx++);
var disabled = !_state.GetOrCreate(_objectManager.Player, out var state);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled, false))
if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled))
{
foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand))
_state.ChangeEquip(state!, slot, item, stain, StateChanged.Source.Manual);
@ -76,7 +75,7 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(data.Kind is ObjectKind.BattleNpc ? "B" : "E");
using (var icon = ImRaii.PushFont(UiBuilder.IconFont))
using (_ = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
@ -86,7 +85,7 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
using var mono = ImRaii.PushFont(UiBuilder.MonoFont);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(_customizeOrGear ? data.Customize.Data.ToString() : data.WriteGear());
ImGui.TextUnformatted(_customizeOrGear ? data.Customize.ToString() : data.WriteGear());
}
}
}

View file

@ -7,14 +7,12 @@ using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;

View file

@ -1,4 +1,4 @@
using Glamourer.Customization;
using Glamourer.GameData;
using OtterGui.Widgets;
namespace Glamourer.Gui.Tabs;

View file

@ -3,7 +3,7 @@ using System.Linq;
using System.Numerics;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.Utility;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Unlocks;

View file

@ -8,7 +8,6 @@ using Dalamud.Interface.Utility;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;

View file

@ -2,7 +2,6 @@
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.State;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui;

View file

@ -1,9 +1,7 @@
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using Glamourer.Unlocks;
using ImGuiNET;
using Lumina.Misc;
@ -61,7 +59,7 @@ public static class UiHelpers
{
var flags = (sbyte)(currentApply ? currentValue ? 1 : -1 : 0);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing);
using (var disabled = ImRaii.Disabled(locked))
using (_ = ImRaii.Disabled(locked))
{
if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw(
"##" + label, flags, out flags))

View file

@ -2,7 +2,6 @@
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
@ -15,7 +14,7 @@ namespace Glamourer.Interop;
/// Changes in Race, body type or Gender are probably ignored.
/// This operates on draw objects, not game objects.
/// </summary>
public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Customize>>, ChangeCustomizeService.Priority>
public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<CustomizeArray>>, ChangeCustomizeService.Priority>
{
private readonly PenumbraReloaded _penumbraReloaded;
private readonly IGameInteropProvider _interop;
@ -81,10 +80,11 @@ public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Cust
{
if (!InUpdate.InMethod)
{
var customize = new Ref<Customize>(new Customize(*(CustomizeArray*)data));
var customize = new Ref<CustomizeArray>(*(CustomizeArray*)data);
Invoke(this, (Model)human, customize);
((Customize*)data)->Load(customize.Value);
*(CustomizeArray*)data = customize.Value;
}
return _changeCustomizeHook.Original(human, data, skipEquipment);
}
}

View file

@ -1,8 +1,6 @@
using System;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -70,7 +68,7 @@ public sealed class CharaFile
return;
data.SetItem(slot, item);
data.SetStain(slot, (StainId)dye);
data.SetStain(slot, dye);
flags |= slot.ToFlag();
flags |= slot.ToStainFlag();
}
@ -94,7 +92,7 @@ public sealed class CharaFile
flags |= slot.ToStainFlag();
}
private static CustomizeFlag ParseCustomize(JObject jObj, ref Customize customize)
private static CustomizeFlag ParseCustomize(JObject jObj, ref CustomizeArray customize)
{
CustomizeFlag ret = 0;
customize.Race = ParseRace(jObj, ref ret);
@ -149,7 +147,7 @@ public sealed class CharaFile
return id;
}
private static void ParseFacial(JObject jObj, ref Customize customize, ref CustomizeFlag application)
private static void ParseFacial(JObject jObj, ref CustomizeArray customize, ref CustomizeFlag application)
{
var jTok = jObj["FacialFeatures"];
if (jTok == null)
@ -186,7 +184,7 @@ public sealed class CharaFile
customize[CustomizeIndex.LegacyTattoo] = CustomizeValue.Max;
}
private static void ParseHighlights(JObject jObj, ref Customize customize, ref CustomizeFlag application)
private static void ParseHighlights(JObject jObj, ref CustomizeArray customize, ref CustomizeFlag application)
{
var jTok = jObj["EnableHighlights"];
if (jTok == null)
@ -242,15 +240,15 @@ public sealed class CharaFile
throw new Exception($"Age {age} != Normal is not supported.");
}
private static unsafe void ParseByte(JObject jObj, string property, CustomizeIndex idx, ref Customize customize,
private static unsafe void ParseByte(JObject jObj, string property, CustomizeIndex idx, ref CustomizeArray customize,
ref CustomizeFlag application)
{
var jTok = jObj[property];
if (jTok == null)
return;
customize.Data.Data[idx.ToByteAndMask().ByteIdx] = jTok.ToObject<byte>();
application |= idx.ToFlag();
customize.Data[idx.ToByteAndMask().ByteIdx] = jTok.ToObject<byte>();
application |= idx.ToFlag();
}
private static SubRace ParseTribe(JObject jObj, ref CustomizeFlag application)

View file

@ -42,7 +42,7 @@ public sealed class CmaFile
var byteData = Convert.FromHexString(bytes);
fixed (byte* ptr = byteData)
{
data.Customize.Data.Read(ptr);
data.Customize.Read(ptr);
}
}
@ -64,7 +64,7 @@ public sealed class CmaFile
data.SetStain(slot, armor.Stain);
}
data.Customize.Data.Read(ptr);
data.Customize.Read(ptr);
}
}

View file

@ -1,12 +1,10 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;

View file

@ -4,13 +4,14 @@ using System.IO;
using System.Linq;
using Dalamud.Interface.DragDrop;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Interop.CharaFile;
using Glamourer.Services;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
@ -139,7 +140,7 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
return true;
}
public bool SaveDesignAsDat(string path, in Customize input, string description)
public bool SaveDesignAsDat(string path, in CustomizeArray input, string description)
{
if (!Verify(input, out var voice))
return false;
@ -168,7 +169,7 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
}
}
public bool Verify(in Customize input, out byte voice, byte? inputVoice = null)
public bool Verify(in CustomizeArray input, out byte voice, byte? inputVoice = null)
{
voice = 0;
if (_customizations.ValidateClan(input.Clan, input.Race, out _, out _).Length > 0)

View file

@ -6,7 +6,9 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using Penumbra.GameData;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
@ -14,19 +16,20 @@ public class JobService : IDisposable
{
private readonly nint _characterDataOffset;
public readonly IReadOnlyDictionary<byte, Job> Jobs;
public readonly IReadOnlyDictionary<ushort, JobGroup> JobGroups;
public readonly IReadOnlyList<JobGroup> AllJobGroups;
public readonly DictJob Jobs;
public readonly DictJobGroup JobGroups;
public IReadOnlyList<JobGroup> AllJobGroups
=> JobGroups.AllJobGroups;
public event Action<Actor, Job, Job>? JobChanged;
public JobService(IDataManager gameData, IGameInteropProvider interop)
public JobService(DictJob jobs, DictJobGroup jobGroups, IDataManager gameData, IGameInteropProvider interop)
{
interop.InitializeFromAttributes(this);
_characterDataOffset = Marshal.OffsetOf<Character>(nameof(Character.CharacterData));
Jobs = GameData.Jobs(gameData);
AllJobGroups = GameData.AllJobGroups(gameData);
JobGroups = GameData.JobGroups(gameData);
Jobs = jobs;
JobGroups = jobGroups;
_changeJobHook.Enable();
}

View file

@ -2,8 +2,6 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Customization;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.String;
@ -116,8 +114,8 @@ public readonly unsafe struct Actor : IEquatable<Actor>
public CharacterWeapon GetOffhand()
=> new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).ModelId.Value);
public Customize GetCustomize()
=> *(Customize*)&AsCharacter->DrawData.CustomizeData;
public CustomizeArray GetCustomize()
=> *(CustomizeArray*)&AsCharacter->DrawData.CustomizeData;
// TODO remove this when available in ClientStructs
internal ref CrestFlag CrestBitfield

View file

@ -1,7 +1,6 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
@ -91,8 +90,8 @@ public readonly unsafe struct Model : IEquatable<Model>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()];
public Customize GetCustomize()
=> *(Customize*)&AsHuman->Customize;
public CustomizeArray GetCustomize()
=> *(CustomizeArray*)&AsHuman->Customize;
public (Model Address, CharacterWeapon Data) GetMainhand()
{

View file

@ -4,6 +4,7 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;

View file

@ -5,7 +5,6 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -14,18 +13,15 @@ namespace Glamourer.Interop;
public unsafe class WeaponService : IDisposable
{
private readonly WeaponLoading _event;
private readonly CrestService _crestService;
private readonly ThreadLocal<bool> _inUpdate = new(() => false);
private readonly delegate* unmanaged[Stdcall]<DrawDataContainer*, uint, ulong, byte, byte, byte, byte, void>
_original;
public WeaponService(WeaponLoading @event, IGameInteropProvider interop, CrestService crestService)
public WeaponService(WeaponLoading @event, IGameInteropProvider interop)
{
_event = @event;
_crestService = crestService;
_event = @event;
_loadWeaponHook =
interop.HookFromAddress<LoadWeaponDelegate>((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_original =
@ -73,7 +69,7 @@ public unsafe class WeaponService : IDisposable
_event.Invoke(actor, EquipSlot.MainHand, ref tmpWeapon);
_loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4);
if (tmpWeapon.Value != weapon.Value)
{
if (tmpWeapon.Skeleton.Id == 0)

View file

@ -5,13 +5,11 @@ using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;

View file

@ -2,10 +2,11 @@ using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Dalamud.Plugin.Services;
using Glamourer.Customization;
using Glamourer.GameData;
using OtterGui.Services;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Services;
@ -30,13 +31,12 @@ public sealed class CustomizationService(
public Task Awaiter
=> _task;
public (Customize NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(Customize oldValues, Customize newValues,
public (CustomizeArray NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(CustomizeArray oldValues, CustomizeArray newValues,
CustomizeFlag applyWhich, bool allowUnknown)
{
CustomizeFlag applied = 0;
CustomizeFlag changed = 0;
Customize ret = default;
ret.Load(oldValues);
var ret = oldValues;
if (applyWhich.HasFlag(CustomizeFlag.Clan))
{
changed |= ChangeClan(ref ret, newValues.Clan);
@ -247,7 +247,7 @@ public sealed class CustomizationService(
}
/// <summary> Change a clan while keeping all other customizations valid. </summary>
public CustomizeFlag ChangeClan(ref Customize customize, SubRace newClan)
public CustomizeFlag ChangeClan(ref CustomizeArray customize, SubRace newClan)
{
if (customize.Clan == newClan)
return 0;
@ -271,7 +271,7 @@ public sealed class CustomizationService(
}
/// <summary> Change a gender while keeping all other customizations valid. </summary>
public CustomizeFlag ChangeGender(ref Customize customize, Gender newGender)
public CustomizeFlag ChangeGender(ref CustomizeArray customize, Gender newGender)
{
if (customize.Gender == newGender)
return 0;
@ -288,7 +288,7 @@ public sealed class CustomizationService(
return FixValues(set, ref customize) | CustomizeFlag.Gender;
}
private static CustomizeFlag FixValues(CustomizationSet set, ref Customize customize)
private static CustomizeFlag FixValues(CustomizationSet set, ref CustomizeArray customize)
{
CustomizeFlag flags = 0;
foreach (var idx in CustomizationExtensions.AllBasic)

View file

@ -15,7 +15,6 @@ using Glamourer.Gui.Tabs.UnlocksTab;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Unlocks;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
@ -24,6 +23,7 @@ using OtterGui.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Services;

View file

@ -1,13 +1,11 @@
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Structs;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using CustomizeIndex = Glamourer.Customization.CustomizeIndex;
using Penumbra.GameData.Structs;
namespace Glamourer.State;
@ -34,7 +32,7 @@ public class ActorState
public DesignData ModelData;
/// <summary> The last seen job. </summary>
public byte LastJob;
public JobId LastJob;
/// <summary> The Lock-Key locking this state. </summary>
public uint Combination;

View file

@ -2,7 +2,6 @@
using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Gui;
using Glamourer.Interop;
@ -12,7 +11,7 @@ using ImGuiNET;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using CustomizeIndex = Glamourer.Customization.CustomizeIndex;
using CustomizeIndex = Penumbra.GameData.Enums.CustomizeIndex;
namespace Glamourer.State;
@ -107,7 +106,7 @@ public unsafe class FunModule : IDisposable
}
}
public void ApplyFun(Actor actor, Span<CharacterArmor> armor, ref Customize customize)
public void ApplyFun(Actor actor, Span<CharacterArmor> armor, ref CustomizeArray customize)
{
if (!ValidFunTarget(actor))
return;
@ -181,7 +180,7 @@ public unsafe class FunModule : IDisposable
}
}
public void ApplyOops(ref Customize customize)
public void ApplyOops(ref CustomizeArray customize)
{
if (_codes.EnabledOops == Race.Unknown)
return;
@ -193,7 +192,7 @@ public unsafe class FunModule : IDisposable
_customizations.ChangeClan(ref customize, targetClan);
}
public void ApplyIndividual(ref Customize customize)
public void ApplyIndividual(ref CustomizeArray customize)
{
if (!_codes.EnabledIndividual)
return;
@ -209,7 +208,7 @@ public unsafe class FunModule : IDisposable
}
}
public void Apply63(ref Customize customize)
public void Apply63(ref CustomizeArray customize)
{
if (!_codes.Enabled63 || customize.Race is Race.Hrothgar) // TODO Female Hrothgar
return;
@ -217,7 +216,7 @@ public unsafe class FunModule : IDisposable
_customizations.ChangeGender(ref customize, customize.Gender is Gender.Male ? Gender.Female : Gender.Male);
}
public void ApplySizing(Actor actor, ref Customize customize)
public void ApplySizing(Actor actor, ref CustomizeArray customize)
{
if (_codes.EnabledSizing == CodeService.Sizing.None)
return;

View file

@ -1,11 +1,9 @@
using System.Linq;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.Structs;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -40,7 +38,7 @@ public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, We
/// Change the customization values of actors either by applying them via update or redrawing,
/// this depends on whether the changes include changes to Race, Gender, Body Type or Face.
/// </summary>
public unsafe void ChangeCustomize(ActorData data, in Customize customize, ActorState? state = null)
public unsafe void ChangeCustomize(ActorData data, in CustomizeArray customize, ActorState? _ = null)
{
foreach (var actor in data.Objects)
{
@ -48,15 +46,15 @@ public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, We
if (!mdl.IsCharacterBase)
continue;
var flags = Customize.Compare(mdl.GetCustomize(), customize);
var flags = CustomizeArray.Compare(mdl.GetCustomize(), customize);
if (!flags.RequiresRedraw() || !mdl.IsHuman)
{
_changeCustomize.UpdateCustomize(mdl, customize.Data);
_changeCustomize.UpdateCustomize(mdl, customize);
}
else if (data.Objects.Count > 1 && _objects.IsInGPose && !actor.IsGPoseOrCutscene)
{
var mdlCustomize = (Customize*)&mdl.AsHuman->Customize;
mdlCustomize->Load(customize);
var mdlCustomize = (CustomizeArray*)&mdl.AsHuman->Customize;
*mdlCustomize = customize;
_penumbra.RedrawObject(actor, RedrawType.AfterGPose);
}
else
@ -66,7 +64,7 @@ public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, We
}
}
/// <inheritdoc cref="ChangeCustomize(ActorData, in Customize, ActorState?)"/>
/// <inheritdoc cref="ChangeCustomize(ActorData, in CustomizeArray, ActorState?)"/>
public ActorData ChangeCustomize(ActorState state, bool apply)
{
var data = GetData(state);

View file

@ -1,10 +1,8 @@
using System;
using System.Linq;
using Dalamud.Plugin.Services;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Services;
using Glamourer.Structs;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -30,7 +28,7 @@ public class StateEditor
/// <summary> Change the model id. If the actor is changed from a human to another human, customize and equipData are unused. </summary>
/// <remarks> We currently only allow changing things to humans, not humans to monsters. </remarks>
public bool ChangeModelId(ActorState state, uint modelId, in Customize customize, nint equipData, StateChanged.Source source,
public bool ChangeModelId(ActorState state, uint modelId, in CustomizeArray customize, nint equipData, StateChanged.Source source,
out uint oldModelId, uint key = 0)
{
oldModelId = state.ModelData.ModelId;
@ -57,7 +55,7 @@ public class StateEditor
return false;
// Fix up everything else to make sure the result is a valid human.
state.ModelData.Customize = Customize.Default;
state.ModelData.Customize = CustomizeArray.Default;
state.ModelData.SetDefaultEquipment(_items);
state.ModelData.SetHatVisible(true);
state.ModelData.SetWeaponVisible(true);
@ -104,8 +102,8 @@ public class StateEditor
}
/// <summary> Change an entire customization array according to flags. </summary>
public bool ChangeHumanCustomize(ActorState state, in Customize customizeInput, CustomizeFlag applyWhich, StateChanged.Source source,
out Customize old, out CustomizeFlag changed, uint key = 0)
public bool ChangeHumanCustomize(ActorState state, in CustomizeArray customizeInput, CustomizeFlag applyWhich, StateChanged.Source source,
out CustomizeArray old, out CustomizeFlag changed, uint key = 0)
{
old = state.ModelData.Customize;
changed = 0;

View file

@ -1,5 +1,4 @@
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
@ -12,7 +11,6 @@ using Penumbra.GameData.Structs;
using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Glamourer.Structs;
using Penumbra.GameData.DataContainers;
namespace Glamourer.State;
@ -114,7 +112,7 @@ public class StateListener : IDisposable
_creatingIdentifier = actor.GetIdentifier(_actors);
ref var modelId = ref *(uint*)modelPtr;
ref var customize = ref *(Customize*)customizePtr;
ref var customize = ref *(CustomizeArray*)customizePtr;
if (_autoDesignApplier.Reduce(actor, _creatingIdentifier, out _creatingState))
{
switch (UpdateBaseData(actor, _creatingState, modelId, customizePtr, equipDataPtr))
@ -140,7 +138,7 @@ public class StateListener : IDisposable
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
}
private unsafe void OnCustomizeChange(Model model, Ref<Customize> customize)
private unsafe void OnCustomizeChange(Model model, Ref<CustomizeArray> customize)
{
if (!model.IsHuman)
return;
@ -156,7 +154,7 @@ public class StateListener : IDisposable
UpdateCustomize(actor, state, ref customize.Value, false);
}
private void UpdateCustomize(Actor actor, ActorState state, ref Customize customize, bool checkTransform)
private void UpdateCustomize(Actor actor, ActorState state, ref CustomizeArray customize, bool checkTransform)
{
switch (UpdateBaseData(actor, state, customize, checkTransform))
{
@ -515,7 +513,7 @@ public class StateListener : IDisposable
if (isHuman)
state.BaseData = _manager.FromActor(actor, false, false);
else
state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, equipData);
state.BaseData.LoadNonHuman(modelId, *(CustomizeArray*)customizeData, equipData);
return UpdateState.Change;
}
@ -526,7 +524,7 @@ public class StateListener : IDisposable
/// only if we kept track of state of someone who went to the aesthetician,
/// or if they used other tools to change things.
/// </summary>
private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize, bool checkTransform)
private UpdateState UpdateBaseData(Actor actor, ActorState state, CustomizeArray customize, bool checkTransform)
{
// Customize array does not agree between game object and draw object => transformation.
if (checkTransform && !actor.GetCustomize().Equals(customize))
@ -537,7 +535,7 @@ public class StateListener : IDisposable
return UpdateState.NoChange; // TODO: handle wrong base data.
// Update customize base state.
state.BaseData.Customize.Load(customize);
state.BaseData.Customize = customize;
return UpdateState.Change;
}

View file

@ -4,13 +4,11 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.Structs;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
@ -18,8 +16,15 @@ using Penumbra.GameData.Structs;
namespace Glamourer.State;
public class StateManager(ActorManager _actors, ItemManager _items, StateChanged _event, StateApplier _applier, StateEditor _editor,
HumanModelList _humans, ICondition _condition, IClientState _clientState)
public class StateManager(
ActorManager _actors,
ItemManager _items,
StateChanged _event,
StateApplier _applier,
StateEditor _editor,
HumanModelList _humans,
ICondition _condition,
IClientState _clientState)
: IReadOnlyDictionary<ActorIdentifier, ActorState>
{
private readonly Dictionary<ActorIdentifier, ActorState> _states = [];
@ -102,7 +107,7 @@ public class StateManager(ActorManager _actors, ItemManager _items, StateChanged
// TODO reverse search model data to get model id from model.
if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId))
{
ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData,
(nint)(&actor.AsCharacter->DrawData.Head));
return ret;
}
@ -194,9 +199,9 @@ public class StateManager(ActorManager _actors, ItemManager _items, StateChanged
return;
var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Skeleton, (Variant)offhand.Weapon.Id);
offhand.Skeleton = (PrimaryId)(mainhand.Skeleton.Id + 50);
offhand.Variant = mainhand.Variant;
offhand.Weapon = mainhand.Weapon;
offhand.Skeleton = (PrimaryId)(mainhand.Skeleton.Id + 50);
offhand.Variant = mainhand.Variant;
offhand.Weapon = mainhand.Weapon;
ret.SetItem(EquipSlot.Hands, gauntlets);
ret.SetStain(EquipSlot.Hands, mainhand.Stain);
}
@ -205,10 +210,10 @@ public class StateManager(ActorManager _actors, ItemManager _items, StateChanged
/// <summary> Turn an actor human. </summary>
public void TurnHuman(ActorState state, StateChanged.Source source, uint key = 0)
=> ChangeModelId(state, 0, Customize.Default, nint.Zero, source, key);
=> ChangeModelId(state, 0, CustomizeArray.Default, nint.Zero, source, key);
/// <summary> Turn an actor to. </summary>
public void ChangeModelId(ActorState state, uint modelId, Customize customize, nint equipData, StateChanged.Source source,
public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateChanged.Source source,
uint key = 0)
{
if (!_editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key))
@ -233,7 +238,8 @@ public class StateManager(ActorManager _actors, ItemManager _items, StateChanged
}
/// <summary> Change an entire customization array according to flags. </summary>
public void ChangeCustomize(ActorState state, in Customize customizeInput, CustomizeFlag apply, StateChanged.Source source, uint key = 0)
public void ChangeCustomize(ActorState state, in CustomizeArray customizeInput, CustomizeFlag apply, StateChanged.Source source,
uint key = 0)
{
if (!_editor.ChangeHumanCustomize(state, customizeInput, apply, source, out var old, out var applied, key))
return;
@ -447,7 +453,7 @@ public class StateManager(ActorManager _actors, ItemManager _items, StateChanged
var redraw = state.ModelData.ModelId != state.BaseData.ModelId
|| !state.ModelData.IsHuman
|| Customize.Compare(state.ModelData.Customize, state.BaseData.Customize).RequiresRedraw();
|| CustomizeArray.Compare(state.ModelData.Customize, state.BaseData.Customize).RequiresRedraw();
state.ModelData = state.BaseData;
state.ModelData.SetIsWet(false);
foreach (var index in Enum.GetValues<CustomizeIndex>())
@ -458,7 +464,7 @@ public class StateManager(ActorManager _actors, ItemManager _items, StateChanged
state[slot, true] = StateChanged.Source.Game;
state[slot, false] = StateChanged.Source.Game;
}
foreach (var type in Enum.GetValues<ActorState.MetaIndex>())
state[type] = StateChanged.Source.Game;
@ -470,7 +476,7 @@ public class StateManager(ActorManager _actors, ItemManager _items, StateChanged
actors = ApplyAll(state, redraw, true);
Glamourer.Log.Verbose(
$"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Reset, StateChanged.Source.Manual, state, actors, null);
_event.Invoke(StateChanged.Type.Reset, StateChanged.Source.Manual, state, actors);
}
public void ResetStateFixed(ActorState state, uint key = 0)
@ -538,7 +544,7 @@ public class StateManager(ActorManager _actors, ItemManager _items, StateChanged
if (!GetOrCreate(actor, out var state))
return;
ApplyAll(state, !actor.Model.IsHuman || Customize.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(),
ApplyAll(state, !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(),
false);
}

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;

View file

@ -8,10 +8,11 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Events;
using Glamourer.Services;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
namespace Glamourer.Unlocks;
@ -172,7 +173,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable
=> UnlockDictionaryHelpers.Load(ToFilename(_saveService.FileNames), _unlocked, id => Unlockable.Any(c => c.Value.Data == id),
"customization");
/// <summary> Create a list of all unlockable hairstyles and facepaints. </summary>
/// <summary> Create a list of all unlockable hairstyles and face paints. </summary>
private static Dictionary<CustomizeData, (uint Data, string Name)> CreateUnlockableCustomizations(CustomizationService customizations,
IDataManager gameData)
{