Add some support for NPC customizations.

This commit is contained in:
Ottermandias 2023-07-17 18:34:04 +02:00
parent 38527f4320
commit 56ad7dc968
6 changed files with 197 additions and 11 deletions

View file

@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.Linq;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization;
public static class CustomizationNpcOptions
{
public static Dictionary<(SubRace, Gender), HashSet<(CustomizeIndex, CustomizeValue)>> CreateNpcData(CustomizationSet[] sets,
ExcelSheet<BNpcCustomize> bNpc, ExcelSheet<ENpcBase> eNpc)
{
var customizes = bNpc.SelectWhere(FromBnpcCustomize)
.Concat(eNpc.SelectWhere(FromEnpcBase)).ToList();
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 customizes)
{
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 = new HashSet<(CustomizeIndex, CustomizeValue)> { (customizeIndex, value) };
dict.Add((set.Clan, set.Gender), npcSet);
}
else
{
npcSet.Add((customizeIndex, value));
}
}
}
return dict;
}
private static (bool, Customize) FromBnpcCustomize(BNpcCustomize bnpcCustomize)
{
var customize = new Customize();
customize.Data.Set(0, (byte)bnpcCustomize.Race.Row);
customize.Data.Set(1, bnpcCustomize.Gender);
customize.Data.Set(2, bnpcCustomize.BodyType);
customize.Data.Set(3, bnpcCustomize.Height);
customize.Data.Set(4, (byte)bnpcCustomize.Tribe.Row);
customize.Data.Set(5, bnpcCustomize.Face);
customize.Data.Set(6, bnpcCustomize.HairStyle);
customize.Data.Set(7, bnpcCustomize.HairHighlight);
customize.Data.Set(8, bnpcCustomize.SkinColor);
customize.Data.Set(9, bnpcCustomize.EyeHeterochromia);
customize.Data.Set(10, bnpcCustomize.HairColor);
customize.Data.Set(11, bnpcCustomize.HairHighlightColor);
customize.Data.Set(12, bnpcCustomize.FacialFeature);
customize.Data.Set(13, bnpcCustomize.FacialFeatureColor);
customize.Data.Set(14, bnpcCustomize.Eyebrows);
customize.Data.Set(15, bnpcCustomize.EyeColor);
customize.Data.Set(16, bnpcCustomize.EyeShape);
customize.Data.Set(17, bnpcCustomize.Nose);
customize.Data.Set(18, bnpcCustomize.Jaw);
customize.Data.Set(19, bnpcCustomize.Mouth);
customize.Data.Set(20, bnpcCustomize.LipColor);
customize.Data.Set(21, bnpcCustomize.BustOrTone1);
customize.Data.Set(22, bnpcCustomize.ExtraFeature1);
customize.Data.Set(23, bnpcCustomize.ExtraFeature2OrBust);
customize.Data.Set(24, bnpcCustomize.FacePaint);
customize.Data.Set(25, bnpcCustomize.FacePaintColor);
if (customize.BodyType.Value != 1
|| !CustomizationOptions.Races.Contains(customize.Race)
|| !CustomizationOptions.Clans.Contains(customize.Clan)
|| !CustomizationOptions.Genders.Contains(customize.Gender))
return (false, Customize.Default);
return (true, customize);
}
private static (bool, Customize) FromEnpcBase(ENpcBase enpcBase)
{
if (enpcBase.ModelChara.Row != 0)
return (false, Customize.Default);
var customize = new Customize();
customize.Data.Set(0, (byte)enpcBase.Race.Row);
customize.Data.Set(1, enpcBase.Gender);
customize.Data.Set(2, enpcBase.BodyType);
customize.Data.Set(3, enpcBase.Height);
customize.Data.Set(4, (byte)enpcBase.Tribe.Row);
customize.Data.Set(5, enpcBase.Face);
customize.Data.Set(6, enpcBase.HairStyle);
customize.Data.Set(7, enpcBase.HairHighlight);
customize.Data.Set(8, enpcBase.SkinColor);
customize.Data.Set(9, enpcBase.EyeHeterochromia);
customize.Data.Set(10, enpcBase.HairColor);
customize.Data.Set(11, enpcBase.HairHighlightColor);
customize.Data.Set(12, enpcBase.FacialFeature);
customize.Data.Set(13, enpcBase.FacialFeatureColor);
customize.Data.Set(14, enpcBase.Eyebrows);
customize.Data.Set(15, enpcBase.EyeColor);
customize.Data.Set(16, enpcBase.EyeShape);
customize.Data.Set(17, enpcBase.Nose);
customize.Data.Set(18, enpcBase.Jaw);
customize.Data.Set(19, enpcBase.Mouth);
customize.Data.Set(20, enpcBase.LipColor);
customize.Data.Set(21, enpcBase.BustOrTone1);
customize.Data.Set(22, enpcBase.ExtraFeature1);
customize.Data.Set(23, enpcBase.ExtraFeature2OrBust);
customize.Data.Set(24, enpcBase.FacePaint);
customize.Data.Set(25, enpcBase.FacePaintColor);
if (customize.BodyType.Value != 1
|| !CustomizationOptions.Races.Contains(customize.Race)
|| !CustomizationOptions.Clans.Contains(customize.Clan)
|| !CustomizationOptions.Genders.Contains(customize.Gender))
return (false, Customize.Default);
return (true, customize);
}
}

