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 EquippedGearset _equippedGearset;
private readonly ActorManager _actors;
private readonly CustomizationService _customizations;
private readonly CustomizeService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly ItemUnlockManager _itemUnlocks;
private readonly AutomationChanged _event;
@ -48,7 +48,7 @@ public class AutoDesignApplier : IDisposable
}
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,
EquippedGearset equippedGearset)
{
@ -468,7 +468,7 @@ public class AutoDesignApplier : IDisposable
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;
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
@ -477,7 +477,7 @@ public class AutoDesignApplier : IDisposable
continue;
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 _))
continue;

View file

@ -15,7 +15,7 @@ public sealed class Design : DesignBase, ISavable
{
#region Data
internal Design(CustomizationService customize, ItemManager items)
internal Design(CustomizeService customize, ItemManager items)
: base(customize, items)
{ }
@ -98,7 +98,7 @@ public sealed class Design : DesignBase, ISavable
#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;
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)
{

View file

@ -25,35 +25,35 @@ public class DesignBase
public ref DesignData GetDesignDataRef()
=> ref _designData;
internal DesignBase(CustomizationService customize, ItemManager items)
internal DesignBase(CustomizeService customize, ItemManager 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;
ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
ApplyEquip = equipFlags & EquipFlagExtensions.All;
_designFlags = 0;
CustomizationSet = SetCustomizationSet(customize);
CustomizeSet = SetCustomizationSet(customize);
}
internal DesignBase(DesignBase clone)
{
_designData = clone._designData;
CustomizationSet = clone.CustomizationSet;
CustomizeSet = clone.CustomizeSet;
ApplyCustomize = clone.ApplyCustomizeRaw;
ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
_designFlags = clone._designFlags & (DesignFlags)0x0F;
}
/// <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;
CustomizationSet = SetCustomizationSet(customize);
CustomizeSet = SetCustomizationSet(customize);
}
#region Application Data
@ -69,11 +69,11 @@ public class DesignBase
}
private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant;
public CustomizationSet CustomizationSet { get; private set; }
public CustomizeSet CustomizeSet { get; private set; }
internal CustomizeFlag ApplyCustomize
{
get => _applyCustomize.FixApplication(CustomizationSet);
get => _applyCustomize.FixApplication(CustomizeSet);
set => _applyCustomize = value & CustomizeFlagExtensions.AllRelevant;
}
@ -84,13 +84,13 @@ public class DesignBase
internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant;
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))
return false;
_designData.Customize = customize;
CustomizationSet = customizationService.Service.GetList(customize.Clan, customize.Gender);
CustomizeSet = customizeService.Manager.GetSet(customize.Clan, customize.Gender);
return true;
}
@ -240,10 +240,10 @@ public class DesignBase
}
}
private CustomizationSet SetCustomizationSet(CustomizationService customize)
private CustomizeSet SetCustomizationSet(CustomizeService customize)
=> !_designData.IsHuman
? customize.Service.GetList(SubRace.Midlander, Gender.Male)
: customize.Service.GetList(_designData.Customize.Clan, _designData.Customize.Gender);
? customize.Manager.GetSet(SubRace.Midlander, Gender.Male)
: customize.Manager.GetSet(_designData.Customize.Clan, _designData.Customize.Gender);
#endregion
@ -330,7 +330,7 @@ public class DesignBase
#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;
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);
LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true);
@ -435,7 +435,7 @@ public class DesignBase
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)
{
if (json == null)
@ -473,7 +473,7 @@ public class DesignBase
{
var arrayText = json["Array"]?.ToObject<string>() ?? string.Empty;
design._designData.Customize.LoadBase64(arrayText);
design.CustomizationSet = design.SetCustomizationSet(customizations);
design.CustomizeSet = design.SetCustomizationSet(customizations);
return;
}
@ -485,18 +485,18 @@ public class DesignBase
design._designData.Customize.Race = race;
design._designData.Customize.Clan = clan;
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.Clan, json[CustomizeIndex.Clan.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)
{
var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
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));
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
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
{
@ -518,7 +518,7 @@ public class DesignBase
SetApplyVisorToggle(applyVisor);
SetApplyWeaponVisible(applyWeapon);
SetApplyWetness(true);
CustomizationSet = SetCustomizationSet(customize);
CustomizeSet = SetCustomizationSet(customize);
}
catch (Exception ex)
{

View file

@ -13,7 +13,7 @@ using Penumbra.GameData.Structs;
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;

View file

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

View file

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

View file

@ -6,10 +6,12 @@ using Penumbra.String.Functions;
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>
{
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)
=> _rgbaColors.AsSpan(offset, count);
@ -18,6 +20,7 @@ public class ColorParameters : IReadOnlyList<uint>
try
{
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];
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"
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
_rgbaColors = Array.Empty<uint>();
_rgbaColors = [];
}
}
/// <inheritdoc/>
public IEnumerator<uint> GetEnumerator()
=> (IEnumerator<uint>)_rgbaColors.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public int Count
=> _rgbaColors.Length;
/// <inheritdoc/>
public uint this[int 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;
// 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>
/// 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)]
public readonly struct CustomizeData : IEquatable<CustomizeData>
{
/// <summary> The index of the option this value is for. </summary>
[FieldOffset(0)]
public readonly CustomizeIndex Index;
/// <summary> The value for the option. </summary>
[FieldOffset(1)]
public readonly CustomizeValue Value;
/// <summary> The internal ID for sheets. </summary>
[FieldOffset(2)]
public readonly ushort CustomizeId;
/// <summary> An ID for an associated icon. </summary>
[FieldOffset(4)]
public readonly uint IconId;
/// <summary> An ID for an associated color. </summary>
[FieldOffset(4)]
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)
{
Index = index;
@ -34,14 +42,23 @@ public readonly struct CustomizeData : IEquatable<CustomizeData>
CustomizeId = customizeId;
}
/// <inheritdoc/>
public bool Equals(CustomizeData other)
=> Index == other.Index
&& Value.Value == other.Value.Value
&& CustomizeId == other.CustomizeId;
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is CustomizeData other && Equals(other);
/// <inheritdoc/>
public override int GetHashCode()
=> 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;
// Each Subrace and Gender combo has a customization set.
// This describes the available customizations, their types and their names.
public class CustomizationSet
/// <summary>
/// Each SubRace and Gender combo has a customization set.
/// 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;
Clan = clan;
@ -24,6 +26,8 @@ public class CustomizationSet
public SubRace Clan { get; }
public Race Race { get; }
public string Name { get; internal init; } = string.Empty;
public CustomizeFlag SettingAvailable { get; internal set; }
internal void SetAvailable(CustomizeIndex index)
@ -33,7 +37,7 @@ public class CustomizationSet
=> SettingAvailable.HasFlag(index.ToFlag());
// Meta
public IReadOnlyList<string> OptionName { get; internal set; } = null!;
public IReadOnlyList<string> OptionName { get; internal init; } = null!;
public string Option(CustomizeIndex index)
=> OptionName[(int)index];
@ -95,68 +99,6 @@ public class CustomizationSet
{
var type = Types[(int)index];
int GetInteger0(out CustomizeData? custom)
{
if (value < Count(index))
{
custom = new CustomizeData(index, value, 0, value.Value);
return value.Value;
}
custom = null;
return -1;
}
int GetInteger1(out CustomizeData? custom)
{
if (value > 0 && value < Count(index) + 1)
{
custom = new CustomizeData(index, value, 0, (ushort)(value.Value - 1));
return value.Value;
}
custom = null;
return -1;
}
static int GetBool(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom)
{
if (value == CustomizeValue.Zero)
{
custom = new CustomizeData(index, CustomizeValue.Zero, 0, 0);
return 0;
}
var (_, mask) = index.ToByteAndMask();
if (value.Value == mask)
{
custom = new CustomizeData(index, new CustomizeValue(mask), 0, 1);
return 1;
}
custom = null;
return -1;
}
static int Invalid(out CustomizeData? custom)
{
custom = null;
return -1;
}
int Get(IEnumerable<CustomizeData> list, CustomizeValue v, out CustomizeData? output)
{
var (val, idx) = list.Cast<CustomizeData?>().WithIndex().FirstOrDefault(p => p.Item1!.Value.Value == v);
if (val == null)
{
output = null;
return -1;
}
output = val;
return idx;
}
return type switch
{
CharaMakeParams.MenuType.ListSelector => GetInteger0(out custom),
@ -194,6 +136,68 @@ public class CustomizationSet
CharaMakeParams.MenuType.Checkmark => GetBool(index, value, 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)]
@ -244,7 +248,7 @@ public class CustomizationSet
public CharaMakeParams.MenuType Type(CustomizeIndex 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();
ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft;
@ -305,6 +309,6 @@ public class CustomizationSet
public static class CustomizationSetExtensions
{
/// <summary> Return only the available customizations in this set and Clan or Gender. </summary>
public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizationSet set)
public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizeSet set)
=> 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;
/// <summary> Contains a set of all human NPC appearances with their names. </summary>
public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
{
/// <inheritdoc/>
public string Name
=> 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; }
/// <inheritdoc/>
public int TotalCount
=> _data.Count;
/// <inheritdoc/>
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)
{
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)
{
var enpcSheet = data.GetExcelSheet<ENpcBase>()!;
var list = new List<NpcData>(eNpcs.Count);
// Go through all event NPCs already collected into a dictionary.
foreach (var (id, name) in eNpcs)
{
var row = enpcSheet.GetRow(id.Id);
// We only accept NPCs with valid names.
if (row == null || name.IsNullOrWhitespace())
continue;
// Check if the customization is a valid human.
var (valid, customize) = FromEnpcBase(row);
if (!valid)
continue;
@ -63,6 +77,8 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
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)
{
ApplyNpcEquip(ref ret, equip);
@ -90,19 +106,25 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
return list;
}
/// <summary> Create data from battle NPCs. </summary>
private static List<NpcData> CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{
var bnpcSheet = data.GetExcelSheet<BNpcBase>()!;
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)
{
// Only accept humans.
if (baseRow.ModelChara.Value!.Type != 1)
continue;
var bnpcNameIds = bNpcNames[baseRow.RowId];
// Only accept battle NPCs with known associated names.
if (bnpcNameIds.Count == 0)
continue;
// Check if the customization is a valid human.
var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!);
if (!valid)
continue;
@ -115,6 +137,7 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
Kind = ObjectKind.BattleNpc,
};
ApplyNpcEquip(ref ret, equip);
// Add the appearance for each associated name.
foreach (var bnpcNameId in bnpcNameIds)
{
if (bNpcs.TryGetValue(bnpcNameId.Id, out var name) && !name.IsNullOrWhitespace())
@ -125,13 +148,18 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
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();
// This is a maximum since we deduplicate.
_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());
// Iterate through the sorted list.
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)
{
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)
{
_data.Add(duplicates[0]);
@ -152,24 +181,29 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
}
else
{
// Add all distinct duplicates with their ID specified in the name.
_data.AddRange(duplicates
.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);
}
}
// Sort non-alphanumeric entries at the end instead of the beginning.
var lastWeird = _data.FindIndex(d => char.IsAsciiLetterOrDigit(d.Name[0]));
if (lastWeird != -1)
{
_data.AddRange(_data.Take(lastWeird));
_data.RemoveRange(0, lastWeird);
}
// Reduce memory footprint.
_data.TrimExcess();
}
/// <summary> Apply equipment from a NpcEquip row. </summary>
private static void ApplyNpcEquip(ref NpcData data, NpcEquip row)
{
data.Set(0, row.ModelHead | (row.DyeHead.Row << 24));
@ -187,96 +221,102 @@ public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
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)
{
var customize = new CustomizeArray();
customize.SetByIndex(0, (CustomizeValue) (byte)bnpcCustomize.Race.Row);
customize.SetByIndex(1, (CustomizeValue) bnpcCustomize.Gender);
customize.SetByIndex(2, (CustomizeValue) bnpcCustomize.BodyType);
customize.SetByIndex(3, (CustomizeValue) bnpcCustomize.Height);
customize.SetByIndex(4, (CustomizeValue) (byte)bnpcCustomize.Tribe.Row);
customize.SetByIndex(5, (CustomizeValue) bnpcCustomize.Face);
customize.SetByIndex(6, (CustomizeValue) bnpcCustomize.HairStyle);
customize.SetByIndex(7, (CustomizeValue) bnpcCustomize.HairHighlight);
customize.SetByIndex(8, (CustomizeValue) bnpcCustomize.SkinColor);
customize.SetByIndex(9, (CustomizeValue) bnpcCustomize.EyeHeterochromia);
customize.SetByIndex(10, (CustomizeValue) bnpcCustomize.HairColor);
customize.SetByIndex(11, (CustomizeValue) bnpcCustomize.HairHighlightColor);
customize.SetByIndex(12, (CustomizeValue) bnpcCustomize.FacialFeature);
customize.SetByIndex(13, (CustomizeValue) bnpcCustomize.FacialFeatureColor);
customize.SetByIndex(14, (CustomizeValue) bnpcCustomize.Eyebrows);
customize.SetByIndex(15, (CustomizeValue) bnpcCustomize.EyeColor);
customize.SetByIndex(16, (CustomizeValue) bnpcCustomize.EyeShape);
customize.SetByIndex(17, (CustomizeValue) bnpcCustomize.Nose);
customize.SetByIndex(18, (CustomizeValue) bnpcCustomize.Jaw);
customize.SetByIndex(19, (CustomizeValue) bnpcCustomize.Mouth);
customize.SetByIndex(20, (CustomizeValue) bnpcCustomize.LipColor);
customize.SetByIndex(21, (CustomizeValue) bnpcCustomize.BustOrTone1);
customize.SetByIndex(22, (CustomizeValue) bnpcCustomize.ExtraFeature1);
customize.SetByIndex(23, (CustomizeValue) bnpcCustomize.ExtraFeature2OrBust);
customize.SetByIndex(24, (CustomizeValue) bnpcCustomize.FacePaint);
customize.SetByIndex(25, (CustomizeValue) bnpcCustomize.FacePaintColor);
customize.SetByIndex(0, (CustomizeValue)(byte)bnpcCustomize.Race.Row);
customize.SetByIndex(1, (CustomizeValue)bnpcCustomize.Gender);
customize.SetByIndex(2, (CustomizeValue)bnpcCustomize.BodyType);
customize.SetByIndex(3, (CustomizeValue)bnpcCustomize.Height);
customize.SetByIndex(4, (CustomizeValue)(byte)bnpcCustomize.Tribe.Row);
customize.SetByIndex(5, (CustomizeValue)bnpcCustomize.Face);
customize.SetByIndex(6, (CustomizeValue)bnpcCustomize.HairStyle);
customize.SetByIndex(7, (CustomizeValue)bnpcCustomize.HairHighlight);
customize.SetByIndex(8, (CustomizeValue)bnpcCustomize.SkinColor);
customize.SetByIndex(9, (CustomizeValue)bnpcCustomize.EyeHeterochromia);
customize.SetByIndex(10, (CustomizeValue)bnpcCustomize.HairColor);
customize.SetByIndex(11, (CustomizeValue)bnpcCustomize.HairHighlightColor);
customize.SetByIndex(12, (CustomizeValue)bnpcCustomize.FacialFeature);
customize.SetByIndex(13, (CustomizeValue)bnpcCustomize.FacialFeatureColor);
customize.SetByIndex(14, (CustomizeValue)bnpcCustomize.Eyebrows);
customize.SetByIndex(15, (CustomizeValue)bnpcCustomize.EyeColor);
customize.SetByIndex(16, (CustomizeValue)bnpcCustomize.EyeShape);
customize.SetByIndex(17, (CustomizeValue)bnpcCustomize.Nose);
customize.SetByIndex(18, (CustomizeValue)bnpcCustomize.Jaw);
customize.SetByIndex(19, (CustomizeValue)bnpcCustomize.Mouth);
customize.SetByIndex(20, (CustomizeValue)bnpcCustomize.LipColor);
customize.SetByIndex(21, (CustomizeValue)bnpcCustomize.BustOrTone1);
customize.SetByIndex(22, (CustomizeValue)bnpcCustomize.ExtraFeature1);
customize.SetByIndex(23, (CustomizeValue)bnpcCustomize.ExtraFeature2OrBust);
customize.SetByIndex(24, (CustomizeValue)bnpcCustomize.FacePaint);
customize.SetByIndex(25, (CustomizeValue)bnpcCustomize.FacePaintColor);
if (customize.BodyType.Value != 1
|| !CustomizationOptions.Races.Contains(customize.Race)
|| !CustomizationOptions.Clans.Contains(customize.Clan)
|| !CustomizationOptions.Genders.Contains(customize.Gender))
|| !CustomizeManager.Races.Contains(customize.Race)
|| !CustomizeManager.Clans.Contains(customize.Clan)
|| !CustomizeManager.Genders.Contains(customize.Gender))
return (false, CustomizeArray.Default);
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)
{
if (enpcBase.ModelChara.Value?.Type != 1)
return (false, CustomizeArray.Default);
var customize = new CustomizeArray();
customize.SetByIndex(0, (CustomizeValue) (byte)enpcBase.Race.Row);
customize.SetByIndex(1, (CustomizeValue) enpcBase.Gender);
customize.SetByIndex(2, (CustomizeValue) enpcBase.BodyType);
customize.SetByIndex(3, (CustomizeValue) enpcBase.Height);
customize.SetByIndex(4, (CustomizeValue) (byte)enpcBase.Tribe.Row);
customize.SetByIndex(5, (CustomizeValue) enpcBase.Face);
customize.SetByIndex(6, (CustomizeValue) enpcBase.HairStyle);
customize.SetByIndex(7, (CustomizeValue) enpcBase.HairHighlight);
customize.SetByIndex(8, (CustomizeValue) enpcBase.SkinColor);
customize.SetByIndex(9, (CustomizeValue) enpcBase.EyeHeterochromia);
customize.SetByIndex(10, (CustomizeValue) enpcBase.HairColor);
customize.SetByIndex(11, (CustomizeValue) enpcBase.HairHighlightColor);
customize.SetByIndex(12, (CustomizeValue) enpcBase.FacialFeature);
customize.SetByIndex(13, (CustomizeValue) enpcBase.FacialFeatureColor);
customize.SetByIndex(14, (CustomizeValue) enpcBase.Eyebrows);
customize.SetByIndex(15, (CustomizeValue) enpcBase.EyeColor);
customize.SetByIndex(16, (CustomizeValue) enpcBase.EyeShape);
customize.SetByIndex(17, (CustomizeValue) enpcBase.Nose);
customize.SetByIndex(18, (CustomizeValue) enpcBase.Jaw);
customize.SetByIndex(19, (CustomizeValue) enpcBase.Mouth);
customize.SetByIndex(20, (CustomizeValue) enpcBase.LipColor);
customize.SetByIndex(21, (CustomizeValue) enpcBase.BustOrTone1);
customize.SetByIndex(22, (CustomizeValue) enpcBase.ExtraFeature1);
customize.SetByIndex(23, (CustomizeValue) enpcBase.ExtraFeature2OrBust);
customize.SetByIndex(24, (CustomizeValue) enpcBase.FacePaint);
customize.SetByIndex(25, (CustomizeValue) enpcBase.FacePaintColor);
customize.SetByIndex(0, (CustomizeValue)(byte)enpcBase.Race.Row);
customize.SetByIndex(1, (CustomizeValue)enpcBase.Gender);
customize.SetByIndex(2, (CustomizeValue)enpcBase.BodyType);
customize.SetByIndex(3, (CustomizeValue)enpcBase.Height);
customize.SetByIndex(4, (CustomizeValue)(byte)enpcBase.Tribe.Row);
customize.SetByIndex(5, (CustomizeValue)enpcBase.Face);
customize.SetByIndex(6, (CustomizeValue)enpcBase.HairStyle);
customize.SetByIndex(7, (CustomizeValue)enpcBase.HairHighlight);
customize.SetByIndex(8, (CustomizeValue)enpcBase.SkinColor);
customize.SetByIndex(9, (CustomizeValue)enpcBase.EyeHeterochromia);
customize.SetByIndex(10, (CustomizeValue)enpcBase.HairColor);
customize.SetByIndex(11, (CustomizeValue)enpcBase.HairHighlightColor);
customize.SetByIndex(12, (CustomizeValue)enpcBase.FacialFeature);
customize.SetByIndex(13, (CustomizeValue)enpcBase.FacialFeatureColor);
customize.SetByIndex(14, (CustomizeValue)enpcBase.Eyebrows);
customize.SetByIndex(15, (CustomizeValue)enpcBase.EyeColor);
customize.SetByIndex(16, (CustomizeValue)enpcBase.EyeShape);
customize.SetByIndex(17, (CustomizeValue)enpcBase.Nose);
customize.SetByIndex(18, (CustomizeValue)enpcBase.Jaw);
customize.SetByIndex(19, (CustomizeValue)enpcBase.Mouth);
customize.SetByIndex(20, (CustomizeValue)enpcBase.LipColor);
customize.SetByIndex(21, (CustomizeValue)enpcBase.BustOrTone1);
customize.SetByIndex(22, (CustomizeValue)enpcBase.ExtraFeature1);
customize.SetByIndex(23, (CustomizeValue)enpcBase.ExtraFeature2OrBust);
customize.SetByIndex(24, (CustomizeValue)enpcBase.FacePaint);
customize.SetByIndex(25, (CustomizeValue)enpcBase.FacePaintColor);
if (customize.BodyType.Value != 1
|| !CustomizationOptions.Races.Contains(customize.Race)
|| !CustomizationOptions.Clans.Contains(customize.Clan)
|| !CustomizationOptions.Genders.Contains(customize.Gender))
|| !CustomizeManager.Races.Contains(customize.Race)
|| !CustomizeManager.Clans.Contains(customize.Clan)
|| !CustomizeManager.Genders.Contains(customize.Gender))
return (false, CustomizeArray.Default);
return (true, customize);
}
/// <inheritdoc/>
public IEnumerator<NpcData> GetEnumerator()
=> _data.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public int Count
=> _data.Count;
/// <inheritdoc/>
public NpcData this[int index]
=> _data[index];
}

