mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
Add some support for NPC customizations.
This commit is contained in:
parent
38527f4320
commit
56ad7dc968
6 changed files with 197 additions and 11 deletions
141
Glamourer.GameData/Customization/CustomizationNpcOptions.cs
Normal file
141
Glamourer.GameData/Customization/CustomizationNpcOptions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,7 +45,7 @@ public partial class CustomizationOptions
|
||||||
|
|
||||||
|
|
||||||
// Get the index for the given pair of tribe and gender.
|
// 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);
|
var idx = ((int)race - 1) * Genders.Length + (gender == Gender.Female ? 1 : 0);
|
||||||
if (idx < 0 || idx >= ListSize)
|
if (idx < 0 || idx >= ListSize)
|
||||||
|
|
@ -59,8 +59,6 @@ public partial class CustomizationOptions
|
||||||
|
|
||||||
public partial class CustomizationOptions
|
public partial class CustomizationOptions
|
||||||
{
|
{
|
||||||
private readonly bool _valid;
|
|
||||||
|
|
||||||
public string GetName(CustomName name)
|
public string GetName(CustomName name)
|
||||||
=> _names[(int)name];
|
=> _names[(int)name];
|
||||||
|
|
||||||
|
|
@ -68,13 +66,13 @@ public partial class CustomizationOptions
|
||||||
{
|
{
|
||||||
var tmp = new TemporaryData(gameData, this);
|
var tmp = new TemporaryData(gameData, this);
|
||||||
_icons = new IconStorage(pi, gameData, _customizationSets.Length * 50);
|
_icons = new IconStorage(pi, gameData, _customizationSets.Length * 50);
|
||||||
_valid = tmp.Valid;
|
|
||||||
SetNames(gameData, tmp);
|
SetNames(gameData, tmp);
|
||||||
foreach (var race in Clans)
|
foreach (var race in Clans)
|
||||||
{
|
{
|
||||||
foreach (var gender in Genders)
|
foreach (var gender in Genders)
|
||||||
_customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender);
|
_customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender);
|
||||||
}
|
}
|
||||||
|
tmp.SetNpcData(_customizationSets);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain localized names of customization options and race names from the game data.
|
// Obtain localized names of customization options and race names from the game data.
|
||||||
|
|
@ -171,11 +169,24 @@ public partial class CustomizationOptions
|
||||||
return set;
|
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)
|
public TemporaryData(DataManager gameData, CustomizationOptions options)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
_cmpFile = new CmpFile(gameData);
|
_cmpFile = new CmpFile(gameData);
|
||||||
_customizeSheet = gameData.GetExcelSheet<CharaMakeCustomize>()!;
|
_customizeSheet = gameData.GetExcelSheet<CharaMakeCustomize>()!;
|
||||||
|
_bnpcCustomize = gameData.GetExcelSheet<BNpcCustomize>()!;
|
||||||
|
_enpcBase = gameData.GetExcelSheet<ENpcBase>()!;
|
||||||
Lobby = gameData.GetExcelSheet<Lobby>()!;
|
Lobby = gameData.GetExcelSheet<Lobby>()!;
|
||||||
var tmp = gameData.Excel.GetType().GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
|
var tmp = gameData.Excel.GetType().GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
|
||||||
.MakeGenericMethod(typeof(CharaMakeParams)).Invoke(gameData.Excel, new object?[]
|
.MakeGenericMethod(typeof(CharaMakeParams)).Invoke(gameData.Excel, new object?[]
|
||||||
|
|
@ -199,6 +210,8 @@ public partial class CustomizationOptions
|
||||||
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
|
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
|
||||||
private readonly ExcelSheet<CharaMakeParams> _listSheet;
|
private readonly ExcelSheet<CharaMakeParams> _listSheet;
|
||||||
private readonly ExcelSheet<HairMakeType> _hairSheet;
|
private readonly ExcelSheet<HairMakeType> _hairSheet;
|
||||||
|
private readonly ExcelSheet<BNpcCustomize> _bnpcCustomize;
|
||||||
|
private readonly ExcelSheet<ENpcBase> _enpcBase;
|
||||||
public readonly ExcelSheet<Lobby> Lobby;
|
public readonly ExcelSheet<Lobby> Lobby;
|
||||||
private readonly CmpFile _cmpFile;
|
private readonly CmpFile _cmpFile;
|
||||||
|
|
||||||
|
|
@ -213,6 +226,7 @@ public partial class CustomizationOptions
|
||||||
|
|
||||||
private readonly CustomizationOptions _options;
|
private readonly CustomizationOptions _options;
|
||||||
|
|
||||||
|
|
||||||
private CustomizeData[] CreateColorPicker(CustomizeIndex index, int offset, int num, bool light = false)
|
private CustomizeData[] CreateColorPicker(CustomizeIndex index, int offset, int num, bool light = false)
|
||||||
=> _cmpFile.GetSlice(offset, num)
|
=> _cmpFile.GetSlice(offset, num)
|
||||||
.Select((c, i) => new CustomizeData(index, (CustomizeValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))
|
.Select((c, i) => new CustomizeData(index, (CustomizeValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ public class CustomizationSet
|
||||||
public (CustomizeData, CustomizeData) LegacyTattoo { get; internal set; }
|
public (CustomizeData, CustomizeData) LegacyTattoo { get; internal set; }
|
||||||
public IReadOnlyList<CustomizeData> FacePaints { get; internal init; } = null!;
|
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
|
// Always Color Selector
|
||||||
public IReadOnlyList<CustomizeData> SkinColors { get; internal init; } = null!;
|
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> LipColorsLight { get; internal init; } = null!;
|
||||||
public IReadOnlyList<CustomizeData> LipColorsDark { 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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
public int DataByValue(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face)
|
public int DataByValue(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ public class Glamourer : IDalamudPlugin
|
||||||
public static readonly Logger Log = new();
|
public static readonly Logger Log = new();
|
||||||
public static ChatService Chat { get; private set; } = null!;
|
public static ChatService Chat { get; private set; } = null!;
|
||||||
|
|
||||||
|
|
||||||
private readonly ServiceProvider _services;
|
private readonly ServiceProvider _services;
|
||||||
|
|
||||||
public Glamourer(DalamudPluginInterface pluginInterface)
|
public Glamourer(DalamudPluginInterface pluginInterface)
|
||||||
|
|
@ -45,7 +44,5 @@ public class Glamourer : IDalamudPlugin
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
=> _services?.Dispose();
|
||||||
_services?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -822,7 +822,11 @@ public unsafe class DebugTab : ITab
|
||||||
foreach (var clan in _customization.AwaitedService.Clans)
|
foreach (var clan in _customization.AwaitedService.Clans)
|
||||||
{
|
{
|
||||||
foreach (var gender in _customization.AwaitedService.Genders)
|
foreach (var gender in _customization.AwaitedService.Genders)
|
||||||
DrawCustomizationInfo(_customization.AwaitedService.GetList(clan, gender));
|
{
|
||||||
|
var set = _customization.AwaitedService.GetList(clan, gender);
|
||||||
|
DrawCustomizationInfo(set);
|
||||||
|
DrawNpcCustomizationInfo(set);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -846,6 +850,23 @@ public unsafe class DebugTab : ITab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawNpcCustomizationInfo(CustomizationSet set)
|
||||||
|
{
|
||||||
|
using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)");
|
||||||
|
if (!tree)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var table = ImRaii.Table("npc", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||||
|
if (!table)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach(var (index, value) in set.NpcOptions)
|
||||||
|
{
|
||||||
|
ImGuiUtil.DrawTableColumn(index.ToString());
|
||||||
|
ImGuiUtil.DrawTableColumn(value.Value.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Designs
|
#region Designs
|
||||||
|
|
|
||||||
|
|
@ -122,12 +122,12 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value,
|
public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value,
|
||||||
[NotNullWhen(true)] out CustomizeData? data)
|
[NotNullWhen(true)] out CustomizeData? data)
|
||||||
=> set.DataByValue(type, value, out data, face) >= 0 || !set.IsAvailable(type) && value.Value == 0;
|
=> set.Validate(type, value, out data, face);
|
||||||
|
|
||||||
/// <summary> Returns whether a customization value is valid for a given clan, gender and face. </summary>
|
/// <summary> Returns whether a customization value is valid for a given clan, gender and face. </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
public bool IsCustomizationValid(SubRace race, Gender gender, CustomizeValue face, CustomizeIndex type, CustomizeValue value)
|
public bool IsCustomizationValid(SubRace race, Gender gender, CustomizeValue face, CustomizeIndex type, CustomizeValue value)
|
||||||
=> AwaitedService.GetList(race, gender).DataByValue(type, value, out _, face) >= 0;
|
=> IsCustomizationValid(AwaitedService.GetList(race, gender), face, type, value);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check that the given race and clan are valid.
|
/// Check that the given race and clan are valid.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue