mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-13 12:14:18 +01:00
Rework and improve CustomizationManager and stuff.
This commit is contained in:
parent
aae4141550
commit
ab76d3508b
34 changed files with 916 additions and 1025 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
77
Glamourer/GameData/CustomizeManager.cs
Normal file
77
Glamourer/GameData/CustomizeManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
459
Glamourer/GameData/CustomizeSetFactory.cs
Normal file
459
Glamourer/GameData/CustomizeSetFactory.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 197d23eee167c232000f22ef40a7a2bded913b6c
|
||||
Subproject commit 4404d62b7442daa7e3436dc417364905e3d5cd2f
|
||||
Loading…
Add table
Add a link
Reference in a new issue