View file

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

View file

@ -29,7 +29,7 @@ public partial class CustomizationDrawer
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))
{
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
@ -69,7 +69,7 @@ public partial class CustomizationDrawer
for (var i = 0; i < _currentCount; ++i)
{
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 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 feature = _set.Data(featureIdx, 0, face);
var icon = featureIdx == CustomizeIndex.LegacyTattoo
? _legacyTattoo ?? _service.Service.GetIcon(feature.IconId)
: _service.Service.GetIcon(feature.IconId);
? _legacyTattoo ?? _service.Manager.GetIcon(feature.IconId)
: _service.Manager.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X,
Vector4.Zero, enabled ? Vector4.One : _redTint))
{

View file

@ -14,7 +14,7 @@ using Penumbra.GameData.Structs;
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
{
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 CustomizeArray _customize = CustomizeArray.Default;
private CustomizationSet _set = null!;
private CustomizeSet _set = null!;
public CustomizeArray Customize
=> _customize;
@ -117,7 +117,7 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
return DrawArtisan();
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])
PercentageSelector(id);

View file

@ -180,7 +180,7 @@ public sealed class RevertDesignCombo : DesignComboBase, IDisposable
private readonly AutoDesignManager _autoDesignManager;
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)
: this(designs, fileSystem, tabSelected, designColors, CreateRevertDesign(customize, items), log, designChanged, autoDesignManager,
config)
@ -210,7 +210,7 @@ public sealed class RevertDesignCombo : DesignComboBase, IDisposable
_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)
{
Index = RevertDesignIndex,

View file

@ -1,4 +1,5 @@
using System.Numerics;
using System;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
@ -61,7 +62,7 @@ public class GenericPopupWindow : Window
private void DrawFestivalPopup()
{
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));
using var popup = ImRaii.Popup("FestivalPopup", ImGuiWindowFlags.Modal);
if (!popup)