View file

@ -45,7 +45,7 @@ public partial class CustomizationOptions
// Get the index for the given pair of tribe and gender.
private static int ToIndex(SubRace race, Gender 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)
@ -59,8 +59,6 @@ public partial class CustomizationOptions
public partial class CustomizationOptions
{
private readonly bool _valid;
public string GetName(CustomName name)
=> _names[(int)name];
@ -68,13 +66,13 @@ public partial class CustomizationOptions
{
var tmp = new TemporaryData(gameData, this);
_icons = new IconStorage(pi, gameData, _customizationSets.Length * 50);
_valid = tmp.Valid;
SetNames(gameData, tmp);
foreach (var race in Clans)
{
foreach (var gender in Genders)
_customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender);
}
tmp.SetNpcData(_customizationSets);
}
// Obtain localized names of customization options and race names from the game data.
@ -171,11 +169,24 @@ public partial class CustomizationOptions
return set;
}
public void SetNpcData(CustomizationSet[] sets)
{
var data = CustomizationNpcOptions.CreateNpcData(sets, _bnpcCustomize, _enpcBase);
foreach (var set in sets)
{
if (data.TryGetValue((set.Clan, set.Gender), out var npcData))
set.NpcOptions = npcData.ToArray();
}
}
public TemporaryData(DataManager gameData, CustomizationOptions options)
{
_options = options;
_cmpFile = new CmpFile(gameData);
_customizeSheet = gameData.GetExcelSheet<CharaMakeCustomize>()!;
_bnpcCustomize = gameData.GetExcelSheet<BNpcCustomize>()!;
_enpcBase = gameData.GetExcelSheet<ENpcBase>()!;
Lobby = gameData.GetExcelSheet<Lobby>()!;
var tmp = gameData.Excel.GetType().GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
.MakeGenericMethod(typeof(CharaMakeParams)).Invoke(gameData.Excel, new object?[]
@ -199,6 +210,8 @@ public partial class CustomizationOptions
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
private readonly ExcelSheet<CharaMakeParams> _listSheet;
private readonly ExcelSheet<HairMakeType> _hairSheet;
private readonly ExcelSheet<BNpcCustomize> _bnpcCustomize;
private readonly ExcelSheet<ENpcBase> _enpcBase;
public readonly ExcelSheet<Lobby> Lobby;
private readonly CmpFile _cmpFile;
@ -213,6 +226,7 @@ public partial class CustomizationOptions
private readonly CustomizationOptions _options;
private CustomizeData[] CreateColorPicker(CustomizeIndex index, int offset, int num, bool light = false)
=> _cmpFile.GetSlice(offset, num)
.Select((c, i) => new CustomizeData(index, (CustomizeValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))

View file

@ -65,6 +65,8 @@ public class CustomizationSet
public (CustomizeData, CustomizeData) LegacyTattoo { get; internal set; }
public IReadOnlyList<CustomizeData> FacePaints { get; internal init; } = null!;
public IReadOnlyList<(CustomizeIndex Type, CustomizeValue Value)> NpcOptions { get; internal set; } =
Array.Empty<(CustomizeIndex Type, CustomizeValue Value)>();
// Always Color Selector
public IReadOnlyList<CustomizeData> SkinColors { get; internal init; } = null!;
@ -77,6 +79,17 @@ public class CustomizationSet
public IReadOnlyList<CustomizeData> LipColorsLight { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> LipColorsDark { get; internal init; } = null!;
public bool Validate(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face)
{
if (IsAvailable(index))
return DataByValue(index, value, out custom, face) >= 0
|| NpcOptions.Any(t => t.Type == index && t.Value == value);
custom = null;
return value == CustomizeValue.Zero;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int DataByValue(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face)
{