Rework and improve CustomizationManager and stuff.

This commit is contained in:
Ottermandias 2023-12-23 19:33:50 +01:00
parent aae4141550
commit ab76d3508b
34 changed files with 916 additions and 1025 deletions

View file

@ -27,7 +27,7 @@ public class AutoDesignApplier : IDisposable
private readonly JobService _jobs; private readonly JobService _jobs;
private readonly EquippedGearset _equippedGearset; private readonly EquippedGearset _equippedGearset;
private readonly ActorManager _actors; private readonly ActorManager _actors;
private readonly CustomizationService _customizations; private readonly CustomizeService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks; private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly ItemUnlockManager _itemUnlocks; private readonly ItemUnlockManager _itemUnlocks;
private readonly AutomationChanged _event; private readonly AutomationChanged _event;
@ -48,7 +48,7 @@ public class AutoDesignApplier : IDisposable
} }
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs,
CustomizationService customizations, ActorManager actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks, CustomizeService customizations, ActorManager actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks,
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState, AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState,
EquippedGearset equippedGearset) EquippedGearset equippedGearset)
{ {
@ -468,7 +468,7 @@ public class AutoDesignApplier : IDisposable
totalCustomizeFlags |= CustomizeFlag.Face; totalCustomizeFlags |= CustomizeFlag.Face;
} }
var set = _customizations.Service.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); var set = _customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender);
var face = state.ModelData.Customize.Face; var face = state.ModelData.Customize.Face;
foreach (var index in Enum.GetValues<CustomizeIndex>()) foreach (var index in Enum.GetValues<CustomizeIndex>())
{ {
@ -477,7 +477,7 @@ public class AutoDesignApplier : IDisposable
continue; continue;
var value = design.Customize[index]; var value = design.Customize[index];
if (CustomizationService.IsCustomizationValid(set, face, index, value, out var data)) if (CustomizeService.IsCustomizationValid(set, face, index, value, out var data))
{ {
if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _)) if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _))
continue; continue;

View file

@ -15,7 +15,7 @@ public sealed class Design : DesignBase, ISavable
{ {
#region Data #region Data
internal Design(CustomizationService customize, ItemManager items) internal Design(CustomizeService customize, ItemManager items)
: base(customize, items) : base(customize, items)
{ } { }
@ -98,7 +98,7 @@ public sealed class Design : DesignBase, ISavable
#region Deserialization #region Deserialization
public static Design LoadDesign(CustomizationService customizations, ItemManager items, JObject json) public static Design LoadDesign(CustomizeService customizations, ItemManager items, JObject json)
{ {
var version = json["FileVersion"]?.ToObject<int>() ?? 0; var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch return version switch
@ -108,7 +108,7 @@ public sealed class Design : DesignBase, ISavable
}; };
} }
private static Design LoadDesignV1(CustomizationService customizations, ItemManager items, JObject json) private static Design LoadDesignV1(CustomizeService customizations, ItemManager items, JObject json)
{ {
static string[] ParseTags(JObject json) static string[] ParseTags(JObject json)
{ {

View file

@ -25,35 +25,35 @@ public class DesignBase
public ref DesignData GetDesignDataRef() public ref DesignData GetDesignDataRef()
=> ref _designData; => ref _designData;
internal DesignBase(CustomizationService customize, ItemManager items) internal DesignBase(CustomizeService customize, ItemManager items)
{ {
_designData.SetDefaultEquipment(items); _designData.SetDefaultEquipment(items);
CustomizationSet = SetCustomizationSet(customize); CustomizeSet = SetCustomizationSet(customize);
} }
internal DesignBase(CustomizationService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags) internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{ {
_designData = designData; _designData = designData;
ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant; ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
ApplyEquip = equipFlags & EquipFlagExtensions.All; ApplyEquip = equipFlags & EquipFlagExtensions.All;
_designFlags = 0; _designFlags = 0;
CustomizationSet = SetCustomizationSet(customize); CustomizeSet = SetCustomizationSet(customize);
} }
internal DesignBase(DesignBase clone) internal DesignBase(DesignBase clone)
{ {
_designData = clone._designData; _designData = clone._designData;
CustomizationSet = clone.CustomizationSet; CustomizeSet = clone.CustomizeSet;
ApplyCustomize = clone.ApplyCustomizeRaw; ApplyCustomize = clone.ApplyCustomizeRaw;
ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All; ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
_designFlags = clone._designFlags & (DesignFlags)0x0F; _designFlags = clone._designFlags & (DesignFlags)0x0F;
} }
/// <summary> Ensure that the customization set is updated when the design data changes. </summary> /// <summary> Ensure that the customization set is updated when the design data changes. </summary>
internal void SetDesignData(CustomizationService customize, in DesignData other) internal void SetDesignData(CustomizeService customize, in DesignData other)
{ {
_designData = other; _designData = other;
CustomizationSet = SetCustomizationSet(customize); CustomizeSet = SetCustomizationSet(customize);
} }
#region Application Data #region Application Data
@ -69,11 +69,11 @@ public class DesignBase
} }
private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant; private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant;
public CustomizationSet CustomizationSet { get; private set; } public CustomizeSet CustomizeSet { get; private set; }
internal CustomizeFlag ApplyCustomize internal CustomizeFlag ApplyCustomize
{ {
get => _applyCustomize.FixApplication(CustomizationSet); get => _applyCustomize.FixApplication(CustomizeSet);
set => _applyCustomize = value & CustomizeFlagExtensions.AllRelevant; set => _applyCustomize = value & CustomizeFlagExtensions.AllRelevant;
} }
@ -84,13 +84,13 @@ public class DesignBase
internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant; internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant;
private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible; private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible;
public bool SetCustomize(CustomizationService customizationService, CustomizeArray customize) public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize)
{ {
if (customize.Equals(_designData.Customize)) if (customize.Equals(_designData.Customize))
return false; return false;
_designData.Customize = customize; _designData.Customize = customize;
CustomizationSet = customizationService.Service.GetList(customize.Clan, customize.Gender); CustomizeSet = customizeService.Manager.GetSet(customize.Clan, customize.Gender);
return true; return true;
} }
@ -240,10 +240,10 @@ public class DesignBase
} }
} }
private CustomizationSet SetCustomizationSet(CustomizationService customize) private CustomizeSet SetCustomizationSet(CustomizeService customize)
=> !_designData.IsHuman => !_designData.IsHuman
? customize.Service.GetList(SubRace.Midlander, Gender.Male) ? customize.Manager.GetSet(SubRace.Midlander, Gender.Male)
: customize.Service.GetList(_designData.Customize.Clan, _designData.Customize.Gender); : customize.Manager.GetSet(_designData.Customize.Clan, _designData.Customize.Gender);
#endregion #endregion
@ -330,7 +330,7 @@ public class DesignBase
#region Deserialization #region Deserialization
public static DesignBase LoadDesignBase(CustomizationService customizations, ItemManager items, JObject json) public static DesignBase LoadDesignBase(CustomizeService customizations, ItemManager items, JObject json)
{ {
var version = json["FileVersion"]?.ToObject<int>() ?? 0; var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch return version switch
@ -340,7 +340,7 @@ public class DesignBase
}; };
} }
private static DesignBase LoadDesignV1Base(CustomizationService customizations, ItemManager items, JObject json) private static DesignBase LoadDesignV1Base(CustomizeService customizations, ItemManager items, JObject json)
{ {
var ret = new DesignBase(customizations, items); var ret = new DesignBase(customizations, items);
LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true); LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true);
@ -435,7 +435,7 @@ public class DesignBase
design._designData.SetVisor(metaValue.ForcedValue); design._designData.SetVisor(metaValue.ForcedValue);
} }
protected static void LoadCustomize(CustomizationService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman, protected static void LoadCustomize(CustomizeService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman,
bool allowUnknown) bool allowUnknown)
{ {
if (json == null) if (json == null)
@ -473,7 +473,7 @@ public class DesignBase
{ {
var arrayText = json["Array"]?.ToObject<string>() ?? string.Empty; var arrayText = json["Array"]?.ToObject<string>() ?? string.Empty;
design._designData.Customize.LoadBase64(arrayText); design._designData.Customize.LoadBase64(arrayText);
design.CustomizationSet = design.SetCustomizationSet(customizations); design.CustomizeSet = design.SetCustomizationSet(customizations);
return; return;
} }
@ -485,18 +485,18 @@ public class DesignBase
design._designData.Customize.Race = race; design._designData.Customize.Race = race;
design._designData.Customize.Clan = clan; design._designData.Customize.Clan = clan;
design._designData.Customize.Gender = gender; design._designData.Customize.Gender = gender;
design.CustomizationSet = design.SetCustomizationSet(customizations); design.CustomizeSet = design.SetCustomizationSet(customizations);
design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject<bool>() ?? false); design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject<bool>() ?? false); design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject<bool>() ?? false); design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
var set = design.CustomizationSet; var set = design.CustomizeSet;
foreach (var idx in CustomizationExtensions.AllBasic) foreach (var idx in CustomizationExtensions.AllBasic)
{ {
var tok = json[idx.ToString()]; var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0); var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
if (set.IsAvailable(idx)) if (set.IsAvailable(idx))
PrintWarning(CustomizationService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data, PrintWarning(CustomizeService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data,
allowUnknown)); allowUnknown));
var apply = tok?["Apply"]?.ToObject<bool>() ?? false; var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
design._designData.Customize[idx] = data; design._designData.Customize[idx] = data;
@ -504,7 +504,7 @@ public class DesignBase
} }
} }
public void MigrateBase64(CustomizationService customize, ItemManager items, HumanModelList humans, string base64) public void MigrateBase64(CustomizeService customize, ItemManager items, HumanModelList humans, string base64)
{ {
try try
{ {
@ -518,7 +518,7 @@ public class DesignBase
SetApplyVisorToggle(applyVisor); SetApplyVisorToggle(applyVisor);
SetApplyWeaponVisible(applyWeapon); SetApplyWeaponVisible(applyWeapon);
SetApplyWetness(true); SetApplyWetness(true);
CustomizationSet = SetCustomizationSet(customize); CustomizeSet = SetCustomizationSet(customize);
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -13,7 +13,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Designs; namespace Glamourer.Designs;
public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizationService _customize, HumanModelList _humans) public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizeService _customize, HumanModelList _humans)
{ {
public const byte Version = 6; public const byte Version = 6;

View file

@ -18,7 +18,7 @@ namespace Glamourer.Designs;
public class DesignManager public class DesignManager
{ {
private readonly CustomizationService _customizations; private readonly CustomizeService _customizations;
private readonly ItemManager _items; private readonly ItemManager _items;
private readonly HumanModelList _humans; private readonly HumanModelList _humans;
private readonly SaveService _saveService; private readonly SaveService _saveService;
@ -29,7 +29,7 @@ public class DesignManager
public IReadOnlyList<Design> Designs public IReadOnlyList<Design> Designs
=> _designs; => _designs;
public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations, public DesignManager(SaveService saveService, ItemManager items, CustomizeService customizations,
DesignChanged @event, HumanModelList humans) DesignChanged @event, HumanModelList humans)
{ {
_saveService = saveService; _saveService = saveService;

View file

@ -4,9 +4,7 @@ using Lumina.Excel.GeneratedSheets;
namespace Glamourer.GameData; namespace Glamourer.GameData;
/// <summary> /// <summary> A custom version of CharaMakeParams that is easier to parse. </summary>
/// A custom version of CharaMakeParams that is easier to parse.
/// </summary>
[Sheet("CharaMakeParams")] [Sheet("CharaMakeParams")]
public class CharaMakeParams : ExcelRow public class CharaMakeParams : ExcelRow
{ {

View file

@ -6,10 +6,12 @@ using Penumbra.String.Functions;
namespace Glamourer.GameData; namespace Glamourer.GameData;
/// <summary> Parse the Human.cmp file as a list of 4-byte integer values to obtain colors. </summary>
public class ColorParameters : IReadOnlyList<uint> public class ColorParameters : IReadOnlyList<uint>
{ {
private readonly uint[] _rgbaColors; private readonly uint[] _rgbaColors;
/// <summary> Get a slice of the colors starting at <paramref name="offset"/> and containing <paramref name="count"/> colors. </summary>
public ReadOnlySpan<uint> GetSlice(int offset, int count) public ReadOnlySpan<uint> GetSlice(int offset, int count)
=> _rgbaColors.AsSpan(offset, count); => _rgbaColors.AsSpan(offset, count);
@ -18,6 +20,7 @@ public class ColorParameters : IReadOnlyList<uint>
try try
{ {
var file = gameData.GetFile("chara/xls/charamake/human.cmp")!; var file = gameData.GetFile("chara/xls/charamake/human.cmp")!;
// Just copy all the data into an uint array.
_rgbaColors = new uint[file.Data.Length >> 2]; _rgbaColors = new uint[file.Data.Length >> 2];
fixed (byte* ptr1 = file.Data) fixed (byte* ptr1 = file.Data)
{ {
@ -32,19 +35,23 @@ public class ColorParameters : IReadOnlyList<uint>
log.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n" 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" + "======== 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); + "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
_rgbaColors = Array.Empty<uint>(); _rgbaColors = [];
} }
} }
/// <inheritdoc/>
public IEnumerator<uint> GetEnumerator() public IEnumerator<uint> GetEnumerator()
=> (IEnumerator<uint>)_rgbaColors.GetEnumerator(); => (IEnumerator<uint>)_rgbaColors.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
/// <inheritdoc/>
public int Count public int Count
=> _rgbaColors.Length; => _rgbaColors.Length;
/// <inheritdoc/>
public uint this[int index] public uint this[int index]
=> _rgbaColors[index]; => _rgbaColors[index];
} }

View file

@ -1,38 +0,0 @@
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

@ -1,38 +0,0 @@
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

@ -1,56 +0,0 @@
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

@ -1,530 +0,0 @@
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

@ -5,26 +5,34 @@ using Penumbra.GameData.Structs;
namespace Glamourer.GameData; namespace Glamourer.GameData;
// Any customization value can be represented in 8 bytes by its ID, /// <summary>
// a byte value, an optional value-id and an optional icon or color. /// 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.
/// </summary>
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
public readonly struct CustomizeData : IEquatable<CustomizeData> public readonly struct CustomizeData : IEquatable<CustomizeData>
{ {
/// <summary> The index of the option this value is for. </summary>
[FieldOffset(0)] [FieldOffset(0)]
public readonly CustomizeIndex Index; public readonly CustomizeIndex Index;
/// <summary> The value for the option. </summary>
[FieldOffset(1)] [FieldOffset(1)]
public readonly CustomizeValue Value; public readonly CustomizeValue Value;
/// <summary> The internal ID for sheets. </summary>
[FieldOffset(2)] [FieldOffset(2)]
public readonly ushort CustomizeId; public readonly ushort CustomizeId;
/// <summary> An ID for an associated icon. </summary>
[FieldOffset(4)] [FieldOffset(4)]
public readonly uint IconId; public readonly uint IconId;
/// <summary> An ID for an associated color. </summary>
[FieldOffset(4)] [FieldOffset(4)]
public readonly uint Color; public readonly uint Color;
/// <summary> Construct a CustomizeData from single data values. </summary>
public CustomizeData(CustomizeIndex index, CustomizeValue value, uint data = 0, ushort customizeId = 0) public CustomizeData(CustomizeIndex index, CustomizeValue value, uint data = 0, ushort customizeId = 0)
{ {
Index = index; Index = index;
@ -34,14 +42,23 @@ public readonly struct CustomizeData : IEquatable<CustomizeData>
CustomizeId = customizeId; CustomizeId = customizeId;
} }
/// <inheritdoc/>
public bool Equals(CustomizeData other) public bool Equals(CustomizeData other)
=> Index == other.Index => Index == other.Index
&& Value.Value == other.Value.Value && Value.Value == other.Value.Value
&& CustomizeId == other.CustomizeId; && CustomizeId == other.CustomizeId;
/// <inheritdoc/>
public override bool Equals(object? obj) public override bool Equals(object? obj)
=> obj is CustomizeData other && Equals(other); => obj is CustomizeData other && Equals(other);
/// <inheritdoc/>
public override int GetHashCode() public override int GetHashCode()
=> HashCode.Combine((int)Index, Value.Value, CustomizeId); => HashCode.Combine((int)Index, Value.Value, CustomizeId);
public static bool operator ==(CustomizeData left, CustomizeData right)
=> left.Equals(right);
public static bool operator !=(CustomizeData left, CustomizeData right)
=> !(left == right);
} }

View file

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.GameData;
/// <summary> Generate everything about customization per tribe and gender. </summary>
public class CustomizeManager : IAsyncService
{
/// <summary> All races except for Unknown </summary>
public static readonly IReadOnlyList<Race> Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
/// <summary> All tribes except for Unknown </summary>
public static readonly IReadOnlyList<SubRace> Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
/// <summary> Two genders. </summary>
public static readonly IReadOnlyList<Gender> Genders =
[
Gender.Male,
Gender.Female,
];
/// <summary> Every tribe and gender has a separate set of available customizations. </summary>
public CustomizeSet GetSet(SubRace race, Gender gender)
{
if (!Awaiter.IsCompletedSuccessfully)
Awaiter.Wait();
return _customizationSets[ToIndex(race, gender)];
}
/// <summary> Get specific icons. </summary>
public IDalamudTextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id)!;
/// <summary> Iterate over all supported genders and clans. </summary>
public static IEnumerable<(SubRace Clan, Gender Gender)> AllSets()
{
foreach (var clan in Clans)
{
yield return (clan, Gender.Male);
yield return (clan, Gender.Female);
}
}
public CustomizeManager(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet)
{
_icons = new IconStorage(textures, gameData);
var tmpTask = Task.Run(() => new CustomizeSetFactory(gameData, log, _icons, npcCustomizeSet));
var setTasks = AllSets().Select(p
=> tmpTask.ContinueWith(t => _customizationSets[ToIndex(p.Clan, p.Gender)] = t.Result.CreateSet(p.Clan, p.Gender)));
Awaiter = Task.WhenAll(setTasks);
}
/// <inheritdoc/>
public Task Awaiter { get; }
private readonly IconStorage _icons;
private static readonly int ListSize = Clans.Count * Genders.Count;
private readonly CustomizeSet[] _customizationSets = new CustomizeSet[ListSize];
/// <summary> Get the index for the given pair of tribe and gender. </summary>
private static int ToIndex(SubRace race, Gender gender)
{
var idx = ((int)race - 1) * Genders.Count + (gender == Gender.Female ? 1 : 0);
if (idx < 0 || idx >= ListSize)
throw new Exception($"Invalid customization requested for {race} {gender}.");
return idx;
}
}

View file

@ -8,11 +8,13 @@ using Penumbra.GameData.Structs;
namespace Glamourer.GameData; namespace Glamourer.GameData;
// Each Subrace and Gender combo has a customization set. /// <summary>
// This describes the available customizations, their types and their names. /// Each SubRace and Gender combo has a customization set.
public class CustomizationSet /// This describes the available customizations, their types and their names.
/// </summary>
public class CustomizeSet
{ {
internal CustomizationSet(SubRace clan, Gender gender) internal CustomizeSet(SubRace clan, Gender gender)
{ {
Gender = gender; Gender = gender;
Clan = clan; Clan = clan;
@ -24,6 +26,8 @@ public class CustomizationSet
public SubRace Clan { get; } public SubRace Clan { get; }
public Race Race { get; } public Race Race { get; }
public string Name { get; internal init; } = string.Empty;
public CustomizeFlag SettingAvailable { get; internal set; } public CustomizeFlag SettingAvailable { get; internal set; }
internal void SetAvailable(CustomizeIndex index) internal void SetAvailable(CustomizeIndex index)
@ -33,7 +37,7 @@ public class CustomizationSet
=> SettingAvailable.HasFlag(index.ToFlag()); => SettingAvailable.HasFlag(index.ToFlag());
// Meta // Meta
public IReadOnlyList<string> OptionName { get; internal set; } = null!; public IReadOnlyList<string> OptionName { get; internal init; } = null!;
public string Option(CustomizeIndex index) public string Option(CustomizeIndex index)
=> OptionName[(int)index]; => OptionName[(int)index];
@ -95,68 +99,6 @@ public class CustomizationSet
{ {
var type = Types[(int)index]; 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 return type switch
{ {
CharaMakeParams.MenuType.ListSelector => GetInteger0(out custom), CharaMakeParams.MenuType.ListSelector => GetInteger0(out custom),
@ -194,6 +136,68 @@ public class CustomizationSet
CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom), CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom),
_ => Invalid(out custom), _ => Invalid(out custom),
}; };
int Get(IEnumerable<CustomizeData> list, CustomizeValue v, out CustomizeData? output)
{
var (val, idx) = list.Cast<CustomizeData?>().WithIndex().FirstOrDefault(p => p.Value!.Value.Value == v);
if (val == null)
{
output = null;
return -1;
}
output = val;
return idx;
}
static int Invalid(out CustomizeData? custom)
{
custom = null;
return -1;
}
static int GetBool(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom)
{
if (value == CustomizeValue.Zero)
{
custom = new CustomizeData(index, CustomizeValue.Zero);
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;
}
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;
}
int GetInteger0(out CustomizeData? custom)
{
if (value < Count(index))
{
custom = new CustomizeData(index, value, 0, value.Value);
return value.Value;
}
custom = null;
return -1;
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
@ -244,7 +248,7 @@ public class CustomizationSet
public CharaMakeParams.MenuType Type(CustomizeIndex index) public CharaMakeParams.MenuType Type(CustomizeIndex index)
=> Types[(int)index]; => Types[(int)index];
internal static IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizeIndex[]> ComputeOrder(CustomizationSet set) internal static IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizeIndex[]> ComputeOrder(CustomizeSet set)
{ {
var ret = Enum.GetValues<CustomizeIndex>().ToArray(); var ret = Enum.GetValues<CustomizeIndex>().ToArray();
ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft; ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft;
@ -305,6 +309,6 @@ public class CustomizationSet
public static class CustomizationSetExtensions public static class CustomizationSetExtensions
{ {
/// <summary> Return only the available customizations in this set and Clan or Gender. </summary> /// <summary> Return only the available customizations in this set and Clan or Gender. </summary>
public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizationSet set) public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizeSet set)
=> flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender); => flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender);
} }

View file

@ -0,0 +1,459 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud;
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;
internal class CustomizeSetFactory(
IDataManager _gameData,
IPluginLog _log,
IconStorage _icons,
NpcCustomizeSet _npcCustomizeSet,
ColorParameters _colors)
{
public CustomizeSetFactory(IDataManager gameData, IPluginLog log, IconStorage icons, NpcCustomizeSet npcCustomizeSet)
: this(gameData, log, icons, npcCustomizeSet, new ColorParameters(gameData, log))
{ }
/// <summary> Create the set of all available customization options for a given clan and gender. </summary>
public CustomizeSet CreateSet(SubRace race, Gender gender)
{
var (skin, hair) = GetSkinHairColors(race, gender);
var row = _charaMakeSheet.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 CustomizeSet(race, gender)
{
Name = GetName(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 ? [] : _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),
OptionName = GetOptionNames(row),
Types = GetMenuTypes(row),
};
SetPostProcessing(set, row);
return set;
}
/// <summary> Some data can not be set independently of the rest, so we need a post-processing step to finalize. </summary>
private void SetPostProcessing(CustomizeSet set, CharaMakeParams row)
{
SetAvailability(set, row);
SetFacialFeatures(set, row);
SetHairByFace(set);
SetNpcData(set, set.Clan, set.Gender);
}
/// <summary> Given a customize set with filled data, find all customizations used by valid NPCs that are not regularly available. </summary>
private void SetNpcData(CustomizeSet set, SubRace race, Gender gender)
{
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,
};
var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>();
_npcCustomizeSet.Awaiter.Wait();
foreach (var customize in _npcCustomizeSet.Select(s => s.Customize).Where(c => c.Clan == race && c.Gender == 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;
npcCustomizations.Add((customizeIndex, value));
}
}
set.NpcOptions = npcCustomizations.OrderBy(p => p.Item1).ThenBy(p => p.Item2.Value).ToArray();
}
private readonly ColorParameters _colorParameters = new(_gameData, _log);
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet = _gameData.GetExcelSheet<CharaMakeCustomize>(ClientLanguage.English)!;
private readonly ExcelSheet<Lobby> _lobbySheet = _gameData.GetExcelSheet<Lobby>(ClientLanguage.English)!;
private readonly ExcelSheet<HairMakeType> _hairSheet = _gameData.GetExcelSheet<HairMakeType>(ClientLanguage.English)!;
private readonly ExcelSheet<Tribe> _tribeSheet = _gameData.GetExcelSheet<Tribe>(ClientLanguage.English)!;
// Those color pickers are shared between all races.
private readonly CustomizeData[] _highlightPicker = CreateColors(_colors, CustomizeIndex.HighlightsColor, 256, 192);
private readonly CustomizeData[] _lipColorPickerDark = CreateColors(_colors, CustomizeIndex.LipColor, 512, 96);
private readonly CustomizeData[] _lipColorPickerLight = CreateColors(_colors, CustomizeIndex.LipColor, 1024, 96, true);
private readonly CustomizeData[] _eyeColorPicker = CreateColors(_colors, CustomizeIndex.EyeColorLeft, 0, 192);
private readonly CustomizeData[] _facePaintColorPickerDark = CreateColors(_colors, CustomizeIndex.FacePaintColor, 640, 96);
private readonly CustomizeData[] _facePaintColorPickerLight = CreateColors(_colors, CustomizeIndex.FacePaintColor, 1152, 96, true);
private readonly CustomizeData[] _tattooColorPicker = CreateColors(_colors, CustomizeIndex.TattooColor, 0, 192);
private readonly ExcelSheet<CharaMakeParams> _charaMakeSheet = _gameData.Excel
.GetType()
.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
.MakeGenericMethod(typeof(CharaMakeParams))
.Invoke(_gameData.Excel, ["charamaketype", _gameData.Language.ToLumina(), null])! as ExcelSheet<CharaMakeParams>
?? null!;
/// <summary> Obtain available skin and hair colors for the given clan and gender. </summary>
private (CustomizeData[] Skin, CustomizeData[] Hair) GetSkinHairColors(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 (CreateColors(_colorParameters, CustomizeIndex.SkinColor, idx << 8, 192),
CreateColors(_colorParameters, CustomizeIndex.HairColor, (idx + 1) << 8, 192));
}
/// <summary> Obtain the gender-specific clan name. </summary>
private string GetName(SubRace race, Gender gender)
=> gender switch
{
Gender.Male => _tribeSheet.GetRow((uint)race)?.Masculine.ToDalamudString().TextValue ?? race.ToName(),
Gender.Female => _tribeSheet.GetRow((uint)race)?.Feminine.ToDalamudString().TextValue ?? race.ToName(),
_ => "Unknown",
};
/// <summary> Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. </summary>
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 (_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)];
}
/// <summary> Specific icons for tails or ears. </summary>
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()
?? [];
/// <summary> Specific icons for faces. </summary>
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()
?? [];
/// <summary> Specific icons for Hrothgar patterns. </summary>
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()
?? [];
/// <summary> Get face paints from the hair sheet via reflection since there are also unlockable face paints. </summary>
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);
// Face paint Row from CustomizeSheet might not be set in case of unlockable face paints.
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)];
}
/// <summary> Get List sizes. </summary>
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;
}
/// <summary> Get generic Features. </summary>
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);
}
/// <summary> Create generic color sets from the parameters. </summary>
private static CustomizeData[] CreateColors(ColorParameters colorParameters, CustomizeIndex index, int offset, int num,
bool light = false)
{
var ret = new CustomizeData[num];
var idx = 0;
foreach (var value in colorParameters.GetSlice(offset, num))
{
ret[idx] = new CustomizeData(index, (CustomizeValue)(light ? 128 + idx : idx), value, (ushort)(offset + idx));
++idx;
}
return ret;
}
/// <summary> Set the specific option names for the given set of parameters. </summary>
private string[] GetOptionNames(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 _lobbySheet.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 = _lobbySheet.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();
return nameArray;
}
/// <summary> Get the manu types for all available options. </summary>
private CharaMakeParams.MenuType[] GetMenuTypes(CharaMakeParams row)
{
// Set up the menu types for all customizations.
return 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();
}
/// <summary> Set the availability of options according to actual availability. </summary>
private static void SetAvailability(CustomizeSet set, CharaMakeParams row)
{
// TODO: Hrothgar female
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);
}
}
/// <summary> Set hairstyles per face for Hrothgar and make it simple for non-Hrothgar. </summary>
private void SetHairByFace(CustomizeSet 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)
{
tmp[i] = set.HairStyles.Where(Valid).ToArray();
continue;
bool Valid(CustomizeData c)
{
var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0;
return data == 0 || data == i + set.Faces.Count;
}
}
set.HairByFace = tmp;
}
/// <summary>
/// Create a list of lists of facial features and the legacy tattoo.
/// Facial Features are bools in a bitfield, so we supply an "off" and an "on" value for simplicity of use.
/// </summary>
private static void SetFacialFeatures(CustomizeSet 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));
}
}

View file

@ -1,17 +0,0 @@
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

@ -13,20 +13,30 @@ using Penumbra.GameData.Structs;
namespace Glamourer.GameData; namespace Glamourer.GameData;
/// <summary> Contains a set of all human NPC appearances with their names. </summary>
public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData> public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
{ {
/// <inheritdoc/>
public string Name public string Name
=> nameof(NpcCustomizeSet); => nameof(NpcCustomizeSet);
private readonly List<NpcData> _data = []; /// <inheritdoc/>
public long Time { get; private set; } public long Time { get; private set; }
/// <inheritdoc/>
public long Memory { get; private set; } public long Memory { get; private set; }
/// <inheritdoc/>
public int TotalCount public int TotalCount
=> _data.Count; => _data.Count;
/// <inheritdoc/>
public Task Awaiter { get; } public Task Awaiter { get; }
/// <summary> The list of data. </summary>
private readonly List<NpcData> _data = [];
/// <summary> Create the data when ready. </summary>
public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames) public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{ {
var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter); var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter);
@ -40,17 +50,21 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
}); });
} }
/// <summary> Create data from event NPCs. </summary>
private static List<NpcData> CreateEnpcData(IDataManager data, DictENpc eNpcs) private static List<NpcData> CreateEnpcData(IDataManager data, DictENpc eNpcs)
{ {
var enpcSheet = data.GetExcelSheet<ENpcBase>()!; var enpcSheet = data.GetExcelSheet<ENpcBase>()!;
var list = new List<NpcData>(eNpcs.Count); var list = new List<NpcData>(eNpcs.Count);
// Go through all event NPCs already collected into a dictionary.
foreach (var (id, name) in eNpcs) foreach (var (id, name) in eNpcs)
{ {
var row = enpcSheet.GetRow(id.Id); var row = enpcSheet.GetRow(id.Id);
// We only accept NPCs with valid names.
if (row == null || name.IsNullOrWhitespace()) if (row == null || name.IsNullOrWhitespace())
continue; continue;
// Check if the customization is a valid human.
var (valid, customize) = FromEnpcBase(row); var (valid, customize) = FromEnpcBase(row);
if (!valid) if (!valid)
continue; continue;
@ -63,6 +77,8 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
Kind = ObjectKind.EventNpc, Kind = ObjectKind.EventNpc,
}; };
// Event NPCs have a reference to NpcEquip but also contain the appearance in their own row.
// Prefer the NpcEquip reference if it is set, otherwise use the own.
if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip) if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip)
{ {
ApplyNpcEquip(ref ret, equip); ApplyNpcEquip(ref ret, equip);
@ -90,19 +106,25 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
return list; return list;
} }
/// <summary> Create data from battle NPCs. </summary>
private static List<NpcData> CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames) private static List<NpcData> CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{ {
var bnpcSheet = data.GetExcelSheet<BNpcBase>()!; var bnpcSheet = data.GetExcelSheet<BNpcBase>()!;
var list = new List<NpcData>((int)bnpcSheet.RowCount); var list = new List<NpcData>((int)bnpcSheet.RowCount);
// We go through all battle NPCs in the sheet because the dictionary refers to names.
foreach (var baseRow in bnpcSheet) foreach (var baseRow in bnpcSheet)
{ {
// Only accept humans.
if (baseRow.ModelChara.Value!.Type != 1) if (baseRow.ModelChara.Value!.Type != 1)
continue; continue;
var bnpcNameIds = bNpcNames[baseRow.RowId]; var bnpcNameIds = bNpcNames[baseRow.RowId];
// Only accept battle NPCs with known associated names.
if (bnpcNameIds.Count == 0) if (bnpcNameIds.Count == 0)
continue; continue;
// Check if the customization is a valid human.
var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!); var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!);
if (!valid) if (!valid)
continue; continue;
@ -115,6 +137,7 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
Kind = ObjectKind.BattleNpc, Kind = ObjectKind.BattleNpc,
}; };
ApplyNpcEquip(ref ret, equip); ApplyNpcEquip(ref ret, equip);
// Add the appearance for each associated name.
foreach (var bnpcNameId in bnpcNameIds) foreach (var bnpcNameId in bnpcNameIds)
{ {
if (bNpcs.TryGetValue(bnpcNameId.Id, out var name) && !name.IsNullOrWhitespace()) if (bNpcs.TryGetValue(bnpcNameId.Id, out var name) && !name.IsNullOrWhitespace())
@ -125,13 +148,18 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
return list; return list;
} }
private void FilterAndOrderNpcData(List<NpcData> eNpcEquip, List<NpcData> bNpcEquip) /// <summary> Given the battle NPC and event NPC lists, order and deduplicate entries. </summary>
private void FilterAndOrderNpcData(IReadOnlyCollection<NpcData> eNpcEquip, IReadOnlyCollection<NpcData> bNpcEquip)
{ {
_data.Clear(); _data.Clear();
// This is a maximum since we deduplicate.
_data.EnsureCapacity(eNpcEquip.Count + bNpcEquip.Count); _data.EnsureCapacity(eNpcEquip.Count + bNpcEquip.Count);
// Convert the NPCs to a dictionary of lists grouped by name.
var groups = eNpcEquip.Concat(bNpcEquip).GroupBy(d => d.Name).ToDictionary(g => g.Key, g => g.ToList()); var groups = eNpcEquip.Concat(bNpcEquip).GroupBy(d => d.Name).ToDictionary(g => g.Key, g => g.ToList());
// Iterate through the sorted list.
foreach (var (name, duplicates) in groups.OrderBy(kvp => kvp.Key)) foreach (var (name, duplicates) in groups.OrderBy(kvp => kvp.Key))
{ {
// Remove any duplicate entries for a name with identical data.
for (var i = 0; i < duplicates.Count; ++i) for (var i = 0; i < duplicates.Count; ++i)
{ {
var current = duplicates[i]; var current = duplicates[i];
@ -145,6 +173,7 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
} }
} }
// If there is only a single entry, add that. This does not take additional string memory through interning.
if (duplicates.Count == 1) if (duplicates.Count == 1)
{ {
_data.Add(duplicates[0]); _data.Add(duplicates[0]);
@ -152,24 +181,29 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
} }
else else
{ {
// Add all distinct duplicates with their ID specified in the name.
_data.AddRange(duplicates _data.AddRange(duplicates
.Select(duplicate => duplicate with .Select(duplicate => duplicate with
{ {
Name = $"{name} ({(duplicate.Kind is ObjectKind.BattleNpc ? 'B' : 'E')}{duplicate.Id})" Name = $"{name} ({(duplicate.Kind is ObjectKind.BattleNpc ? 'B' : 'E')}{duplicate.Id})",
})); }));
Memory += 96 * duplicates.Count + duplicates.Sum(d => d.Name.Length * 2); Memory += 96 * duplicates.Count + duplicates.Sum(d => d.Name.Length * 2);
} }
} }
// Sort non-alphanumeric entries at the end instead of the beginning.
var lastWeird = _data.FindIndex(d => char.IsAsciiLetterOrDigit(d.Name[0])); var lastWeird = _data.FindIndex(d => char.IsAsciiLetterOrDigit(d.Name[0]));
if (lastWeird != -1) if (lastWeird != -1)
{ {
_data.AddRange(_data.Take(lastWeird)); _data.AddRange(_data.Take(lastWeird));
_data.RemoveRange(0, lastWeird); _data.RemoveRange(0, lastWeird);
} }
// Reduce memory footprint.
_data.TrimExcess(); _data.TrimExcess();
} }
/// <summary> Apply equipment from a NpcEquip row. </summary>
private static void ApplyNpcEquip(ref NpcData data, NpcEquip row) private static void ApplyNpcEquip(ref NpcData data, NpcEquip row)
{ {
data.Set(0, row.ModelHead | (row.DyeHead.Row << 24)); data.Set(0, row.ModelHead | (row.DyeHead.Row << 24));
@ -187,6 +221,7 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
data.VisorToggled = row.Visor; data.VisorToggled = row.Visor;
} }
/// <summary> Obtain customizations from a BNpcCustomize row and check if the human is valid. </summary>
private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize) private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize)
{ {
var customize = new CustomizeArray(); var customize = new CustomizeArray();
@ -218,14 +253,15 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
customize.SetByIndex(25, (CustomizeValue)bnpcCustomize.FacePaintColor); customize.SetByIndex(25, (CustomizeValue)bnpcCustomize.FacePaintColor);
if (customize.BodyType.Value != 1 if (customize.BodyType.Value != 1
|| !CustomizationOptions.Races.Contains(customize.Race) || !CustomizeManager.Races.Contains(customize.Race)
|| !CustomizationOptions.Clans.Contains(customize.Clan) || !CustomizeManager.Clans.Contains(customize.Clan)
|| !CustomizationOptions.Genders.Contains(customize.Gender)) || !CustomizeManager.Genders.Contains(customize.Gender))
return (false, CustomizeArray.Default); return (false, CustomizeArray.Default);
return (true, customize); return (true, customize);
} }
/// <summary> Obtain customizations from a ENpcBase row and check if the human is valid. </summary>
private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase) private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase)
{ {
if (enpcBase.ModelChara.Value?.Type != 1) if (enpcBase.ModelChara.Value?.Type != 1)
@ -260,23 +296,27 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
customize.SetByIndex(25, (CustomizeValue)enpcBase.FacePaintColor); customize.SetByIndex(25, (CustomizeValue)enpcBase.FacePaintColor);
if (customize.BodyType.Value != 1 if (customize.BodyType.Value != 1
|| !CustomizationOptions.Races.Contains(customize.Race) || !CustomizeManager.Races.Contains(customize.Race)
|| !CustomizationOptions.Clans.Contains(customize.Clan) || !CustomizeManager.Clans.Contains(customize.Clan)
|| !CustomizationOptions.Genders.Contains(customize.Gender)) || !CustomizeManager.Genders.Contains(customize.Gender))
return (false, CustomizeArray.Default); return (false, CustomizeArray.Default);
return (true, customize); return (true, customize);
} }
/// <inheritdoc/>
public IEnumerator<NpcData> GetEnumerator() public IEnumerator<NpcData> GetEnumerator()
=> _data.GetEnumerator(); => _data.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
/// <inheritdoc/>
public int Count public int Count
=> _data.Count; => _data.Count;
/// <inheritdoc/>
public NpcData this[int index] public NpcData this[int index]
=> _data[index]; => _data[index];
} }

View file

@ -5,17 +5,34 @@ using Penumbra.GameData.Structs;
namespace Glamourer.GameData; namespace Glamourer.GameData;
/// <summary> A struct containing everything to replicate the appearance of a human NPC. </summary>
public unsafe struct NpcData public unsafe struct NpcData
{ {
/// <summary> The name of the NPC. </summary>
public string Name; public string Name;
/// <summary> The customizations of the NPC. </summary>
public CustomizeArray Customize; public CustomizeArray Customize;
/// <summary> The equipment appearance of the NPC, 10 * CharacterArmor. </summary>
private fixed byte _equip[40]; private fixed byte _equip[40];
/// <summary> The mainhand weapon appearance of the NPC. </summary>
public CharacterWeapon Mainhand; public CharacterWeapon Mainhand;
/// <summary> The offhand weapon appearance of the NPC. </summary>
public CharacterWeapon Offhand; public CharacterWeapon Offhand;
/// <summary> The data ID of the NPC, either event NPC or battle NPC name. </summary>
public NpcId Id; public NpcId Id;
/// <summary> Whether the NPCs visor is toggled. </summary>
public bool VisorToggled; public bool VisorToggled;
/// <summary> Whether the NPC is an event NPC or a battle NPC. </summary>
public ObjectKind Kind; public ObjectKind Kind;
/// <summary> Obtain the equipment as CharacterArmors. </summary>
public ReadOnlySpan<CharacterArmor> Equip public ReadOnlySpan<CharacterArmor> Equip
{ {
get get
@ -27,38 +44,40 @@ public unsafe struct NpcData
} }
} }
/// <summary> Write all the gear appearance to a single string. </summary>
public string WriteGear() public string WriteGear()
{ {
var sb = new StringBuilder(128); var sb = new StringBuilder(128);
var span = Equip; var span = Equip;
for (var i = 0; i < 10; ++i) for (var i = 0; i < 10; ++i)
{ {
sb.Append(span[i].Set.Id.ToString("D4")); sb.Append(span[i].Set.Id.ToString("D4"))
sb.Append('-'); .Append('-')
sb.Append(span[i].Variant.Id.ToString("D3")); .Append(span[i].Variant.Id.ToString("D3"))
sb.Append('-'); .Append('-')
sb.Append(span[i].Stain.Id.ToString("D3")); .Append(span[i].Stain.Id.ToString("D3"))
sb.Append(", "); .Append(", ");
} }
sb.Append(Mainhand.Skeleton.Id.ToString("D4")); sb.Append(Mainhand.Skeleton.Id.ToString("D4"))
sb.Append('-'); .Append('-')
sb.Append(Mainhand.Weapon.Id.ToString("D4")); .Append(Mainhand.Weapon.Id.ToString("D4"))
sb.Append('-'); .Append('-')
sb.Append(Mainhand.Variant.Id.ToString("D3")); .Append(Mainhand.Variant.Id.ToString("D3"))
sb.Append('-'); .Append('-')
sb.Append(Mainhand.Stain.Id.ToString("D4")); .Append(Mainhand.Stain.Id.ToString("D4"))
sb.Append(", "); .Append(", ")
sb.Append(Offhand.Skeleton.Id.ToString("D4")); .Append(Offhand.Skeleton.Id.ToString("D4"))
sb.Append('-'); .Append('-')
sb.Append(Offhand.Weapon.Id.ToString("D4")); .Append(Offhand.Weapon.Id.ToString("D4"))
sb.Append('-'); .Append('-')
sb.Append(Offhand.Variant.Id.ToString("D3")); .Append(Offhand.Variant.Id.ToString("D3"))
sb.Append('-'); .Append('-')
sb.Append(Offhand.Stain.Id.ToString("D3")); .Append(Offhand.Stain.Id.ToString("D3"));
return sb.ToString(); return sb.ToString();
} }
/// <summary> Set an equipment piece to a given value. </summary>
internal void Set(int idx, uint value) internal void Set(int idx, uint value)
{ {
fixed (byte* ptr = _equip) fixed (byte* ptr = _equip)
@ -67,6 +86,7 @@ public unsafe struct NpcData
} }
} }
/// <summary> Check if the appearance data, excluding ID and Name, of two NpcData is equal. </summary>
public bool DataEquals(in NpcData other) public bool DataEquals(in NpcData other)
{ {
if (VisorToggled != other.VisorToggled) if (VisorToggled != other.VisorToggled)

View file

@ -29,7 +29,7 @@ public partial class CustomizationDrawer
npc = true; npc = true;
} }
var icon = _service.Service.GetIcon(custom!.Value.IconId); var icon = _service.Manager.GetIcon(custom!.Value.IconId);
using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw)) using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw))
{ {
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize)) if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
@ -69,7 +69,7 @@ public partial class CustomizationDrawer
for (var i = 0; i < _currentCount; ++i) for (var i = 0; i < _currentCount; ++i)
{ {
var custom = _set.Data(_currentIndex, i, _customize.Face); var custom = _set.Data(_currentIndex, i, _customize.Face);
var icon = _service.Service.GetIcon(custom.IconId); var icon = _service.Manager.GetIcon(custom.IconId);
using (var _ = ImRaii.Group()) using (var _ = ImRaii.Group())
{ {
using var frameColor = ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed, current == i); using var frameColor = ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed, current == i);
@ -180,8 +180,8 @@ public partial class CustomizationDrawer
var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero; var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero;
var feature = _set.Data(featureIdx, 0, face); var feature = _set.Data(featureIdx, 0, face);
var icon = featureIdx == CustomizeIndex.LegacyTattoo var icon = featureIdx == CustomizeIndex.LegacyTattoo
? _legacyTattoo ?? _service.Service.GetIcon(feature.IconId) ? _legacyTattoo ?? _service.Manager.GetIcon(feature.IconId)
: _service.Service.GetIcon(feature.IconId); : _service.Manager.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X, if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X,
Vector4.Zero, enabled ? Vector4.One : _redTint)) Vector4.Zero, enabled ? Vector4.One : _redTint))
{ {

View file

@ -14,7 +14,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization; namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizationService _service, CodeService _codes, Configuration _config) public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeService _service, CodeService _codes, Configuration _config)
: IDisposable : IDisposable
{ {
private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f); private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f);
@ -23,7 +23,7 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
private Exception? _terminate; private Exception? _terminate;
private CustomizeArray _customize = CustomizeArray.Default; private CustomizeArray _customize = CustomizeArray.Default;
private CustomizationSet _set = null!; private CustomizeSet _set = null!;
public CustomizeArray Customize public CustomizeArray Customize
=> _customize; => _customize;
@ -117,7 +117,7 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
return DrawArtisan(); return DrawArtisan();
DrawRaceGenderSelector(); DrawRaceGenderSelector();
_set = _service.Service.GetList(_customize.Clan, _customize.Gender); _set = _service.Manager.GetSet(_customize.Clan, _customize.Gender);
foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage]) foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage])
PercentageSelector(id); PercentageSelector(id);

View file

@ -180,7 +180,7 @@ public sealed class RevertDesignCombo : DesignComboBase, IDisposable
private readonly AutoDesignManager _autoDesignManager; private readonly AutoDesignManager _autoDesignManager;
public RevertDesignCombo(DesignManager designs, DesignFileSystem fileSystem, TabSelected tabSelected, DesignColors designColors, public RevertDesignCombo(DesignManager designs, DesignFileSystem fileSystem, TabSelected tabSelected, DesignColors designColors,
ItemManager items, CustomizationService customize, Logger log, DesignChanged designChanged, AutoDesignManager autoDesignManager, ItemManager items, CustomizeService customize, Logger log, DesignChanged designChanged, AutoDesignManager autoDesignManager,
EphemeralConfig config) EphemeralConfig config)
: this(designs, fileSystem, tabSelected, designColors, CreateRevertDesign(customize, items), log, designChanged, autoDesignManager, : this(designs, fileSystem, tabSelected, designColors, CreateRevertDesign(customize, items), log, designChanged, autoDesignManager,
config) config)
@ -210,7 +210,7 @@ public sealed class RevertDesignCombo : DesignComboBase, IDisposable
_autoDesignManager.AddDesign(set, CurrentSelection!.Item1 == RevertDesign ? null : CurrentSelection!.Item1); _autoDesignManager.AddDesign(set, CurrentSelection!.Item1 == RevertDesign ? null : CurrentSelection!.Item1);
} }
private static Design CreateRevertDesign(CustomizationService customize, ItemManager items) private static Design CreateRevertDesign(CustomizeService customize, ItemManager items)
=> new(customize, items) => new(customize, items)
{ {
Index = RevertDesignIndex, Index = RevertDesignIndex,

View file

@ -1,4 +1,5 @@
using System.Numerics; using System;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
@ -61,7 +62,7 @@ public class GenericPopupWindow : Window
private void DrawFestivalPopup() private void DrawFestivalPopup()
{ {
var viewportSize = ImGui.GetWindowViewport().Size; var viewportSize = ImGui.GetWindowViewport().Size;
ImGui.SetNextWindowSize(new Vector2(viewportSize.X / 5, viewportSize.Y / 7)); ImGui.SetNextWindowSize(new Vector2(Math.Max(viewportSize.X / 5, 400), Math.Max(viewportSize.Y / 7, 150)));
ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f)); ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f));
using var popup = ImRaii.Popup("FestivalPopup", ImGuiWindowFlags.Modal); using var popup = ImRaii.Popup("FestivalPopup", ImGuiWindowFlags.Modal);
if (!popup) if (!popup)

View file

@ -27,7 +27,7 @@ public class SetPanel(
ItemUnlockManager _itemUnlocks, ItemUnlockManager _itemUnlocks,
RevertDesignCombo _designCombo, RevertDesignCombo _designCombo,
CustomizeUnlockManager _customizeUnlocks, CustomizeUnlockManager _customizeUnlocks,
CustomizationService _customizations, CustomizeService _customizations,
IdentifierDrawer _identifierDrawer, IdentifierDrawer _identifierDrawer,
Configuration _config) Configuration _config)
{ {
@ -295,7 +295,7 @@ public class SetPanel(
if (!design.Design.DesignData.IsHuman) if (!design.Design.DesignData.IsHuman)
sb.AppendLine("The base model id can not be changed automatically to something non-human."); sb.AppendLine("The base model id can not be changed automatically to something non-human.");
var set = _customizations.Service.GetList(customize.Clan, customize.Gender); var set = _customizations.Manager.GetSet(customize.Clan, customize.Gender);
foreach (var type in CustomizationExtensions.All) foreach (var type in CustomizationExtensions.All)
{ {
var flag = type.ToFlag(); var flag = type.ToFlag();

View file

@ -8,30 +8,27 @@ using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.DebugTab; namespace Glamourer.Gui.Tabs.DebugTab;
public class CustomizationServicePanel(CustomizationService _customization) : IDebugTabTree public class CustomizationServicePanel(CustomizeService customize) : IDebugTabTree
{ {
public string Label public string Label
=> "Customization Service"; => "Customization Service";
public bool Disabled public bool Disabled
=> !_customization.Awaiter.IsCompletedSuccessfully; => !customize.Awaiter.IsCompletedSuccessfully;
public void Draw() public void Draw()
{ {
foreach (var clan in _customization.Service.Clans) foreach (var (clan, gender) in CustomizeManager.AllSets())
{ {
foreach (var gender in _customization.Service.Genders) var set = customize.Manager.GetSet(clan, gender);
{
var set = _customization.Service.GetList(clan, gender);
DrawCustomizationInfo(set); DrawCustomizationInfo(set);
DrawNpcCustomizationInfo(set); DrawNpcCustomizationInfo(set);
} }
} }
}
private void DrawCustomizationInfo(CustomizationSet set) private void DrawCustomizationInfo(CustomizeSet set)
{ {
using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}"); using var tree = ImRaii.TreeNode($"{customize.ClanName(set.Clan, set.Gender)} {set.Gender}");
if (!tree) if (!tree)
return; return;
@ -49,9 +46,9 @@ public class CustomizationServicePanel(CustomizationService _customization) : ID
} }
} }
private void DrawNpcCustomizationInfo(CustomizationSet set) private void DrawNpcCustomizationInfo(CustomizeSet set)
{ {
using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)"); using var tree = ImRaii.TreeNode($"{customize.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)");
if (!tree) if (!tree)
return; return;

View file

@ -154,7 +154,7 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
private void DrawCustomizeApplication() private void DrawCustomizeApplication()
{ {
var set = _selector.Selected!.CustomizationSet; var set = _selector.Selected!.CustomizeSet;
var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender; var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender;
var flags = _selector.Selected!.ApplyCustomize == 0 ? 0 : (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1; var flags = _selector.Selected!.ApplyCustomize == 0 ? 0 : (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1;
if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3)) if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3))

View file

@ -19,7 +19,7 @@ public class UnlockOverview
{ {
private readonly ItemManager _items; private readonly ItemManager _items;
private readonly ItemUnlockManager _itemUnlocks; private readonly ItemUnlockManager _itemUnlocks;
private readonly CustomizationService _customizations; private readonly CustomizeService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks; private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly PenumbraChangedItemTooltip _tooltip; private readonly PenumbraChangedItemTooltip _tooltip;
private readonly TextureService _textures; private readonly TextureService _textures;
@ -52,11 +52,9 @@ public class UnlockOverview
} }
} }
foreach (var clan in _customizations.Service.Clans) foreach (var (clan, gender) in CustomizeManager.AllSets())
{ {
foreach (var gender in _customizations.Service.Genders) if (_customizations.Manager.GetSet(clan, gender).HairStyles.Count == 0)
{
if (_customizations.Service.GetList(clan, gender).HairStyles.Count == 0)
continue; continue;
if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint", if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint",
@ -68,9 +66,8 @@ public class UnlockOverview
} }
} }
} }
}
public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks, public UnlockOverview(ItemManager items, CustomizeService customizations, ItemUnlockManager itemUnlocks,
CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureService textures, CodeService codes, CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureService textures, CodeService codes,
JobService jobs, FavoriteManager favorites) JobService jobs, FavoriteManager favorites)
{ {
@ -107,7 +104,7 @@ public class UnlockOverview
private void DrawCustomizations() private void DrawCustomizations()
{ {
var set = _customizations.Service.GetList(_selected2, _selected3); var set = _customizations.Manager.GetSet(_selected2, _selected3);
var spacing = IconSpacing; var spacing = IconSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
@ -121,7 +118,7 @@ public class UnlockOverview
continue; continue;
var unlocked = _customizeUnlocks.IsUnlocked(customize, out var time); var unlocked = _customizeUnlocks.IsUnlocked(customize, out var time);
var icon = _customizations.Service.GetIcon(customize.IconId); var icon = _customizations.Manager.GetIcon(customize.IconId);
ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One, ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One,
unlocked || _codes.EnabledShirts ? Vector4.One : UnavailableTint); unlocked || _codes.EnabledShirts ? Vector4.One : UnavailableTint);

View file

@ -15,7 +15,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Interop; namespace Glamourer.Interop;
public class ImportService(CustomizationService _customizations, IDragDropManager _dragDropManager, ItemManager _items) public class ImportService(CustomizeService _customizations, IDragDropManager _dragDropManager, ItemManager _items)
{ {
public void CreateDatSource() public void CreateDatSource()
=> _dragDropManager.CreateImGuiSource("DatDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".dat"), m => => _dragDropManager.CreateImGuiSource("DatDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".dat"), m =>
@ -179,14 +179,14 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
if (input.BodyType.Value != 1) if (input.BodyType.Value != 1)
return false; return false;
var set = _customizations.Service.GetList(input.Clan, input.Gender); var set = _customizations.Manager.GetSet(input.Clan, input.Gender);
voice = set.Voices[0]; voice = set.Voices[0];
if (inputVoice.HasValue && !set.Voices.Contains(inputVoice.Value)) if (inputVoice.HasValue && !set.Voices.Contains(inputVoice.Value))
return false; return false;
foreach (var index in CustomizationExtensions.AllBasic) foreach (var index in CustomizationExtensions.AllBasic)
{ {
if (!CustomizationService.IsCustomizationValid(set, input.Face, index, input[index])) if (!CustomizeService.IsCustomizationValid(set, input.Face, index, input[index]))
return false; return false;
} }

View file

@ -1,7 +1,6 @@
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Plugin.Services;
using Glamourer.GameData; using Glamourer.GameData;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
@ -10,26 +9,18 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Services; namespace Glamourer.Services;
public sealed class CustomizationService( public sealed class CustomizeService(
ITextureProvider textures,
IDataManager gameData,
HumanModelList humanModels, HumanModelList humanModels,
IPluginLog log, NpcCustomizeSet npcCustomizeSet,
NpcCustomizeSet npcCustomizeSet) CustomizeManager manager)
: IAsyncService : IAsyncService
{ {
public readonly HumanModelList HumanModels = humanModels; public readonly HumanModelList HumanModels = humanModels;
public readonly CustomizeManager Manager = manager;
public readonly NpcCustomizeSet NpcCustomizeSet = npcCustomizeSet;
private ICustomizationManager? _service; public Task Awaiter { get; }
= Task.WhenAll(humanModels.Awaiter, manager.Awaiter, npcCustomizeSet.Awaiter);
private readonly Task<ICustomizationManager> _task = Task.WhenAll(humanModels.Awaiter, npcCustomizeSet.Awaiter)
.ContinueWith(_ => CustomizationManager.Create(textures, gameData, log, npcCustomizeSet));
public ICustomizationManager Service
=> _service ??= _task.Result;
public Task Awaiter
=> _task;
public (CustomizeArray NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(CustomizeArray oldValues, CustomizeArray newValues, public (CustomizeArray NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(CustomizeArray oldValues, CustomizeArray newValues,
CustomizeFlag applyWhich, bool allowUnknown) CustomizeFlag applyWhich, bool allowUnknown)
@ -51,7 +42,7 @@ public sealed class CustomizationService(
} }
var set = Service.GetList(ret.Clan, ret.Gender); var set = Manager.GetSet(ret.Clan, ret.Gender);
applyWhich = applyWhich.FixApplication(set); applyWhich = applyWhich.FixApplication(set);
foreach (var index in CustomizationExtensions.AllBasic) foreach (var index in CustomizationExtensions.AllBasic)
{ {
@ -79,69 +70,34 @@ public sealed class CustomizationService(
gender = Gender.Female; gender = Gender.Female;
if (gender == Gender.MaleNpc) if (gender == Gender.MaleNpc)
gender = Gender.Male; gender = Gender.Male;
return (gender, race) switch return Manager.GetSet(race, gender).Name;
{
(Gender.Male, SubRace.Midlander) => Service.GetName(CustomName.MidlanderM),
(Gender.Male, SubRace.Highlander) => Service.GetName(CustomName.HighlanderM),
(Gender.Male, SubRace.Wildwood) => Service.GetName(CustomName.WildwoodM),
(Gender.Male, SubRace.Duskwight) => Service.GetName(CustomName.DuskwightM),
(Gender.Male, SubRace.Plainsfolk) => Service.GetName(CustomName.PlainsfolkM),
(Gender.Male, SubRace.Dunesfolk) => Service.GetName(CustomName.DunesfolkM),
(Gender.Male, SubRace.SeekerOfTheSun) => Service.GetName(CustomName.SeekerOfTheSunM),
(Gender.Male, SubRace.KeeperOfTheMoon) => Service.GetName(CustomName.KeeperOfTheMoonM),
(Gender.Male, SubRace.Seawolf) => Service.GetName(CustomName.SeawolfM),
(Gender.Male, SubRace.Hellsguard) => Service.GetName(CustomName.HellsguardM),
(Gender.Male, SubRace.Raen) => Service.GetName(CustomName.RaenM),
(Gender.Male, SubRace.Xaela) => Service.GetName(CustomName.XaelaM),
(Gender.Male, SubRace.Helion) => Service.GetName(CustomName.HelionM),
(Gender.Male, SubRace.Lost) => Service.GetName(CustomName.LostM),
(Gender.Male, SubRace.Rava) => Service.GetName(CustomName.RavaM),
(Gender.Male, SubRace.Veena) => Service.GetName(CustomName.VeenaM),
(Gender.Female, SubRace.Midlander) => Service.GetName(CustomName.MidlanderF),
(Gender.Female, SubRace.Highlander) => Service.GetName(CustomName.HighlanderF),
(Gender.Female, SubRace.Wildwood) => Service.GetName(CustomName.WildwoodF),
(Gender.Female, SubRace.Duskwight) => Service.GetName(CustomName.DuskwightF),
(Gender.Female, SubRace.Plainsfolk) => Service.GetName(CustomName.PlainsfolkF),
(Gender.Female, SubRace.Dunesfolk) => Service.GetName(CustomName.DunesfolkF),
(Gender.Female, SubRace.SeekerOfTheSun) => Service.GetName(CustomName.SeekerOfTheSunF),
(Gender.Female, SubRace.KeeperOfTheMoon) => Service.GetName(CustomName.KeeperOfTheMoonF),
(Gender.Female, SubRace.Seawolf) => Service.GetName(CustomName.SeawolfF),
(Gender.Female, SubRace.Hellsguard) => Service.GetName(CustomName.HellsguardF),
(Gender.Female, SubRace.Raen) => Service.GetName(CustomName.RaenF),
(Gender.Female, SubRace.Xaela) => Service.GetName(CustomName.XaelaF),
(Gender.Female, SubRace.Helion) => Service.GetName(CustomName.HelionM),
(Gender.Female, SubRace.Lost) => Service.GetName(CustomName.LostM),
(Gender.Female, SubRace.Rava) => Service.GetName(CustomName.RavaF),
(Gender.Female, SubRace.Veena) => Service.GetName(CustomName.VeenaF),
_ => "Unknown",
};
} }
/// <summary> Returns whether a clan is valid. </summary> /// <summary> Returns whether a clan is valid. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsClanValid(SubRace clan) public bool IsClanValid(SubRace clan)
=> Service.Clans.Contains(clan); => CustomizeManager.Clans.Contains(clan);
/// <summary> Returns whether a gender is valid for the given race. </summary> /// <summary> Returns whether a gender is valid for the given race. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsGenderValid(Race race, Gender gender) public bool IsGenderValid(Race race, Gender gender)
=> race is Race.Hrothgar ? gender == Gender.Male : Service.Genders.Contains(gender); => race is Race.Hrothgar ? gender == Gender.Male : CustomizeManager.Genders.Contains(gender);
/// <inheritdoc cref="IsCustomizationValid(CustomizationSet,CustomizeValue,CustomizeIndex,CustomizeValue, out CustomizeData?)"/> /// <inheritdoc cref="IsCustomizationValid(CustomizeSet,CustomizeValue,CustomizeIndex,CustomizeValue, out CustomizeData?)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value) public static bool IsCustomizationValid(CustomizeSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value)
=> IsCustomizationValid(set, face, type, value, out _); => IsCustomizationValid(set, face, type, value, out _);
/// <summary> Returns whether a customization value is valid for a given clan/gender set and face. </summary> /// <summary> Returns whether a customization value is valid for a given clan/gender set and face. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value, public static bool IsCustomizationValid(CustomizeSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value,
out CustomizeData? data) out CustomizeData? data)
=> set.Validate(type, value, out data, face); => set.Validate(type, value, out data, face);
/// <summary> Returns whether a customization value is valid for a given clan, gender and face. </summary> /// <summary> Returns whether a customization value is valid for a given clan, gender and face. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsCustomizationValid(SubRace race, Gender gender, CustomizeValue face, CustomizeIndex type, CustomizeValue value) public bool IsCustomizationValid(SubRace race, Gender gender, CustomizeValue face, CustomizeIndex type, CustomizeValue value)
=> IsCustomizationValid(Service.GetList(race, gender), face, type, value); => IsCustomizationValid(Manager.GetSet(race, gender), face, type, value);
/// <summary> /// <summary>
/// Check that the given race and clan are valid. /// Check that the given race and clan are valid.
@ -160,10 +116,10 @@ public sealed class CustomizationService(
return string.Empty; return string.Empty;
} }
if (Service.Races.Contains(race)) if (CustomizeManager.Races.Contains(race))
{ {
actualRace = race; actualRace = race;
actualClan = Service.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown); actualClan = CustomizeManager.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown);
// This should not happen. // This should not happen.
if (actualClan == SubRace.Unknown) if (actualClan == SubRace.Unknown)
{ {
@ -189,7 +145,7 @@ public sealed class CustomizationService(
/// </summary> /// </summary>
public string ValidateGender(Race race, Gender gender, out Gender actualGender) public string ValidateGender(Race race, Gender gender, out Gender actualGender)
{ {
if (!Service.Genders.Contains(gender)) if (!CustomizeManager.Genders.Contains(gender))
{ {
actualGender = Gender.Male; actualGender = Gender.Male;
return $"The gender {gender.ToName()} is unknown, reset to {Gender.Male.ToName()}."; return $"The gender {gender.ToName()} is unknown, reset to {Gender.Male.ToName()}.";
@ -230,7 +186,7 @@ public sealed class CustomizationService(
/// The returned actualValue is either the correct value or the one with index 0. /// The returned actualValue is either the correct value or the one with index 0.
/// The return value is an empty string or a warning message. /// The return value is an empty string or a warning message.
/// </summary> /// </summary>
public static string ValidateCustomizeValue(CustomizationSet set, CustomizeValue face, CustomizeIndex index, CustomizeValue value, public static string ValidateCustomizeValue(CustomizeSet set, CustomizeValue face, CustomizeIndex index, CustomizeValue value,
out CustomizeValue actualValue, bool allowUnknown) out CustomizeValue actualValue, bool allowUnknown)
{ {
if (allowUnknown || IsCustomizationValid(set, face, index, value)) if (allowUnknown || IsCustomizationValid(set, face, index, value))
@ -266,7 +222,7 @@ public sealed class CustomizationService(
flags |= CustomizeFlag.Gender; flags |= CustomizeFlag.Gender;
} }
var set = Service.GetList(customize.Clan, customize.Gender); var set = Manager.GetSet(customize.Clan, customize.Gender);
return FixValues(set, ref customize) | flags; return FixValues(set, ref customize) | flags;
} }
@ -284,11 +240,11 @@ public sealed class CustomizationService(
return 0; return 0;
customize.Gender = newGender; customize.Gender = newGender;
var set = Service.GetList(customize.Clan, customize.Gender); var set = Manager.GetSet(customize.Clan, customize.Gender);
return FixValues(set, ref customize) | CustomizeFlag.Gender; return FixValues(set, ref customize) | CustomizeFlag.Gender;
} }
private static CustomizeFlag FixValues(CustomizationSet set, ref CustomizeArray customize) private static CustomizeFlag FixValues(CustomizeSet set, ref CustomizeArray customize)
{ {
CustomizeFlag flags = 0; CustomizeFlag flags = 0;
foreach (var idx in CustomizationExtensions.AllBasic) foreach (var idx in CustomizationExtensions.AllBasic)

View file

@ -84,7 +84,7 @@ public static class ServiceManagerA
=> services.AddSingleton<ObjectIdentification>() => services.AddSingleton<ObjectIdentification>()
.AddSingleton<ItemData>() .AddSingleton<ItemData>()
.AddSingleton<ActorManager>() .AddSingleton<ActorManager>()
.AddSingleton<CustomizationService>() .AddSingleton<CustomizeService>()
.AddSingleton<ItemManager>() .AddSingleton<ItemManager>()
.AddSingleton<GamePathParser>() .AddSingleton<GamePathParser>()
.AddSingleton<HumanModelList>(); .AddSingleton<HumanModelList>();

View file

@ -27,7 +27,7 @@ public unsafe class FunModule : IDisposable
private readonly WorldSets _worldSets = new(); private readonly WorldSets _worldSets = new();
private readonly ItemManager _items; private readonly ItemManager _items;
private readonly CustomizationService _customizations; private readonly CustomizeService _customizations;
private readonly Configuration _config; private readonly Configuration _config;
private readonly CodeService _codes; private readonly CodeService _codes;
private readonly Random _rng; private readonly Random _rng;
@ -67,7 +67,7 @@ public unsafe class FunModule : IDisposable
internal void ResetFestival() internal void ResetFestival()
=> OnDayChange(DateTime.Now.Day, DateTime.Now.Month, DateTime.Now.Year); => OnDayChange(DateTime.Now.Day, DateTime.Now.Month, DateTime.Now.Year);
public FunModule(CodeService codes, CustomizationService customizations, ItemManager items, Configuration config, public FunModule(CodeService codes, CustomizeService customizations, ItemManager items, Configuration config,
GenericPopupWindow popupWindow, StateManager stateManager, ObjectManager objects, DesignConverter designConverter, GenericPopupWindow popupWindow, StateManager stateManager, ObjectManager objects, DesignConverter designConverter,
DesignManager designManager) DesignManager designManager)
{ {
@ -197,7 +197,7 @@ public unsafe class FunModule : IDisposable
if (!_codes.EnabledIndividual) if (!_codes.EnabledIndividual)
return; return;
var set = _customizations.Service.GetList(customize.Clan, customize.Gender); var set = _customizations.Manager.GetSet(customize.Clan, customize.Gender);
foreach (var index in Enum.GetValues<CustomizeIndex>()) foreach (var index in Enum.GetValues<CustomizeIndex>())
{ {
if (index is CustomizeIndex.Face || !set.IsAvailable(index)) if (index is CustomizeIndex.Face || !set.IsAvailable(index))

View file

@ -12,12 +12,12 @@ namespace Glamourer.State;
public class StateEditor public class StateEditor
{ {
private readonly ItemManager _items; private readonly ItemManager _items;
private readonly CustomizationService _customizations; private readonly CustomizeService _customizations;
private readonly HumanModelList _humans; private readonly HumanModelList _humans;
private readonly GPoseService _gPose; private readonly GPoseService _gPose;
private readonly ICondition _condition; private readonly ICondition _condition;
public StateEditor(CustomizationService customizations, HumanModelList humans, ItemManager items, GPoseService gPose, ICondition condition) public StateEditor(CustomizeService customizations, HumanModelList humans, ItemManager items, GPoseService gPose, ICondition condition)
{ {
_customizations = customizations; _customizations = customizations;
_humans = humans; _humans = humans;
@ -72,7 +72,7 @@ public class StateEditor
state[CustomizeIndex.Clan] = source; state[CustomizeIndex.Clan] = source;
state[CustomizeIndex.Gender] = source; state[CustomizeIndex.Gender] = source;
var set = _customizations.Service.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender); var set = _customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender);
foreach (var index in Enum.GetValues<CustomizeIndex>().Where(set.IsAvailable)) foreach (var index in Enum.GetValues<CustomizeIndex>().Where(set.IsAvailable))
state[index] = source; state[index] = source;
} }

View file

@ -28,7 +28,7 @@ public class StateListener : IDisposable
private readonly StateManager _manager; private readonly StateManager _manager;
private readonly StateApplier _applier; private readonly StateApplier _applier;
private readonly ItemManager _items; private readonly ItemManager _items;
private readonly CustomizationService _customizations; private readonly CustomizeService _customizations;
private readonly PenumbraService _penumbra; private readonly PenumbraService _penumbra;
private readonly SlotUpdating _slotUpdating; private readonly SlotUpdating _slotUpdating;
private readonly WeaponLoading _weaponLoading; private readonly WeaponLoading _weaponLoading;
@ -52,7 +52,7 @@ public class StateListener : IDisposable
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility, SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans,
StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose,
ChangeCustomizeService changeCustomizeService, CustomizationService customizations, ICondition condition, CrestService crestService) ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, CrestService crestService)
{ {
_manager = manager; _manager = manager;
_items = items; _items = items;
@ -167,7 +167,7 @@ public class StateListener : IDisposable
return; return;
} }
var set = _customizations.Service.GetList(model.Clan, model.Gender); var set = _customizations.Manager.GetSet(model.Clan, model.Gender);
foreach (var index in CustomizationExtensions.AllBasic) foreach (var index in CustomizationExtensions.AllBasic)
{ {
if (state[index] is not StateChanged.Source.Fixed) if (state[index] is not StateChanged.Source.Fixed)

View file

@ -29,7 +29,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable
public IReadOnlyDictionary<uint, long> Unlocked public IReadOnlyDictionary<uint, long> Unlocked
=> _unlocked; => _unlocked;
public CustomizeUnlockManager(SaveService saveService, CustomizationService customizations, IDataManager gameData, public CustomizeUnlockManager(SaveService saveService, CustomizeService customizations, IDataManager gameData,
IClientState clientState, ObjectUnlocked @event, IGameInteropProvider interop) IClientState clientState, ObjectUnlocked @event, IGameInteropProvider interop)
{ {
interop.InitializeFromAttributes(this); interop.InitializeFromAttributes(this);
@ -174,16 +174,14 @@ public class CustomizeUnlockManager : IDisposable, ISavable
"customization"); "customization");
/// <summary> Create a list of all unlockable hairstyles and face paints. </summary> /// <summary> Create a list of all unlockable hairstyles and face paints. </summary>
private static Dictionary<CustomizeData, (uint Data, string Name)> CreateUnlockableCustomizations(CustomizationService customizations, private static Dictionary<CustomizeData, (uint Data, string Name)> CreateUnlockableCustomizations(CustomizeService customizations,
IDataManager gameData) IDataManager gameData)
{ {
var ret = new Dictionary<CustomizeData, (uint Data, string Name)>(); var ret = new Dictionary<CustomizeData, (uint Data, string Name)>();
var sheet = gameData.GetExcelSheet<CharaMakeCustomize>(ClientLanguage.English)!; var sheet = gameData.GetExcelSheet<CharaMakeCustomize>(ClientLanguage.English)!;
foreach (var clan in customizations.Service.Clans) foreach (var (clan, gender) in CustomizeManager.AllSets())
{ {
foreach (var gender in customizations.Service.Genders) var list = customizations.Manager.GetSet(clan, gender);
{
var list = customizations.Service.GetList(clan, gender);
foreach (var hair in list.HairStyles) foreach (var hair in list.HairStyles)
{ {
var x = sheet.FirstOrDefault(f => f.FeatureID == hair.Value.Value); var x = sheet.FirstOrDefault(f => f.FeatureID == hair.Value.Value);
@ -208,7 +206,6 @@ public class CustomizeUnlockManager : IDisposable, ISavable
} }
} }
} }
}
return ret; return ret;
} }

@ -1 +1 @@
Subproject commit 197d23eee167c232000f22ef40a7a2bded913b6c Subproject commit 4404d62b7442daa7e3436dc417364905e3d5cd2f