View file

@ -27,7 +27,7 @@ public class SetPanel(
ItemUnlockManager _itemUnlocks,
RevertDesignCombo _designCombo,
CustomizeUnlockManager _customizeUnlocks,
CustomizationService _customizations,
CustomizeService _customizations,
IdentifierDrawer _identifierDrawer,
Configuration _config)
{
@ -295,7 +295,7 @@ public class SetPanel(
if (!design.Design.DesignData.IsHuman)
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)
{
var flag = type.ToFlag();

View file

@ -8,30 +8,27 @@ using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.DebugTab;
public class CustomizationServicePanel(CustomizationService _customization) : IDebugTabTree
public class CustomizationServicePanel(CustomizeService customize) : IDebugTabTree
{
public string Label
=> "Customization Service";
public bool Disabled
=> !_customization.Awaiter.IsCompletedSuccessfully;
=> !customize.Awaiter.IsCompletedSuccessfully;
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 = _customization.Service.GetList(clan, gender);
DrawCustomizationInfo(set);
DrawNpcCustomizationInfo(set);
}
var set = customize.Manager.GetSet(clan, gender);
DrawCustomizationInfo(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)
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)
return;

View file

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

View file

@ -19,7 +19,7 @@ public class UnlockOverview
{
private readonly ItemManager _items;
private readonly ItemUnlockManager _itemUnlocks;
private readonly CustomizationService _customizations;
private readonly CustomizeService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly PenumbraChangedItemTooltip _tooltip;
private readonly TextureService _textures;
@ -52,25 +52,22 @@ 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.Service.GetList(clan, gender).HairStyles.Count == 0)
continue;
if (_customizations.Manager.GetSet(clan, gender).HairStyles.Count == 0)
continue;
if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint",
_selected2 == clan && _selected3 == gender))
{
_selected1 = FullEquipType.Unknown;
_selected2 = clan;
_selected3 = gender;
}
if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint",
_selected2 == clan && _selected3 == gender))
{
_selected1 = FullEquipType.Unknown;
_selected2 = clan;
_selected3 = gender;
}
}
}
public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks,
public UnlockOverview(ItemManager items, CustomizeService customizations, ItemUnlockManager itemUnlocks,
CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureService textures, CodeService codes,
JobService jobs, FavoriteManager favorites)
{
@ -107,7 +104,7 @@ public class UnlockOverview
private void DrawCustomizations()
{
var set = _customizations.Service.GetList(_selected2, _selected3);
var set = _customizations.Manager.GetSet(_selected2, _selected3);
var spacing = IconSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
@ -121,7 +118,7 @@ public class UnlockOverview
continue;
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,
unlocked || _codes.EnabledShirts ? Vector4.One : UnavailableTint);

View file

@ -15,7 +15,7 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public class ImportService(CustomizationService _customizations, IDragDropManager _dragDropManager, ItemManager _items)
public class ImportService(CustomizeService _customizations, IDragDropManager _dragDropManager, ItemManager _items)
{
public void CreateDatSource()
=> _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)
return false;
var set = _customizations.Service.GetList(input.Clan, input.Gender);
var set = _customizations.Manager.GetSet(input.Clan, input.Gender);
voice = set.Voices[0];
if (inputVoice.HasValue && !set.Voices.Contains(inputVoice.Value))
return false;
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;
}

View file

@ -1,7 +1,6 @@
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Dalamud.Plugin.Services;
using Glamourer.GameData;
using OtterGui.Services;
using Penumbra.GameData.DataContainers;
@ -10,26 +9,18 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Services;
public sealed class CustomizationService(
ITextureProvider textures,
IDataManager gameData,
public sealed class CustomizeService(
HumanModelList humanModels,
IPluginLog log,
NpcCustomizeSet npcCustomizeSet)
NpcCustomizeSet npcCustomizeSet,
CustomizeManager manager)
: IAsyncService
{
public readonly HumanModelList HumanModels = humanModels;
public readonly HumanModelList HumanModels = humanModels;
public readonly CustomizeManager Manager = manager;
public readonly NpcCustomizeSet NpcCustomizeSet = npcCustomizeSet;
private ICustomizationManager? _service;
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 Task Awaiter { get; }
= Task.WhenAll(humanModels.Awaiter, manager.Awaiter, npcCustomizeSet.Awaiter);
public (CustomizeArray NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(CustomizeArray oldValues, CustomizeArray newValues,
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);
foreach (var index in CustomizationExtensions.AllBasic)
{
@ -79,69 +70,34 @@ public sealed class CustomizationService(
gender = Gender.Female;
if (gender == Gender.MaleNpc)
gender = Gender.Male;
return (gender, race) switch
{
(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",
};
return Manager.GetSet(race, gender).Name;
}
/// <summary> Returns whether a clan is valid. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
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>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
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)]
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 _);
/// <summary> Returns whether a customization value is valid for a given clan/gender set and face. </summary>
[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)
=> set.Validate(type, value, out data, face);
/// <summary> Returns whether a customization value is valid for a given clan, gender and face. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
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>
/// Check that the given race and clan are valid.
@ -160,10 +116,10 @@ public sealed class CustomizationService(
return string.Empty;
}
if (Service.Races.Contains(race))
if (CustomizeManager.Races.Contains(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.
if (actualClan == SubRace.Unknown)
{
@ -189,7 +145,7 @@ public sealed class CustomizationService(
/// </summary>
public string ValidateGender(Race race, Gender gender, out Gender actualGender)
{
if (!Service.Genders.Contains(gender))
if (!CustomizeManager.Genders.Contains(gender))
{
actualGender = Gender.Male;
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 return value is an empty string or a warning message.
/// </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)
{
if (allowUnknown || IsCustomizationValid(set, face, index, value))
@ -266,7 +222,7 @@ public sealed class CustomizationService(
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;
}
@ -284,11 +240,11 @@ public sealed class CustomizationService(
return 0;
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;
}
private static CustomizeFlag FixValues(CustomizationSet set, ref CustomizeArray customize)
private static CustomizeFlag FixValues(CustomizeSet set, ref CustomizeArray customize)
{
CustomizeFlag flags = 0;
foreach (var idx in CustomizationExtensions.AllBasic)

View file

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

View file

@ -27,7 +27,7 @@ public unsafe class FunModule : IDisposable
private readonly WorldSets _worldSets = new();
private readonly ItemManager _items;
private readonly CustomizationService _customizations;
private readonly CustomizeService _customizations;
private readonly Configuration _config;
private readonly CodeService _codes;
private readonly Random _rng;
@ -67,7 +67,7 @@ public unsafe class FunModule : IDisposable
internal void ResetFestival()
=> 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,
DesignManager designManager)
{
@ -197,7 +197,7 @@ public unsafe class FunModule : IDisposable
if (!_codes.EnabledIndividual)
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>())
{
if (index is CustomizeIndex.Face || !set.IsAvailable(index))

View file

@ -12,12 +12,12 @@ namespace Glamourer.State;
public class StateEditor
{
private readonly ItemManager _items;
private readonly CustomizationService _customizations;
private readonly CustomizeService _customizations;
private readonly HumanModelList _humans;
private readonly GPoseService _gPose;
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;
_humans = humans;
@ -72,7 +72,7 @@ public class StateEditor
state[CustomizeIndex.Clan] = 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))
state[index] = source;
}

View file

@ -28,7 +28,7 @@ public class StateListener : IDisposable
private readonly StateManager _manager;
private readonly StateApplier _applier;
private readonly ItemManager _items;
private readonly CustomizationService _customizations;
private readonly CustomizeService _customizations;
private readonly PenumbraService _penumbra;
private readonly SlotUpdating _slotUpdating;
private readonly WeaponLoading _weaponLoading;
@ -52,7 +52,7 @@ public class StateListener : IDisposable
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans,
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;
_items = items;
@ -167,7 +167,7 @@ public class StateListener : IDisposable
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)
{
if (state[index] is not StateChanged.Source.Fixed)

View file

@ -29,7 +29,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable
public IReadOnlyDictionary<uint, long> Unlocked
=> _unlocked;
public CustomizeUnlockManager(SaveService saveService, CustomizationService customizations, IDataManager gameData,
public CustomizeUnlockManager(SaveService saveService, CustomizeService customizations, IDataManager gameData,
IClientState clientState, ObjectUnlocked @event, IGameInteropProvider interop)
{
interop.InitializeFromAttributes(this);
@ -174,38 +174,35 @@ public class CustomizeUnlockManager : IDisposable, ISavable
"customization");
/// <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)
{
var ret = new Dictionary<CustomizeData, (uint Data, string Name)>();
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);
foreach (var hair in list.HairStyles)
{
var list = customizations.Service.GetList(clan, gender);
foreach (var hair in list.HairStyles)
var x = sheet.FirstOrDefault(f => f.FeatureID == hair.Value.Value);
if (x?.IsPurchasable == true)
{
var x = sheet.FirstOrDefault(f => f.FeatureID == hair.Value.Value);
if (x?.IsPurchasable == true)
{
var name = x.FeatureID == 61
? "Eternal Bond"
: x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Aesthetics - ", string.Empty)
?? string.Empty;
ret.TryAdd(hair, (x.Data, name));
}
var name = x.FeatureID == 61
? "Eternal Bond"
: x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Aesthetics - ", string.Empty)
?? string.Empty;
ret.TryAdd(hair, (x.Data, name));
}
}
foreach (var paint in list.FacePaints)
foreach (var paint in list.FacePaints)
{
var x = sheet.FirstOrDefault(f => f.FeatureID == paint.Value.Value);
if (x?.IsPurchasable == true)
{
var x = sheet.FirstOrDefault(f => f.FeatureID == paint.Value.Value);
if (x?.IsPurchasable == true)
{
var name = x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Cosmetics - ", string.Empty)
?? string.Empty;
ret.TryAdd(paint, (x.Data, name));
}
var name = x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Cosmetics - ", string.Empty)
?? string.Empty;
ret.TryAdd(paint, (x.Data, name));
}
}
}

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