Compare commits

..

No commits in common. "main" and "1.0.2.2" have entirely different histories.

278 changed files with 11811 additions and 29545 deletions

View file

@ -9,15 +9,13 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
submodules: true
- name: Setup .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v1
with:
dotnet-version: |
10.x.x
9.x.x
dotnet-version: '7.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
@ -39,7 +37,7 @@ jobs:
- name: Archive
run: Compress-Archive -Path Glamourer/bin/Release/* -DestinationPath Glamourer.zip
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2.2.1
with:
path: |
./Glamourer/bin/Release/*

View file

@ -9,15 +9,13 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v2
with:
submodules: recursive
submodules: true
- name: Setup .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v1
with:
dotnet-version: |
10.x.x
9.x.x
dotnet-version: '7.x.x'
- name: Restore dependencies
run: dotnet restore
- name: Download Dalamud
@ -39,7 +37,7 @@ jobs:
- name: Archive
run: Compress-Archive -Path Glamourer/bin/Debug/* -DestinationPath Glamourer.zip
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2.2.1
with:
path: |
./Glamourer/bin/Debug/*

12
.gitmodules vendored
View file

@ -1,20 +1,16 @@
[submodule "OtterGui"]
path = OtterGui
url = https://github.com/Ottermandias/OtterGui.git
url = git@github.com:Ottermandias/OtterGui.git
branch = main
[submodule "Penumbra.GameData"]
path = Penumbra.GameData
url = https://github.com/Ottermandias/Penumbra.GameData.git
url = git@github.com:Ottermandias/Penumbra.GameData.git
branch = main
[submodule "Penumbra.String"]
path = Penumbra.String
url = https://github.com/Ottermandias/Penumbra.String.git
url = git@github.com:Ottermandias/Penumbra.String.git
branch = main
[submodule "Penumbra.Api"]
path = Penumbra.Api
url = https://github.com/Ottermandias/Penumbra.Api.git
branch = main
[submodule "Glamourer.Api"]
path = Glamourer.Api
url = https://github.com/Ottermandias/Glamourer.Api.git
url = git@github.com:Ottermandias/Penumbra.Api.git
branch = main

@ -1 +0,0 @@
Subproject commit 5b6730d46f17bdd02a441e23e2141576cf7acf53

View file

@ -0,0 +1,126 @@
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Customization;
// A custom version of CharaMakeParams that is easier to parse.
[Sheet("CharaMakeParams")]
public class CharaMakeParams : ExcelRow
{
public const int NumMenus = 28;
public const int NumVoices = 12;
public const int NumGraphics = 10;
public const int MaxNumValues = 100;
public const int NumFaces = 8;
public const int NumFeatures = 7;
public const int NumEquip = 3;
public enum MenuType
{
ListSelector = 0,
IconSelector = 1,
ColorPicker = 2,
DoubleColorPicker = 3,
IconCheckmark = 4,
Percentage = 5,
Checkmark = 6, // custom
Nothing = 7, // custom
List1Selector = 8, // custom, 1-indexed lists
}
public struct Menu
{
public uint Id;
public byte InitVal;
public MenuType Type;
public byte Size;
public byte LookAt;
public uint Mask;
public uint Customize;
public uint[] Values;
public byte[] Graphic;
}
public struct FacialFeatures
{
public uint[] Icons;
}
public LazyRow<Race> Race { get; set; } = null!;
public LazyRow<Tribe> Tribe { get; set; } = null!;
public sbyte Gender { get; set; }
public Menu[] Menus { get; set; } = new Menu[NumMenus];
public byte[] Voices { get; set; } = new byte[NumVoices];
public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces];
public CharaMakeType.CharaMakeTypeUnkData3347Obj[] Equip { get; set; } = new CharaMakeType.CharaMakeTypeUnkData3347Obj[NumEquip];
public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language)
{
RowId = parser.RowId;
SubRowId = parser.SubRowId;
Race = new LazyRow<Race>(gameData, parser.ReadColumn<uint>(0), language);
Tribe = new LazyRow<Tribe>(gameData, parser.ReadColumn<uint>(1), language);
Gender = parser.ReadColumn<sbyte>(2);
var currentOffset = 0;
for (var i = 0; i < NumMenus; ++i)
{
currentOffset = 3 + i;
Menus[i].Id = parser.ReadColumn<uint>(0 * NumMenus + currentOffset);
Menus[i].InitVal = parser.ReadColumn<byte>(1 * NumMenus + currentOffset);
Menus[i].Type = (MenuType)parser.ReadColumn<byte>(2 * NumMenus + currentOffset);
Menus[i].Size = parser.ReadColumn<byte>(3 * NumMenus + currentOffset);
Menus[i].LookAt = parser.ReadColumn<byte>(4 * NumMenus + currentOffset);
Menus[i].Mask = parser.ReadColumn<uint>(5 * NumMenus + currentOffset);
Menus[i].Customize = parser.ReadColumn<uint>(6 * NumMenus + currentOffset);
Menus[i].Values = new uint[Menus[i].Size];
switch (Menus[i].Type)
{
case MenuType.ColorPicker:
case MenuType.DoubleColorPicker:
case MenuType.Percentage:
break;
default:
currentOffset += 7 * NumMenus;
for (var j = 0; j < Menus[i].Size; ++j)
Menus[i].Values[j] = parser.ReadColumn<uint>(j * NumMenus + currentOffset);
break;
}
Menus[i].Graphic = new byte[NumGraphics];
currentOffset = 3 + (MaxNumValues + 7) * NumMenus + i;
for (var j = 0; j < NumGraphics; ++j)
Menus[i].Graphic[j] = parser.ReadColumn<byte>(j * NumMenus + currentOffset);
}
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus;
for (var i = 0; i < NumVoices; ++i)
Voices[i] = parser.ReadColumn<byte>(currentOffset++);
for (var i = 0; i < NumFaces; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + i;
FacialFeatureByFace[i].Icons = new uint[NumFeatures];
for (var j = 0; j < NumFeatures; ++j)
FacialFeatureByFace[i].Icons[j] = (uint)parser.ReadColumn<int>(j * NumFaces + currentOffset);
}
for (var i = 0; i < NumEquip; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7;
Equip[i] = new CharaMakeType.CharaMakeTypeUnkData3347Obj()
{
Helmet = parser.ReadColumn<ulong>(currentOffset + 0),
Top = parser.ReadColumn<ulong>(currentOffset + 1),
Gloves = parser.ReadColumn<ulong>(currentOffset + 2),
Legs = parser.ReadColumn<ulong>(currentOffset + 3),
Shoes = parser.ReadColumn<ulong>(currentOffset + 4),
Weapon = parser.ReadColumn<ulong>(currentOffset + 5),
SubWeapon = parser.ReadColumn<ulong>(currentOffset + 6),
};
}
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Logging;
using Dalamud.Plugin.Services;
namespace Glamourer.Customization;
// Convert the Human.Cmp file into color sets.
// If the file can not be read due to TexTools corruption, create a 0-array of size MinSize.
internal class CmpFile
{
private readonly Lumina.Data.FileResource? _file;
private readonly uint[] _rgbaColors;
// No error checking since only called internally.
public IEnumerable<uint> GetSlice(int offset, int count)
=> _rgbaColors.Length >= offset + count ? _rgbaColors.Skip(offset).Take(count) : Enumerable.Repeat(0u, count);
public bool Valid
=> _file != null;
public CmpFile(IDataManager gameData, IPluginLog log)
{
try
{
_file = gameData.GetFile("chara/xls/charamake/human.cmp")!;
_rgbaColors = new uint[_file.Data.Length >> 2];
for (var i = 0; i < _file.Data.Length; i += 4)
{
_rgbaColors[i >> 2] = _file.Data[i]
| (uint)(_file.Data[i + 1] << 8)
| (uint)(_file.Data[i + 2] << 16)
| (uint)(_file.Data[i + 3] << 24);
}
}
catch (Exception e)
{
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);
_file = null;
_rgbaColors = Array.Empty<uint>();
}
}
}

View file

@ -0,0 +1,45 @@
namespace Glamourer.Customization;
// Localization from the game files directly.
public enum CustomName
{
Clan = 0,
Gender,
Reverse,
OddEyes,
IrisSmall,
IrisLarge,
IrisSize,
MidlanderM,
HighlanderM,
WildwoodM,
DuskwightM,
PlainsfolkM,
DunesfolkM,
SeekerOfTheSunM,
KeeperOfTheMoonM,
SeawolfM,
HellsguardM,
RaenM,
XaelaM,
HelionM,
LostM,
RavaM,
VeenaM,
MidlanderF,
HighlanderF,
WildwoodF,
DuskwightF,
PlainsfolkF,
DunesfolkF,
SeekerOfTheSunF,
KeeperOfTheMoonF,
SeawolfF,
HellsguardF,
RaenF,
XaelaF,
HelionF,
LostF,
RavaF,
VeenaF,
}

View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization;
public class CustomizationManager : ICustomizationManager
{
private static CustomizationOptions? _options;
private CustomizationManager()
{ }
public static ICustomizationManager Create(ITextureProvider textures, IDataManager gameData, IPluginLog log)
{
_options ??= new CustomizationOptions(textures, gameData, log);
return new CustomizationManager();
}
public IReadOnlyList<Race> Races
=> CustomizationOptions.Races;
public IReadOnlyList<SubRace> Clans
=> CustomizationOptions.Clans;
public IReadOnlyList<Gender> Genders
=> CustomizationOptions.Genders;
public CustomizationSet GetList(SubRace clan, Gender gender)
=> _options!.GetList(clan, gender);
public IDalamudTextureWrap GetIcon(uint iconId)
=> _options!.GetIcon(iconId);
public string GetName(CustomName name)
=> _options!.GetName(name);
}

View file

@ -0,0 +1,142 @@
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), IReadOnlyList<(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.ToDictionary(kvp => kvp.Key,
kvp => (IReadOnlyList<(CustomizeIndex, CustomizeValue)>)kvp.Value.OrderBy(p => p.Item1).ThenBy(p => p.Item2.Value).ToArray());
}
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.Value?.Type != 1)
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

@ -0,0 +1,527 @@
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 Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Customization;
// 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)
{
var tmp = new TemporaryData(gameData, this, log);
_icons = new IconStorage(textures, gameData);
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.
private readonly string[] _names = new string[Enum.GetValues<CustomName>().Length];
private void SetNames(IDataManager gameData, TemporaryData tmp)
{
var subRace = gameData.GetExcelSheet<Tribe>()!;
void Set(CustomName id, Lumina.Text.SeString? s, string def)
=> _names[(int)id] = s?.ToDalamudString().TextValue ?? def;
Set(CustomName.Clan, tmp.Lobby.GetRow(102)?.Text, "Clan");
Set(CustomName.Gender, tmp.Lobby.GetRow(103)?.Text, "Gender");
Set(CustomName.Reverse, tmp.Lobby.GetRow(2135)?.Text, "Reverse");
Set(CustomName.OddEyes, tmp.Lobby.GetRow(2125)?.Text, "Odd Eyes");
Set(CustomName.IrisSmall, tmp.Lobby.GetRow(1076)?.Text, "Small");
Set(CustomName.IrisLarge, tmp.Lobby.GetRow(1075)?.Text, "Large");
Set(CustomName.IrisSize, tmp.Lobby.GetRow(244)?.Text, "Iris Size");
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 bool Valid
=> _cmpFile.Valid;
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)
{
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(IDataManager gameData, CustomizationOptions options, IPluginLog log)
{
_options = options;
_cmpFile = new CmpFile(gameData, log);
_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?[]
{
"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;
private readonly ExcelSheet<BNpcCustomize> _bnpcCustomize;
private readonly ExcelSheet<ENpcBase> _enpcBase;
public readonly ExcelSheet<Lobby> Lobby;
private readonly CmpFile _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)
=> _cmpFile.GetSlice(offset, num)
.Select((c, i) => new CustomizeData(index, (CustomizeValue)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))
.ToArray();
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.Race == Race.Hrothgar && set.Gender == Gender.Female)
return;
void Set(bool available, CustomizeIndex flag)
{
if (available)
set.SetAvailable(flag);
}
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);
}
// 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);
static (CustomizeData, CustomizeData) Create(CustomizeIndex i, uint data)
=> (new CustomizeData(i, CustomizeValue.Zero, data, 0), new CustomizeData(i, CustomizeValue.Max, data, 1));
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];
}
// 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();
}
// Facial Features and Tattoos is created by combining two strings.
if (c is >= CustomizeIndex.FacialFeature1 and <= CustomizeIndex.LegacyTattoo)
return
$"{Lobby.GetRow(1741)?.Text.ToDalamudString().ToString() ?? "Facial Features"} & {Lobby.GetRow(1742)?.Text.ToDalamudString().ToString() ?? "Tattoos"}";
// 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] = nameArray[(int)CustomizeIndex.EyeColorRight];
nameArray[(int)CustomizeIndex.EyeColorRight] = _options.GetName(CustomName.OddEyes);
set.OptionName = nameArray;
}
// Obtain available skin and hair colors for the given subrace and gender.
private (CustomizeData[], CustomizeData[]) GetColors(SubRace race, Gender gender)
{
if (race is > SubRace.Veena or SubRace.Unknown)
throw new ArgumentOutOfRangeException(nameof(race), race, null);
var gv = gender == Gender.Male ? 0 : 1;
var idx = ((int)race * 2 + gv) * 5 + 3;
return (CreateColorPicker(CustomizeIndex.SkinColor, idx << 8, 192),
CreateColorPicker(CustomizeIndex.HairColor, (idx + 1) << 8, 192));
}
// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender.
private CustomizeData[] GetHairStyles(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
// Unknown30 is the number of available hairstyles.
var hairList = new List<CustomizeData>(row.Unknown30);
// Hairstyles can be found starting at Unknown66.
for (var i = 0; i < row.Unknown30; ++i)
{
var name = $"Unknown{66 + i * 9}";
var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
// Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx);
if (hairRow == null)
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx));
else if (_options._icons.IconExists(hairRow.Icon))
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon,
(ushort)hairRow.RowId));
}
return hairList.OrderBy(h => h.Value.Value).ToArray();
}
// Get Features.
private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new CustomizeData(id, (CustomizeValue)(index + 1), value)
: new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId);
}
// Get List sizes.
private static int GetListSize(CharaMakeParams row, CustomizeIndex index)
{
var gameId = index.ToByteAndMask().ByteIdx;
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == gameId);
return menu?.Size ?? 0;
}
// Get face paints from the hair sheet via reflection.
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<CustomizeData>(row.Unknown37);
// Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i)
{
// Face paints start at Unknown73.
var name = $"Unknown{73 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
if (paintRow != null)
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
(ushort)paintRow.RowId));
else
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
}
return paintList.OrderBy(p => p.Value.Value).ToArray();
}
// Specific icons for tails or ears.
private CustomizeData[] GetTailEarShapes(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray()
?? Array.Empty<CustomizeData>();
// Specific icons for faces.
private CustomizeData[] GetFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx)
?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray()
?? Array.Empty<CustomizeData>();
// Specific icons for Hrothgar patterns.
private CustomizeData[] HrothgarFurPattern(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray()
?? Array.Empty<CustomizeData>();
}
}

View file

@ -1,34 +1,28 @@
using OtterGui;
using OtterGui.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.GameData;
namespace Glamourer.Customization;
/// <summary>
/// Each SubRace and Gender combo has a customization set.
/// This describes the available customizations, their types and their names.
/// </summary>
public class CustomizeSet
// Each Subrace and Gender combo has a customization set.
// This describes the available customizations, their types and their names.
public class CustomizationSet
{
private readonly NpcCustomizeSet _npcCustomizations;
internal CustomizeSet(NpcCustomizeSet npcCustomizations, SubRace clan, Gender gender)
internal CustomizationSet(SubRace clan, Gender gender)
{
_npcCustomizations = npcCustomizations;
Gender = gender;
Clan = clan;
Race = clan.ToRace();
SettingAvailable = 0;
Gender = gender;
Clan = clan;
Race = clan.ToRace();
SettingAvailable = 0;
}
public Gender Gender { get; }
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)
@ -38,14 +32,14 @@ public class CustomizeSet
=> SettingAvailable.HasFlag(index.ToFlag());
// Meta
public IReadOnlyList<string> OptionName { get; internal init; } = null!;
public IReadOnlyList<string> OptionName { get; internal set; } = null!;
public string Option(CustomizeIndex index)
=> OptionName[(int)index];
public IReadOnlyList<byte> Voices { get; internal init; } = null!;
public IReadOnlyList<MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<MenuType, CustomizeIndex[]> Order { get; internal set; } = null!;
public IReadOnlyList<byte> Voices { get; internal init; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizeIndex[]> Order { get; internal set; } = null!;
// Always list selector.
@ -89,7 +83,6 @@ public class CustomizeSet
{
if (IsAvailable(index))
return DataByValue(index, value, out custom, face) >= 0
|| _npcCustomizations.CheckValue(index, value)
|| NpcOptions.Any(t => t.Type == index && t.Value == value);
custom = null;
@ -101,76 +94,12 @@ public class CustomizeSet
{
var type = Types[(int)index];
return type switch
int GetInteger0(out CustomizeData? custom)
{
MenuType.ListSelector => GetInteger0(out custom),
MenuType.List1Selector => GetInteger1(out custom),
MenuType.IconSelector => index switch
if (value < Count(index))
{
CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom),
CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles,
value, out custom),
CustomizeIndex.TailShape => Get(TailEarShapes, value, out custom),
CustomizeIndex.FacePaint => Get(FacePaints, value, out custom),
CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom),
_ => Invalid(out custom),
},
MenuType.ColorPicker => index switch
{
CustomizeIndex.SkinColor => Get(SkinColors, value, out custom),
CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom),
CustomizeIndex.EyeColorRight => Get(EyeColors, value, out custom),
CustomizeIndex.HairColor => Get(HairColors, value, out custom),
CustomizeIndex.HighlightsColor => Get(HighlightColors, value, out custom),
CustomizeIndex.TattooColor => Get(TattooColors, value, out custom),
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom),
},
MenuType.DoubleColorPicker => index switch
{
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom),
},
MenuType.IconCheckmark => GetBool(index, value, out custom),
MenuType.Percentage => GetInteger0(out custom),
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 = new CustomizeData(index, value, 0, value.Value);
return value.Value;
}
custom = null;
@ -189,17 +118,81 @@ public class CustomizeSet
return -1;
}
int GetInteger0(out CustomizeData? custom)
static int GetBool(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom)
{
if (value < Count(index))
if (value == CustomizeValue.Zero)
{
custom = new CustomizeData(index, value, 0, value.Value);
return value.Value;
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),
CharaMakeParams.MenuType.List1Selector => GetInteger1(out custom),
CharaMakeParams.MenuType.IconSelector => index switch
{
CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom),
CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles,
value, out custom),
CustomizeIndex.TailShape => Get(TailEarShapes, value, out custom),
CustomizeIndex.FacePaint => Get(FacePaints, value, out custom),
CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.ColorPicker => index switch
{
CustomizeIndex.SkinColor => Get(SkinColors, value, out custom),
CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom),
CustomizeIndex.EyeColorRight => Get(EyeColors, value, out custom),
CustomizeIndex.HairColor => Get(HairColors, value, out custom),
CustomizeIndex.HighlightsColor => Get(HighlightColors, value, out custom),
CustomizeIndex.TattooColor => Get(TattooColors, value, out custom),
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.DoubleColorPicker => index switch
{
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.IconCheckmark => GetBool(index, value, out custom),
CharaMakeParams.MenuType.Percentage => GetInteger0(out custom),
CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom),
_ => Invalid(out custom),
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
@ -214,10 +207,10 @@ public class CustomizeSet
switch (Types[(int)index])
{
case MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx);
case MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx);
case CharaMakeParams.MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case CharaMakeParams.MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case CharaMakeParams.MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx);
case CharaMakeParams.MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx);
}
return index switch
@ -247,9 +240,22 @@ public class CustomizeSet
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public MenuType Type(CustomizeIndex index)
public CharaMakeParams.MenuType Type(CustomizeIndex index)
=> Types[(int)index];
internal static IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizeIndex[]> ComputeOrder(CustomizationSet set)
{
var ret = Enum.GetValues<CustomizeIndex>().ToArray();
ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft;
ret[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorRight;
ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor;
var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray());
foreach (var type in Enum.GetValues<CharaMakeParams.MenuType>())
dict.TryAdd(type, Array.Empty<CustomizeIndex>());
return dict;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int Count(CustomizeIndex index)
=> Count(index, CustomizeValue.Zero);
@ -262,9 +268,9 @@ public class CustomizeSet
return Type(index) switch
{
MenuType.Percentage => 101,
MenuType.IconCheckmark => 2,
MenuType.Checkmark => 2,
CharaMakeParams.MenuType.Percentage => 101,
CharaMakeParams.MenuType.IconCheckmark => 2,
CharaMakeParams.MenuType.Checkmark => 2,
_ => index switch
{
CustomizeIndex.Face => Faces.Count,
@ -294,10 +300,3 @@ public class CustomizeSet
private CustomizeValue HrothgarFaceHack(CustomizeValue value)
=> Race == Race.Hrothgar && value.Value is > 4 and < 9 ? value - 4 : value;
}
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, CustomizeSet set)
=> flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.BodyType);
}

View file

@ -0,0 +1,123 @@
using System;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization;
public unsafe struct Customize
{
public Penumbra.GameData.Structs.CustomizeData Data;
public Customize(in Penumbra.GameData.Structs.CustomizeData data)
{
Data = data.Clone();
}
public Race Race
{
get => (Race)Data.Get(CustomizeIndex.Race).Value;
set => Data.Set(CustomizeIndex.Race, (CustomizeValue)(byte)value);
}
public Gender Gender
{
get => (Gender)Data.Get(CustomizeIndex.Gender).Value + 1;
set => Data.Set(CustomizeIndex.Gender, (CustomizeValue)(byte)value - 1);
}
public CustomizeValue BodyType
{
get => Data.Get(CustomizeIndex.BodyType);
set => Data.Set(CustomizeIndex.BodyType, value);
}
public SubRace Clan
{
get => (SubRace)Data.Get(CustomizeIndex.Clan).Value;
set => Data.Set(CustomizeIndex.Clan, (CustomizeValue)(byte)value);
}
public CustomizeValue Face
{
get => Data.Get(CustomizeIndex.Face);
set => Data.Set(CustomizeIndex.Face, value);
}
public static readonly Customize Default = GenerateDefault();
public static readonly Customize Empty = new();
public CustomizeValue Get(CustomizeIndex index)
=> Data.Get(index);
public bool Set(CustomizeIndex flag, CustomizeValue index)
=> Data.Set(flag, index);
public bool Equals(Customize other)
=> Equals(Data, other.Data);
public CustomizeValue this[CustomizeIndex index]
{
get => Get(index);
set => Set(index, value);
}
private static Customize GenerateDefault()
{
var ret = new Customize
{
Race = Race.Hyur,
Clan = SubRace.Midlander,
Gender = Gender.Male,
};
ret.Set(CustomizeIndex.BodyType, (CustomizeValue)1);
ret.Set(CustomizeIndex.Height, (CustomizeValue)50);
ret.Set(CustomizeIndex.Face, (CustomizeValue)1);
ret.Set(CustomizeIndex.Hairstyle, (CustomizeValue)1);
ret.Set(CustomizeIndex.SkinColor, (CustomizeValue)1);
ret.Set(CustomizeIndex.EyeColorRight, (CustomizeValue)1);
ret.Set(CustomizeIndex.HighlightsColor, (CustomizeValue)1);
ret.Set(CustomizeIndex.TattooColor, (CustomizeValue)1);
ret.Set(CustomizeIndex.Eyebrows, (CustomizeValue)1);
ret.Set(CustomizeIndex.EyeColorLeft, (CustomizeValue)1);
ret.Set(CustomizeIndex.EyeShape, (CustomizeValue)1);
ret.Set(CustomizeIndex.Nose, (CustomizeValue)1);
ret.Set(CustomizeIndex.Jaw, (CustomizeValue)1);
ret.Set(CustomizeIndex.Mouth, (CustomizeValue)1);
ret.Set(CustomizeIndex.LipColor, (CustomizeValue)1);
ret.Set(CustomizeIndex.MuscleMass, (CustomizeValue)50);
ret.Set(CustomizeIndex.TailShape, (CustomizeValue)1);
ret.Set(CustomizeIndex.BustSize, (CustomizeValue)50);
ret.Set(CustomizeIndex.FacePaint, (CustomizeValue)1);
ret.Set(CustomizeIndex.FacePaintColor, (CustomizeValue)1);
return ret;
}
public void Load(Customize other)
=> Data.Read(&other.Data);
public readonly void Write(nint target)
=> Data.Write((void*)target);
public bool LoadBase64(string data)
=> Data.LoadBase64(data);
public readonly string WriteBase64()
=> Data.WriteBase64();
public static CustomizeFlag Compare(Customize lhs, Customize rhs)
{
CustomizeFlag ret = 0;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
var l = lhs[idx];
var r = rhs[idx];
if (l.Value != r.Value)
ret |= idx.ToFlag();
}
return ret;
}
public override string ToString()
=> Data.ToString();
}

View file

@ -1,36 +1,28 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using System;
using System.Runtime.InteropServices;
namespace Glamourer.GameData;
namespace Glamourer.Customization;
/// <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>
// 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.
[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;
@ -40,23 +32,14 @@ public readonly struct CustomizeData : IEquatable<CustomizeData>
CustomizeId = customizeId;
}
/// <inheritdoc/>
public bool Equals(CustomizeData other)
=> Index == other.Index
&& Value.Value == other.Value.Value
&& CustomizeId == other.CustomizeId;
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is CustomizeData other && Equals(other);
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine((int)Index, Value.Value, CustomizeId);
public static bool operator ==(CustomizeData left, CustomizeData right)
=> left.Equals(right);
public static bool operator !=(CustomizeData left, CustomizeData right)
=> !(left == right);
}

View file

@ -0,0 +1,114 @@
using System;
using System.Runtime.CompilerServices;
namespace Glamourer.Customization;
[Flags]
public enum CustomizeFlag : ulong
{
Invalid = 0,
Race = 1ul << CustomizeIndex.Race,
Gender = 1ul << CustomizeIndex.Gender,
BodyType = 1ul << CustomizeIndex.BodyType,
Height = 1ul << CustomizeIndex.Height,
Clan = 1ul << CustomizeIndex.Clan,
Face = 1ul << CustomizeIndex.Face,
Hairstyle = 1ul << CustomizeIndex.Hairstyle,
Highlights = 1ul << CustomizeIndex.Highlights,
SkinColor = 1ul << CustomizeIndex.SkinColor,
EyeColorRight = 1ul << CustomizeIndex.EyeColorRight,
HairColor = 1ul << CustomizeIndex.HairColor,
HighlightsColor = 1ul << CustomizeIndex.HighlightsColor,
FacialFeature1 = 1ul << CustomizeIndex.FacialFeature1,
FacialFeature2 = 1ul << CustomizeIndex.FacialFeature2,
FacialFeature3 = 1ul << CustomizeIndex.FacialFeature3,
FacialFeature4 = 1ul << CustomizeIndex.FacialFeature4,
FacialFeature5 = 1ul << CustomizeIndex.FacialFeature5,
FacialFeature6 = 1ul << CustomizeIndex.FacialFeature6,
FacialFeature7 = 1ul << CustomizeIndex.FacialFeature7,
LegacyTattoo = 1ul << CustomizeIndex.LegacyTattoo,
TattooColor = 1ul << CustomizeIndex.TattooColor,
Eyebrows = 1ul << CustomizeIndex.Eyebrows,
EyeColorLeft = 1ul << CustomizeIndex.EyeColorLeft,
EyeShape = 1ul << CustomizeIndex.EyeShape,
SmallIris = 1ul << CustomizeIndex.SmallIris,
Nose = 1ul << CustomizeIndex.Nose,
Jaw = 1ul << CustomizeIndex.Jaw,
Mouth = 1ul << CustomizeIndex.Mouth,
Lipstick = 1ul << CustomizeIndex.Lipstick,
LipColor = 1ul << CustomizeIndex.LipColor,
MuscleMass = 1ul << CustomizeIndex.MuscleMass,
TailShape = 1ul << CustomizeIndex.TailShape,
BustSize = 1ul << CustomizeIndex.BustSize,
FacePaint = 1ul << CustomizeIndex.FacePaint,
FacePaintReversed = 1ul << CustomizeIndex.FacePaintReversed,
FacePaintColor = 1ul << CustomizeIndex.FacePaintColor,
}
public static class CustomizeFlagExtensions
{
public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul);
public const CustomizeFlag AllRelevant = All & ~CustomizeFlag.BodyType & ~CustomizeFlag.Race;
public const CustomizeFlag RedrawRequired =
CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face | CustomizeFlag.BodyType;
public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizationSet set)
=> flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender);
public static bool RequiresRedraw(this CustomizeFlag flags)
=> (flags & RedrawRequired) != 0;
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static CustomizeIndex ToIndex(this CustomizeFlag flag)
=> flag switch
{
CustomizeFlag.Race => CustomizeIndex.Race,
CustomizeFlag.Gender => CustomizeIndex.Gender,
CustomizeFlag.BodyType => CustomizeIndex.BodyType,
CustomizeFlag.Height => CustomizeIndex.Height,
CustomizeFlag.Clan => CustomizeIndex.Clan,
CustomizeFlag.Face => CustomizeIndex.Face,
CustomizeFlag.Hairstyle => CustomizeIndex.Hairstyle,
CustomizeFlag.Highlights => CustomizeIndex.Highlights,
CustomizeFlag.SkinColor => CustomizeIndex.SkinColor,
CustomizeFlag.EyeColorRight => CustomizeIndex.EyeColorRight,
CustomizeFlag.HairColor => CustomizeIndex.HairColor,
CustomizeFlag.HighlightsColor => CustomizeIndex.HighlightsColor,
CustomizeFlag.FacialFeature1 => CustomizeIndex.FacialFeature1,
CustomizeFlag.FacialFeature2 => CustomizeIndex.FacialFeature2,
CustomizeFlag.FacialFeature3 => CustomizeIndex.FacialFeature3,
CustomizeFlag.FacialFeature4 => CustomizeIndex.FacialFeature4,
CustomizeFlag.FacialFeature5 => CustomizeIndex.FacialFeature5,
CustomizeFlag.FacialFeature6 => CustomizeIndex.FacialFeature6,
CustomizeFlag.FacialFeature7 => CustomizeIndex.FacialFeature7,
CustomizeFlag.LegacyTattoo => CustomizeIndex.LegacyTattoo,
CustomizeFlag.TattooColor => CustomizeIndex.TattooColor,
CustomizeFlag.Eyebrows => CustomizeIndex.Eyebrows,
CustomizeFlag.EyeColorLeft => CustomizeIndex.EyeColorLeft,
CustomizeFlag.EyeShape => CustomizeIndex.EyeShape,
CustomizeFlag.SmallIris => CustomizeIndex.SmallIris,
CustomizeFlag.Nose => CustomizeIndex.Nose,
CustomizeFlag.Jaw => CustomizeIndex.Jaw,
CustomizeFlag.Mouth => CustomizeIndex.Mouth,
CustomizeFlag.Lipstick => CustomizeIndex.Lipstick,
CustomizeFlag.LipColor => CustomizeIndex.LipColor,
CustomizeFlag.MuscleMass => CustomizeIndex.MuscleMass,
CustomizeFlag.TailShape => CustomizeIndex.TailShape,
CustomizeFlag.BustSize => CustomizeIndex.BustSize,
CustomizeFlag.FacePaint => CustomizeIndex.FacePaint,
CustomizeFlag.FacePaintReversed => CustomizeIndex.FacePaintReversed,
CustomizeFlag.FacePaintColor => CustomizeIndex.FacePaintColor,
_ => (CustomizeIndex)byte.MaxValue,
};
public static bool SetIfDifferent(ref this CustomizeFlag flags, CustomizeFlag flag, bool value)
{
var newValue = value ? flags | flag : flags & ~flag;
if (newValue == flags)
return false;
flags = newValue;
return true;
}
}

View file

@ -0,0 +1,183 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Glamourer.Customization;
public enum CustomizeIndex : byte
{
Race,
Gender,
BodyType,
Height,
Clan,
Face,
Hairstyle,
Highlights,
SkinColor,
EyeColorRight,
HairColor,
HighlightsColor,
FacialFeature1,
FacialFeature2,
FacialFeature3,
FacialFeature4,
FacialFeature5,
FacialFeature6,
FacialFeature7,
LegacyTattoo,
TattooColor,
Eyebrows,
EyeColorLeft,
EyeShape,
SmallIris,
Nose,
Jaw,
Mouth,
Lipstick,
LipColor,
MuscleMass,
TailShape,
BustSize,
FacePaint,
FacePaintReversed,
FacePaintColor,
}
public static class CustomizationExtensions
{
public const int NumIndices = (int)CustomizeIndex.FacePaintColor + 1;
public static readonly CustomizeIndex[] All = Enum.GetValues<CustomizeIndex>()
.Where(v => v is not CustomizeIndex.Race and not CustomizeIndex.BodyType).ToArray();
public static readonly CustomizeIndex[] AllBasic = All
.Where(v => v is not CustomizeIndex.Gender and not CustomizeIndex.Clan).ToArray();
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static (int ByteIdx, byte Mask) ToByteAndMask(this CustomizeIndex index)
=> index switch
{
CustomizeIndex.Race => (0, 0xFF),
CustomizeIndex.Gender => (1, 0xFF),
CustomizeIndex.BodyType => (2, 0xFF),
CustomizeIndex.Height => (3, 0xFF),
CustomizeIndex.Clan => (4, 0xFF),
CustomizeIndex.Face => (5, 0xFF),
CustomizeIndex.Hairstyle => (6, 0xFF),
CustomizeIndex.Highlights => (7, 0x80),
CustomizeIndex.SkinColor => (8, 0xFF),
CustomizeIndex.EyeColorRight => (9, 0xFF),
CustomizeIndex.HairColor => (10, 0xFF),
CustomizeIndex.HighlightsColor => (11, 0xFF),
CustomizeIndex.FacialFeature1 => (12, 0x01),
CustomizeIndex.FacialFeature2 => (12, 0x02),
CustomizeIndex.FacialFeature3 => (12, 0x04),
CustomizeIndex.FacialFeature4 => (12, 0x08),
CustomizeIndex.FacialFeature5 => (12, 0x10),
CustomizeIndex.FacialFeature6 => (12, 0x20),
CustomizeIndex.FacialFeature7 => (12, 0x40),
CustomizeIndex.LegacyTattoo => (12, 0x80),
CustomizeIndex.TattooColor => (13, 0xFF),
CustomizeIndex.Eyebrows => (14, 0xFF),
CustomizeIndex.EyeColorLeft => (15, 0xFF),
CustomizeIndex.EyeShape => (16, 0x7F),
CustomizeIndex.SmallIris => (16, 0x80),
CustomizeIndex.Nose => (17, 0xFF),
CustomizeIndex.Jaw => (18, 0xFF),
CustomizeIndex.Mouth => (19, 0x7F),
CustomizeIndex.Lipstick => (19, 0x80),
CustomizeIndex.LipColor => (20, 0xFF),
CustomizeIndex.MuscleMass => (21, 0xFF),
CustomizeIndex.TailShape => (22, 0xFF),
CustomizeIndex.BustSize => (23, 0xFF),
CustomizeIndex.FacePaint => (24, 0x7F),
CustomizeIndex.FacePaintReversed => (24, 0x80),
CustomizeIndex.FacePaintColor => (25, 0xFF),
_ => (0, 0x00),
};
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static CustomizeFlag ToFlag(this CustomizeIndex index)
=> (CustomizeFlag)(1ul << (int)index);
public static string ToDefaultName(this CustomizeIndex customizeIndex)
=> customizeIndex switch
{
CustomizeIndex.Race => "Race",
CustomizeIndex.Gender => "Gender",
CustomizeIndex.BodyType => "Body Type",
CustomizeIndex.Height => "Height",
CustomizeIndex.Clan => "Clan",
CustomizeIndex.Face => "Head Style",
CustomizeIndex.Hairstyle => "Hair Style",
CustomizeIndex.Highlights => "Highlights",
CustomizeIndex.SkinColor => "Skin Color",
CustomizeIndex.EyeColorRight => "Right Eye Color",
CustomizeIndex.HairColor => "Hair Color",
CustomizeIndex.HighlightsColor => "Highlights Color",
CustomizeIndex.TattooColor => "Tattoo Color",
CustomizeIndex.Eyebrows => "Eyebrow Style",
CustomizeIndex.EyeColorLeft => "Left Eye Color",
CustomizeIndex.EyeShape => "Small Pupils",
CustomizeIndex.Nose => "Nose Style",
CustomizeIndex.Jaw => "Jaw Style",
CustomizeIndex.Mouth => "Mouth Style",
CustomizeIndex.MuscleMass => "Muscle Tone",
CustomizeIndex.TailShape => "Tail Shape",
CustomizeIndex.BustSize => "Bust Size",
CustomizeIndex.FacePaint => "Face Paint",
CustomizeIndex.FacePaintColor => "Face Paint Color",
CustomizeIndex.LipColor => "Lip Color",
CustomizeIndex.FacialFeature1 => "Facial Feature 1",
CustomizeIndex.FacialFeature2 => "Facial Feature 2",
CustomizeIndex.FacialFeature3 => "Facial Feature 3",
CustomizeIndex.FacialFeature4 => "Facial Feature 4",
CustomizeIndex.FacialFeature5 => "Facial Feature 5",
CustomizeIndex.FacialFeature6 => "Facial Feature 6",
CustomizeIndex.FacialFeature7 => "Facial Feature 7",
CustomizeIndex.LegacyTattoo => "Legacy Tattoo",
CustomizeIndex.SmallIris => "Small Iris",
CustomizeIndex.Lipstick => "Enable Lipstick",
CustomizeIndex.FacePaintReversed => "Reverse Face Paint",
_ => string.Empty,
};
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static unsafe CustomizeValue Get(this in Penumbra.GameData.Structs.CustomizeData data, CustomizeIndex index)
{
var (offset, mask) = index.ToByteAndMask();
return (CustomizeValue)(data.Data[offset] & mask);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static unsafe bool Set(this ref Penumbra.GameData.Structs.CustomizeData data, CustomizeIndex index, CustomizeValue value)
{
var (offset, mask) = index.ToByteAndMask();
return mask != 0xFF
? SetIfDifferentMasked(ref data.Data[offset], value, mask)
: SetIfDifferent(ref data.Data[offset], value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool SetIfDifferentMasked(ref byte oldValue, CustomizeValue newValue, byte mask)
{
var tmp = (byte)(newValue.Value & mask);
tmp = (byte)(tmp | (oldValue & ~mask));
if (oldValue == tmp)
return false;
oldValue = tmp;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool SetIfDifferent(ref byte oldValue, CustomizeValue newValue)
{
if (oldValue == newValue.Value)
return false;
oldValue = newValue.Value;
return true;
}
}

View file

@ -0,0 +1,34 @@
namespace Glamourer.Customization;
public record struct CustomizeValue(byte Value)
{
public static readonly CustomizeValue Zero = new(0);
public static readonly CustomizeValue Max = new(0xFF);
public static CustomizeValue Bool(bool b)
=> b ? Max : Zero;
public static explicit operator CustomizeValue(byte value)
=> new(value);
public static CustomizeValue operator ++(CustomizeValue v)
=> new(++v.Value);
public static CustomizeValue operator --(CustomizeValue v)
=> new(--v.Value);
public static bool operator <(CustomizeValue v, int count)
=> v.Value < count;
public static bool operator >(CustomizeValue v, int count)
=> v.Value > count;
public static CustomizeValue operator +(CustomizeValue v, int rhs)
=> new((byte)(v.Value + rhs));
public static CustomizeValue operator -(CustomizeValue v, int rhs)
=> new((byte)(v.Value - rhs));
public override string ToString()
=> Value.ToString();
}

View file

@ -0,0 +1,148 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Memory;
namespace Glamourer.Customization;
[StructLayout(LayoutKind.Explicit, Size = Size)]
public unsafe struct DatCharacterFile
{
public const int Size = 4 + 4 + 4 + 4 + Penumbra.GameData.Structs.CustomizeData.Size + 2 + 4 + 41 * 4; // 212
[FieldOffset(0)]
private fixed byte _data[Size];
[FieldOffset(0)]
public readonly uint Magic = 0x2013FF14;
[FieldOffset(4)]
public readonly uint Version = 0x05;
[FieldOffset(8)]
private uint _checksum;
[FieldOffset(12)]
private readonly uint _padding = 0;
[FieldOffset(16)]
private Penumbra.GameData.Structs.CustomizeData _customize;
[FieldOffset(16 + Penumbra.GameData.Structs.CustomizeData.Size)]
private ushort _voice;
[FieldOffset(16 + Penumbra.GameData.Structs.CustomizeData.Size + 2)]
private uint _timeStamp;
[FieldOffset(Size - 41 * 4)]
private fixed byte _description[41 * 4];
public readonly void Write(Stream stream)
{
for (var i = 0; i < Size; ++i)
stream.WriteByte(_data[i]);
}
public static bool Read(Stream stream, out DatCharacterFile file)
{
if (stream.Length - stream.Position != Size)
{
file = default;
return false;
}
file = new DatCharacterFile(stream);
return true;
}
private DatCharacterFile(Stream stream)
{
for (var i = 0; i < Size; ++i)
_data[i] = (byte)stream.ReadByte();
}
public DatCharacterFile(in Customize customize, byte voice, string text)
{
SetCustomize(customize);
SetVoice(voice);
SetTime(DateTimeOffset.UtcNow);
SetDescription(text);
_checksum = CalculateChecksum();
}
public readonly uint CalculateChecksum()
{
var ret = 0u;
for (var i = 16; i < Size; i++)
ret ^= (uint)(_data[i] << ((i - 16) % 24));
return ret;
}
public readonly uint Checksum
=> _checksum;
public Customize Customize
{
readonly get => new(_customize);
set
{
SetCustomize(value);
_checksum = CalculateChecksum();
}
}
public ushort Voice
{
readonly get => _voice;
set
{
SetVoice(value);
_checksum = CalculateChecksum();
}
}
public string Description
{
readonly get
{
fixed (byte* ptr = _description)
{
return MemoryHelper.ReadStringNullTerminated((nint)ptr);
}
}
set
{
SetDescription(value);
_checksum = CalculateChecksum();
}
}
public DateTimeOffset Time
{
readonly get => DateTimeOffset.FromUnixTimeSeconds(_timeStamp);
set
{
SetTime(value);
_checksum = CalculateChecksum();
}
}
private void SetTime(DateTimeOffset time)
=> _timeStamp = (uint)time.ToUnixTimeSeconds();
private void SetCustomize(in Customize customize)
=> _customize = customize.Data.Clone();
private void SetVoice(ushort voice)
=> _voice = voice;
private void SetDescription(string text)
{
fixed (byte* ptr = _description)
{
var span = new Span<byte>(ptr, 41 * 4);
Encoding.UTF8.GetBytes(text.AsSpan(0, Math.Min(40, text.Length)), span);
}
}
}

View file

@ -0,0 +1,17 @@
using System.Collections.Generic;
using Dalamud.Interface.Internal;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization;
public interface ICustomizationManager
{
public IReadOnlyList<Race> Races { get; }
public IReadOnlyList<SubRace> Clans { get; }
public IReadOnlyList<Gender> Genders { get; }
public CustomizationSet GetList(SubRace race, Gender gender);
public IDalamudTextureWrap GetIcon(uint iconId);
public string GetName(CustomName name);
}

View file

@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud;
using Dalamud.Plugin.Services;
using Glamourer.Structs;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer;
public static class GameData
{
private static Dictionary<byte, Job>? _jobs;
private static Dictionary<ushort, JobGroup>? _jobGroups;
private static JobGroup[]? _allJobGroups;
public static IReadOnlyDictionary<byte, Job> Jobs(IDataManager dataManager)
{
if (_jobs != null)
return _jobs;
var sheet = dataManager.GetExcelSheet<ClassJob>()!;
_jobs = sheet.Where(j => j.Abbreviation.RawData.Length > 0).ToDictionary(j => (byte)j.RowId, j => new Job(j));
return _jobs;
}
public static IReadOnlyList<JobGroup> AllJobGroups(IDataManager dataManager)
{
if (_allJobGroups != null)
return _allJobGroups;
var sheet = dataManager.GetExcelSheet<ClassJobCategory>()!;
var jobs = dataManager.GetExcelSheet<ClassJob>(ClientLanguage.English)!;
_allJobGroups = sheet.Select(j => new JobGroup(j, jobs)).ToArray();
return _allJobGroups;
}
public static IReadOnlyDictionary<ushort, JobGroup> JobGroups(IDataManager dataManager)
{
if (_jobGroups != null)
return _jobGroups;
static bool ValidIndex(uint idx)
{
if (idx is > 0 and < 36)
return true;
return idx switch
{
// Single jobs and big groups
91 => true,
92 => true,
96 => true,
98 => true,
99 => true,
111 => true,
112 => true,
129 => true,
149 => true,
150 => true,
156 => true,
157 => true,
158 => true,
159 => true,
180 => true,
181 => true,
188 => true,
189 => true,
// Class + Job
38 => true,
41 => true,
44 => true,
47 => true,
50 => true,
53 => true,
55 => true,
69 => true,
68 => true,
93 => true,
_ => false,
};
}
_jobGroups = AllJobGroups(dataManager).Where(j => ValidIndex(j.Id))
.ToDictionary(j => (ushort) j.Id, j => j);
return _jobGroups;
}
}

View file

@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>preview</LangVersion>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer.GameData</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DALAMUD_ROOT)\Dalamud.dll</HintPath>
<HintPath>..\libs\Dalamud.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DALAMUD_ROOT)\Lumina.dll</HintPath>
<HintPath>..\libs\Lumina.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DALAMUD_ROOT)\Lumina.Excel.dll</HintPath>
<HintPath>..\libs\Lumina.Excel.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="Util\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,72 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer.GameData</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
<ProjectReference Include="..\OtterGui\OtterGui.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,19 @@
namespace Glamourer;
public static class Offsets
{
public static class Character
{
public const int ClassJobContainer = 0x1A8;
}
public const byte DrawObjectVisorStateFlag = 0x40;
public const byte DrawObjectVisorToggleFlag = 0x80;
}
public static class Sigs
{
public const string ChangeJob = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 80 61";
public const string FlagSlotForUpdate = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A";
public const string ChangeCustomize = "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86";
}

View file

@ -0,0 +1,93 @@
using System;
using Penumbra.GameData.Enums;
namespace Glamourer.Structs;
[Flags]
public enum EquipFlag : uint
{
Head = 0x00000001,
Body = 0x00000002,
Hands = 0x00000004,
Legs = 0x00000008,
Feet = 0x00000010,
Ears = 0x00000020,
Neck = 0x00000040,
Wrist = 0x00000080,
RFinger = 0x00000100,
LFinger = 0x00000200,
Mainhand = 0x00000400,
Offhand = 0x00000800,
HeadStain = 0x00001000,
BodyStain = 0x00002000,
HandsStain = 0x00004000,
LegsStain = 0x00008000,
FeetStain = 0x00010000,
EarsStain = 0x00020000,
NeckStain = 0x00040000,
WristStain = 0x00080000,
RFingerStain = 0x00100000,
LFingerStain = 0x00200000,
MainhandStain = 0x00400000,
OffhandStain = 0x00800000,
}
public static class EquipFlagExtensions
{
public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1);
public const int NumEquipFlags = 24;
public static EquipFlag ToFlag(this EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => EquipFlag.Mainhand,
EquipSlot.OffHand => EquipFlag.Offhand,
EquipSlot.Head => EquipFlag.Head,
EquipSlot.Body => EquipFlag.Body,
EquipSlot.Hands => EquipFlag.Hands,
EquipSlot.Legs => EquipFlag.Legs,
EquipSlot.Feet => EquipFlag.Feet,
EquipSlot.Ears => EquipFlag.Ears,
EquipSlot.Neck => EquipFlag.Neck,
EquipSlot.Wrists => EquipFlag.Wrist,
EquipSlot.RFinger => EquipFlag.RFinger,
EquipSlot.LFinger => EquipFlag.LFinger,
_ => 0,
};
public static EquipFlag ToStainFlag(this EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => EquipFlag.MainhandStain,
EquipSlot.OffHand => EquipFlag.OffhandStain,
EquipSlot.Head => EquipFlag.HeadStain,
EquipSlot.Body => EquipFlag.BodyStain,
EquipSlot.Hands => EquipFlag.HandsStain,
EquipSlot.Legs => EquipFlag.LegsStain,
EquipSlot.Feet => EquipFlag.FeetStain,
EquipSlot.Ears => EquipFlag.EarsStain,
EquipSlot.Neck => EquipFlag.NeckStain,
EquipSlot.Wrists => EquipFlag.WristStain,
EquipSlot.RFinger => EquipFlag.RFingerStain,
EquipSlot.LFinger => EquipFlag.LFingerStain,
_ => 0,
};
public static EquipFlag ToBothFlags(this EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain,
EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain,
EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain,
EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain,
EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain,
EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain,
EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain,
EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain,
EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain,
EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain,
EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain,
EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain,
_ => 0,
};
}

View file

@ -0,0 +1,29 @@
using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Structs;
// A struct containing the different jobs the game supports.
// Also contains the jobs Name and Abbreviation as strings.
public readonly struct Job
{
public readonly string Name;
public readonly string Abbreviation;
public readonly ClassJob Base;
public uint Id
=> Base.RowId;
public JobFlag Flag
=> (JobFlag)(1ul << (int)Base.RowId);
public Job(ClassJob job)
{
Base = job;
Name = job.Name.ToDalamudString().ToString();
Abbreviation = job.Abbreviation.ToDalamudString().ToString();
}
public override string ToString()
=> Name;
}

View file

@ -0,0 +1,61 @@
using System;
using System.Diagnostics;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Structs;
[Flags]
public enum JobFlag : ulong
{ }
// The game specifies different job groups that can contain specific jobs or not.
public readonly struct JobGroup
{
public readonly string Name;
public readonly int Count;
public readonly uint Id;
private readonly JobFlag _flags;
// Create a job group from a given category and the ClassJob sheet.
// It looks up the different jobs contained in the category and sets the flags appropriately.
public JobGroup(ClassJobCategory group, ExcelSheet<ClassJob> jobs)
{
Count = 0;
_flags = 0ul;
Id = group.RowId;
Name = group.Name.ToString();
Debug.Assert(jobs.RowCount < 64, $"Number of Jobs exceeded 63 ({jobs.RowCount}).");
foreach (var job in jobs)
{
var abbr = job.Abbreviation.ToString();
if (abbr.Length == 0)
continue;
var prop = group.GetType().GetProperty(abbr);
Debug.Assert(prop != null, $"Could not get job abbreviation {abbr} property.");
if (!(bool)prop.GetValue(group)!)
continue;
++Count;
_flags |= (JobFlag)(1ul << (int)job.RowId);
}
}
// Check if a job is contained inside this group.
public bool Fits(Job job)
=> _flags.HasFlag(job.Flag);
// Check if any of the jobs in the given flags fit this group.
public bool Fits(JobFlag flag)
=> (_flags & flag) != 0;
// Check if a job is contained inside this group.
public bool Fits(uint jobId)
{
var flag = (JobFlag)(1ul << (int)jobId);
return _flags.HasFlag(flag);
}
}

View file

@ -6,12 +6,11 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\release.yml = .github\workflows\release.yml
Glamourer\Glamourer.json = Glamourer\Glamourer.json
repo.json = repo.json
.github\workflows\test_release.yml = .github\workflows\test_release.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.GameData", "Glamourer.GameData\Glamourer.GameData.csproj", "{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{01EB903D-871F-4285-A8CF-6486561D5B5B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}"
@ -22,38 +21,36 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "OtterGui\OtterGui.csproj", "{EF233CE2-F243-449E-BE05-72B9D110E419}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.Api", "Glamourer.Api\Glamourer.Api.csproj", "{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|x64
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|x64
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|x64
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|x64
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|x64
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|x64
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.ActiveCfg = Debug|x64
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Debug|Any CPU.Build.0 = Debug|x64
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.ActiveCfg = Release|x64
{9B46691B-FAB2-4CC3-9B89-C8B91A590F47}.Release|Any CPU.Build.0 = Release|x64
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.Build.0 = Release|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01EB903D-871F-4285-A8CF-6486561D5B5B}.Release|Any CPU.Build.0 = Release|Any CPU
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29C589ED-7AF1-4DE9-82EF-33EBEF19AAFA}.Release|Any CPU.Build.0 = Release|Any CPU
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C0A2FAF8-C3AE-4B7B-ADDB-4AAC1A855428}.Release|Any CPU.Build.0 = Release|Any CPU
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AAFE22E7-0F9B-462A-AAA3-6EE3B268F3F8}.Release|Any CPU.Build.0 = Release|Any CPU
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF233CE2-F243-449E-BE05-72B9D110E419}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF233CE2-F243-449E-BE05-72B9D110E419}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -1,133 +0,0 @@
using Glamourer.Api.Enums;
using Glamourer.Designs;
using Glamourer.State;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.String;
namespace Glamourer.Api;
public class ApiHelpers(ActorObjectManager objects, StateManager stateManager, ActorManager actors) : IApiService
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal IEnumerable<ActorState> FindExistingStates(string actorName, ushort worldId = ushort.MaxValue)
{
if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString))
yield break;
if (worldId == WorldId.AnyWorld.Id)
{
foreach (var state in stateManager.Values.Where(state
=> state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString))
yield return state;
}
else
{
var identifier = actors.CreatePlayer(byteString, worldId);
if (stateManager.TryGetValue(identifier, out var state))
yield return state;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal GlamourerApiEc FindExistingState(int objectIndex, out ActorState? state)
{
var actor = objects.Objects[objectIndex];
var identifier = actor.GetIdentifier(actors);
if (!identifier.IsValid)
{
state = null;
return GlamourerApiEc.ActorNotFound;
}
stateManager.TryGetValue(identifier, out state);
return GlamourerApiEc.Success;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal ActorState? FindState(int objectIndex)
{
var actor = objects.Objects[objectIndex];
var identifier = actor.GetIdentifier(actors);
if (identifier.IsValid && stateManager.GetOrCreate(identifier, actor, out var state))
return state;
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static DesignBase.FlagRestrictionResetter Restrict(DesignBase design, ApplyFlag flags)
=> (flags & (ApplyFlag.Equipment | ApplyFlag.Customization)) switch
{
ApplyFlag.Equipment => design.TemporarilyRestrictApplication(ApplicationCollection.Equipment),
ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.Customizations),
ApplyFlag.Equipment | ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.All),
_ => design.TemporarilyRestrictApplication(ApplicationCollection.None),
};
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static void Lock(ActorState state, uint key, ApplyFlag flags)
{
if ((flags & ApplyFlag.Lock) != 0 && key != 0)
state.Lock(key);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal IEnumerable<ActorState> FindStates(string objectName)
{
if (objectName.Length == 0 || !ByteString.FromString(objectName, out var byteString))
return [];
return stateManager.Values.Where(state => state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString)
.Concat(objects
.Where(kvp => kvp.Key is { IsValid: true, Type: IdentifierType.Player } && kvp.Key.PlayerName == byteString)
.SelectWhere(kvp =>
{
if (stateManager.ContainsKey(kvp.Key))
return (false, null);
var ret = stateManager.GetOrCreate(kvp.Key, kvp.Value.Objects[0], out var state);
return (ret, state);
}));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static GlamourerApiEc Return(GlamourerApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown")
{
if (ec is GlamourerApiEc.Success or GlamourerApiEc.NothingDone)
Glamourer.Log.Verbose($"[{name}] Called with {args}, returned {ec}.");
else
Glamourer.Log.Debug($"[{name}] Called with {args}, returned {ec}.");
return ec;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal static LazyString Args(params object[] arguments)
{
if (arguments.Length == 0)
return new LazyString(() => "no arguments");
return new LazyString(() =>
{
var sb = new StringBuilder();
for (var i = 0; i < arguments.Length / 2; ++i)
{
sb.Append(arguments[2 * i]);
sb.Append(" = ");
if (arguments[2 * i + 1] is IEnumerable e)
sb.Append($"[{string.Join(',', e)}]");
else
sb.Append(arguments[2 * i + 1]);
sb.Append(", ");
}
return sb.ToString(0, sb.Length - 2);
});
}
}

View file

@ -1,138 +0,0 @@
using Glamourer.Api.Api;
using Glamourer.Api.Enums;
using Glamourer.Designs;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using OtterGui.Services;
namespace Glamourer.Api;
public class DesignsApi(
ApiHelpers helpers,
DesignManager designs,
StateManager stateManager,
DesignFileSystem fileSystem,
DesignColors color,
DesignConverter converter)
: IGlamourerApiDesigns, IApiService
{
public Dictionary<Guid, string> GetDesignList()
=> designs.Designs.ToDictionary(d => d.Identifier, d => d.Name.Text);
public Dictionary<Guid, (string DisplayName, string FullPath, uint DisplayColor, bool ShownInQdb)> GetDesignListExtended()
=> fileSystem.ToDictionary(kvp => kvp.Key.Identifier,
kvp => (kvp.Key.Name.Text, kvp.Value.FullName(), color.GetColor(kvp.Key), kvp.Key.QuickDesign));
public (string DisplayName, string FullPath, uint DisplayColor, bool ShowInQdb) GetExtendedDesignData(Guid designId)
=> designs.Designs.ByIdentifier(designId) is { } d
? (d.Name.Text, fileSystem.TryGetValue(d, out var leaf) ? leaf.FullName() : d.Name.Text, color.GetColor(d), d.QuickDesign)
: (string.Empty, string.Empty, 0, false);
public GlamourerApiEc ApplyDesign(Guid designId, int objectIndex, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Design", designId, "Index", objectIndex, "Key", key, "Flags", flags);
var design = designs.Designs.ByIdentifier(designId);
if (design == null)
return ApiHelpers.Return(GlamourerApiEc.DesignNotFound, args);
if (helpers.FindState(objectIndex) is not { } state)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
ApplyDesign(state, design, key, flags);
ApiHelpers.Lock(state, key, flags);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
private void ApplyDesign(ActorState state, Design design, uint key, ApplyFlag flags)
{
var once = (flags & ApplyFlag.Once) != 0;
var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true,
ResetMaterials: !once && key != 0, IsFinal: true);
using var restrict = ApiHelpers.Restrict(design, flags);
stateManager.ApplyDesign(state, design, settings);
}
public GlamourerApiEc ApplyDesignName(Guid designId, string playerName, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Design", designId, "Name", playerName, "Key", key, "Flags", flags);
var design = designs.Designs.ByIdentifier(designId);
if (design == null)
return ApiHelpers.Return(GlamourerApiEc.DesignNotFound, args);
var any = false;
var anyUnlocked = false;
foreach (var state in helpers.FindStates(playerName))
{
any = true;
if (!state.CanUnlock(key))
continue;
anyUnlocked = true;
ApplyDesign(state, design, key, flags);
ApiHelpers.Lock(state, key, flags);
}
if (!any)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!anyUnlocked)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public (GlamourerApiEc, Guid) AddDesign(string designInput, string name)
{
var args = ApiHelpers.Args("DesignData", designInput, "Name", name);
if (converter.FromBase64(designInput, true, true, out _) is not { } designBase)
try
{
var jObj = JObject.Parse(designInput);
designBase = converter.FromJObject(jObj, true, true);
if (designBase is null)
return (ApiHelpers.Return(GlamourerApiEc.CouldNotParse, args), Guid.Empty);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Failure parsing data for AddDesign due to\n{ex}");
return (ApiHelpers.Return(GlamourerApiEc.CouldNotParse, args), Guid.Empty);
}
try
{
var design = designBase is Design d
? designs.CreateClone(d, name, true)
: designs.CreateClone(designBase, name, true);
return (ApiHelpers.Return(GlamourerApiEc.Success, args), design.Identifier);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Unknown error creating design via IPC:\n{ex}");
return (ApiHelpers.Return(GlamourerApiEc.UnknownError, args), Guid.Empty);
}
}
public GlamourerApiEc DeleteDesign(Guid designId)
{
var args = ApiHelpers.Args("DesignId", designId);
if (designs.Designs.ByIdentifier(designId) is not { } design)
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
designs.Delete(design);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public string? GetDesignBase64(Guid designId)
=> designs.Designs.ByIdentifier(designId) is { } design
? converter.ShareBase64(design)
: null;
public JObject? GetDesignJObject(Guid designId)
=> designs.Designs.ByIdentifier(designId) is { } design
? converter.ShareJObject(design)
: null;
}

View file

@ -1,25 +0,0 @@
using Glamourer.Api.Api;
using OtterGui.Services;
namespace Glamourer.Api;
public class GlamourerApi(Configuration config, DesignsApi designs, StateApi state, ItemsApi items) : IGlamourerApi, IApiService
{
public const int CurrentApiVersionMajor = 1;
public const int CurrentApiVersionMinor = 7;
public (int Major, int Minor) ApiVersion
=> (CurrentApiVersionMajor, CurrentApiVersionMinor);
public bool AutoReloadGearEnabled
=> config.AutoRedrawEquipOnChanges;
public IGlamourerApiDesigns Designs
=> designs;
public IGlamourerApiItems Items
=> items;
public IGlamourerApiState State
=> state;
}

View file

@ -0,0 +1,26 @@
using System;
using Dalamud.Plugin;
using Penumbra.Api.Helpers;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public const string LabelApiVersion = "Glamourer.ApiVersion";
public const string LabelApiVersions = "Glamourer.ApiVersions";
private readonly FuncProvider<int> _apiVersionProvider;
private readonly FuncProvider<(int Major, int Minor)> _apiVersionsProvider;
public static FuncSubscriber<int> ApiVersionSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApiVersion);
public static FuncSubscriber<(int Major, int Minor)> ApiVersionsSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApiVersions);
public int ApiVersion()
=> CurrentApiVersionMajor;
public (int Major, int Minor) ApiVersions()
=> (CurrentApiVersionMajor, CurrentApiVersionMinor);
}

View file

@ -0,0 +1,121 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public const string LabelApplyAll = "Glamourer.ApplyAll";
public const string LabelApplyAllToCharacter = "Glamourer.ApplyAllToCharacter";
public const string LabelApplyOnlyEquipment = "Glamourer.ApplyOnlyEquipment";
public const string LabelApplyOnlyEquipmentToCharacter = "Glamourer.ApplyOnlyEquipmentToCharacter";
public const string LabelApplyOnlyCustomization = "Glamourer.ApplyOnlyCustomization";
public const string LabelApplyOnlyCustomizationToCharacter = "Glamourer.ApplyOnlyCustomizationToCharacter";
public const string LabelApplyAllLock = "Glamourer.ApplyAllLock";
public const string LabelApplyAllToCharacterLock = "Glamourer.ApplyAllToCharacterLock";
public const string LabelApplyOnlyEquipmentLock = "Glamourer.ApplyOnlyEquipmentLock";
public const string LabelApplyOnlyEquipmentToCharacterLock = "Glamourer.ApplyOnlyEquipmentToCharacterLock";
public const string LabelApplyOnlyCustomizationLock = "Glamourer.ApplyOnlyCustomizationLock";
public const string LabelApplyOnlyCustomizationToCharacterLock = "Glamourer.ApplyOnlyCustomizationToCharacterLock";
private readonly ActionProvider<string, string> _applyAllProvider;
private readonly ActionProvider<string, Character?> _applyAllToCharacterProvider;
private readonly ActionProvider<string, string> _applyOnlyEquipmentProvider;
private readonly ActionProvider<string, Character?> _applyOnlyEquipmentToCharacterProvider;
private readonly ActionProvider<string, string> _applyOnlyCustomizationProvider;
private readonly ActionProvider<string, Character?> _applyOnlyCustomizationToCharacterProvider;
private readonly ActionProvider<string, string, uint> _applyAllProviderLock;
private readonly ActionProvider<string, Character?, uint> _applyAllToCharacterProviderLock;
private readonly ActionProvider<string, string, uint> _applyOnlyEquipmentProviderLock;
private readonly ActionProvider<string, Character?, uint> _applyOnlyEquipmentToCharacterProviderLock;
private readonly ActionProvider<string, string, uint> _applyOnlyCustomizationProviderLock;
private readonly ActionProvider<string, Character?, uint> _applyOnlyCustomizationToCharacterProviderLock;
public static ActionSubscriber<string, string> ApplyAllSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAll);
public static ActionSubscriber<string, Character?> ApplyAllToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyAllToCharacter);
public static ActionSubscriber<string, string> ApplyOnlyEquipmentSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyOnlyEquipment);
public static ActionSubscriber<string, Character?> ApplyOnlyEquipmentToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyOnlyEquipmentToCharacter);
public static ActionSubscriber<string, string> ApplyOnlyCustomizationSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyOnlyCustomization);
public static ActionSubscriber<string, Character?> ApplyOnlyCustomizationToCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelApplyOnlyCustomizationToCharacter);
public void ApplyAll(string base64, string characterName)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, 0);
public void ApplyAllToCharacter(string base64, Character? character)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, 0);
public void ApplyOnlyEquipment(string base64, string characterName)
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(characterName), version, 0);
public void ApplyOnlyEquipmentToCharacter(string base64, Character? character)
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(character), version, 0);
public void ApplyOnlyCustomization(string base64, string characterName)
=> ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(characterName), version, 0);
public void ApplyOnlyCustomizationToCharacter(string base64, Character? character)
=> ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(character), version, 0);
public void ApplyAllLock(string base64, string characterName, uint lockCode)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(characterName), version, lockCode);
public void ApplyAllToCharacterLock(string base64, Character? character, uint lockCode)
=> ApplyDesign(_designConverter.FromBase64(base64, true, true, out var version), FindActors(character), version, lockCode);
public void ApplyOnlyEquipmentLock(string base64, string characterName, uint lockCode)
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(characterName), version, lockCode);
public void ApplyOnlyEquipmentToCharacterLock(string base64, Character? character, uint lockCode)
=> ApplyDesign(_designConverter.FromBase64(base64, false, true, out var version), FindActors(character), version, lockCode);
public void ApplyOnlyCustomizationLock(string base64, string characterName, uint lockCode)
=> ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(characterName), version, lockCode);
public void ApplyOnlyCustomizationToCharacterLock(string base64, Character? character, uint lockCode)
=> ApplyDesign(_designConverter.FromBase64(base64, true, false, out var version), FindActors(character), version, lockCode);
private void ApplyDesign(DesignBase? design, IEnumerable<ActorIdentifier> actors, byte version, uint lockCode)
{
if (design == null)
return;
var hasModelId = version >= 3;
_objects.Update();
foreach (var id in actors)
{
if (!_stateManager.TryGetValue(id, out var state))
{
var data = _objects.TryGetValue(id, out var d) ? d : ActorData.Invalid;
if (!data.Valid || !_stateManager.GetOrCreate(id, data.Objects[0], out state))
continue;
}
if ((hasModelId || state.ModelData.ModelId == 0) && state.CanUnlock(lockCode))
{
_stateManager.ApplyDesign(design, state, StateChanged.Source.Ipc, lockCode);
state.Lock(lockCode);
}
}
}
}

View file

@ -0,0 +1,27 @@
using System;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Glamourer.State;
using Penumbra.Api.Helpers;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public const string LabelStateChanged = "Glamourer.StateChanged";
public const string LabelGPoseChanged = "Glamourer.GPoseChanged";
private readonly GPoseService _gPose;
private readonly StateChanged _stateChangedEvent;
private readonly EventProvider<StateChanged.Type, nint, Lazy<string>> _stateChangedProvider;
private readonly EventProvider<bool> _gPoseChangedProvider;
private void OnStateChanged(StateChanged.Type type, StateChanged.Source source, ActorState state, ActorData actors, object? data = null)
{
foreach (var actor in actors.Objects)
_stateChangedProvider.Invoke(type, actor.Address, new Lazy<string>(() => _designConverter.ShareBase64(state)));
}
private void OnGPoseChanged(bool value)
=> _gPoseChangedProvider.Invoke(value);
}

View file

@ -0,0 +1,51 @@
using System.Buffers.Text;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Structs;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public const string LabelGetAllCustomization = "Glamourer.GetAllCustomization";
public const string LabelGetAllCustomizationFromCharacter = "Glamourer.GetAllCustomizationFromCharacter";
private readonly FuncProvider<string, string?> _getAllCustomizationProvider;
private readonly FuncProvider<Character?, string?> _getAllCustomizationFromCharacterProvider;
public static FuncSubscriber<string, string?> GetAllCustomizationSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelGetAllCustomization);
public static FuncSubscriber<Character?, string?> GetAllCustomizationFromCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelGetAllCustomizationFromCharacter);
public string? GetAllCustomization(string characterName)
=> GetCustomization(FindActors(characterName));
public string? GetAllCustomizationFromCharacter(Character? character)
=> GetCustomization(FindActors(character));
private string? GetCustomization(IEnumerable<ActorIdentifier> actors)
{
var actor = actors.FirstOrDefault(ActorIdentifier.Invalid);
if (!actor.IsValid)
return null;
if (!_stateManager.TryGetValue(actor, out var state))
{
_objects.Update();
if (!_objects.TryGetValue(actor, out var data) || !data.Valid)
return null;
if (!_stateManager.GetOrCreate(actor, data.Objects[0], out state))
return null;
}
return _designConverter.ShareBase64(state);
}
}

View file

@ -0,0 +1,121 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Events;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public const string LabelRevert = "Glamourer.Revert";
public const string LabelRevertCharacter = "Glamourer.RevertCharacter";
public const string LabelRevertLock = "Glamourer.RevertLock";
public const string LabelRevertCharacterLock = "Glamourer.RevertCharacterLock";
public const string LabelRevertToAutomation = "Glamourer.RevertToAutomation";
public const string LabelRevertToAutomationCharacter = "Glamourer.RevertToAutomationCharacter";
public const string LabelUnlock = "Glamourer.Unlock";
public const string LabelUnlockName = "Glamourer.UnlockName";
private readonly ActionProvider<string> _revertProvider;
private readonly ActionProvider<Character?> _revertCharacterProvider;
private readonly ActionProvider<string, uint> _revertProviderLock;
private readonly ActionProvider<Character?, uint> _revertCharacterProviderLock;
private readonly FuncProvider<string, uint, bool> _revertToAutomationProvider;
private readonly FuncProvider<Character?, uint, bool> _revertToAutomationCharacterProvider;
private readonly FuncProvider<string, uint, bool> _unlockNameProvider;
private readonly FuncProvider<Character?, uint, bool> _unlockProvider;
public static ActionSubscriber<string> RevertSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelRevert);
public static ActionSubscriber<Character?> RevertCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelRevertCharacter);
public static ActionSubscriber<string> RevertLockSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelRevertLock);
public static ActionSubscriber<Character?> RevertCharacterLockSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelRevertCharacterLock);
public static FuncSubscriber<string, uint, bool> UnlockNameSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelUnlockName);
public static FuncSubscriber<Character?, uint, bool> UnlockSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelUnlock);
public static FuncSubscriber<string, uint, bool> RevertToAutomationSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelRevertToAutomation);
public static FuncSubscriber<Character?, uint, bool> RevertToAutomationCharacterSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelRevertToAutomationCharacter);
public void Revert(string characterName)
=> Revert(FindActorsRevert(characterName), 0);
public void RevertCharacter(Character? character)
=> Revert(FindActors(character), 0);
public void RevertLock(string characterName, uint lockCode)
=> Revert(FindActorsRevert(characterName), lockCode);
public void RevertCharacterLock(Character? character, uint lockCode)
=> Revert(FindActors(character), lockCode);
public bool Unlock(string characterName, uint lockCode)
=> Unlock(FindActorsRevert(characterName), lockCode);
public bool Unlock(Character? character, uint lockCode)
=> Unlock(FindActors(character), lockCode);
public bool RevertToAutomation(string characterName, uint lockCode)
=> RevertToAutomation(FindActorsRevert(characterName), lockCode);
public bool RevertToAutomation(Character? character, uint lockCode)
=> RevertToAutomation(FindActors(character), lockCode);
private void Revert(IEnumerable<ActorIdentifier> actors, uint lockCode)
{
foreach (var id in actors)
{
if (_stateManager.TryGetValue(id, out var state))
_stateManager.ResetState(state, StateChanged.Source.Ipc, lockCode);
}
}
private bool Unlock(IEnumerable<ActorIdentifier> actors, uint lockCode)
{
var ret = false;
foreach (var id in actors)
{
if (_stateManager.TryGetValue(id, out var state))
ret |= state.Unlock(lockCode);
}
return ret;
}
private bool RevertToAutomation(IEnumerable<ActorIdentifier> actors, uint lockCode)
{
var ret = false;
foreach (var id in actors)
{
if (_stateManager.TryGetValue(id, out var state))
{
ret |= state.Unlock(lockCode);
if (_objects.TryGetValue(id, out var data))
foreach (var obj in data.Objects)
{
_autoDesignApplier.ReapplyAutomation(obj, state.Identifier, state);
_stateManager.ReapplyState(obj);
}
}
}
return ret;
}
}

View file

@ -0,0 +1,151 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;
using Penumbra.String;
namespace Glamourer.Api;
public partial class GlamourerIpc : IDisposable
{
public const int CurrentApiVersionMajor = 0;
public const int CurrentApiVersionMinor = 4;
private readonly StateManager _stateManager;
private readonly ObjectManager _objects;
private readonly ActorService _actors;
private readonly DesignConverter _designConverter;
private readonly AutoDesignApplier _autoDesignApplier;
public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors,
DesignConverter designConverter, StateChanged stateChangedEvent, GPoseService gPose, AutoDesignApplier autoDesignApplier)
{
_stateManager = stateManager;
_objects = objects;
_actors = actors;
_designConverter = designConverter;
_autoDesignApplier = autoDesignApplier;
_gPose = gPose;
_stateChangedEvent = stateChangedEvent;
_apiVersionProvider = new FuncProvider<int>(pi, LabelApiVersion, ApiVersion);
_apiVersionsProvider = new FuncProvider<(int Major, int Minor)>(pi, LabelApiVersions, ApiVersions);
_getAllCustomizationProvider = new FuncProvider<string, string?>(pi, LabelGetAllCustomization, GetAllCustomization);
_getAllCustomizationFromCharacterProvider =
new FuncProvider<Character?, string?>(pi, LabelGetAllCustomizationFromCharacter, GetAllCustomizationFromCharacter);
_applyAllProvider = new ActionProvider<string, string>(pi, LabelApplyAll, ApplyAll);
_applyAllToCharacterProvider = new ActionProvider<string, Character?>(pi, LabelApplyAllToCharacter, ApplyAllToCharacter);
_applyOnlyEquipmentProvider = new ActionProvider<string, string>(pi, LabelApplyOnlyEquipment, ApplyOnlyEquipment);
_applyOnlyEquipmentToCharacterProvider =
new ActionProvider<string, Character?>(pi, LabelApplyOnlyEquipmentToCharacter, ApplyOnlyEquipmentToCharacter);
_applyOnlyCustomizationProvider = new ActionProvider<string, string>(pi, LabelApplyOnlyCustomization, ApplyOnlyCustomization);
_applyOnlyCustomizationToCharacterProvider =
new ActionProvider<string, Character?>(pi, LabelApplyOnlyCustomizationToCharacter, ApplyOnlyCustomizationToCharacter);
_applyAllProviderLock = new ActionProvider<string, string, uint>(pi, LabelApplyAllLock, ApplyAllLock);
_applyAllToCharacterProviderLock =
new ActionProvider<string, Character?, uint>(pi, LabelApplyAllToCharacterLock, ApplyAllToCharacterLock);
_applyOnlyEquipmentProviderLock = new ActionProvider<string, string, uint>(pi, LabelApplyOnlyEquipmentLock, ApplyOnlyEquipmentLock);
_applyOnlyEquipmentToCharacterProviderLock =
new ActionProvider<string, Character?, uint>(pi, LabelApplyOnlyEquipmentToCharacterLock, ApplyOnlyEquipmentToCharacterLock);
_applyOnlyCustomizationProviderLock =
new ActionProvider<string, string, uint>(pi, LabelApplyOnlyCustomizationLock, ApplyOnlyCustomizationLock);
_applyOnlyCustomizationToCharacterProviderLock =
new ActionProvider<string, Character?, uint>(pi, LabelApplyOnlyCustomizationToCharacterLock, ApplyOnlyCustomizationToCharacterLock);
_revertProvider = new ActionProvider<string>(pi, LabelRevert, Revert);
_revertCharacterProvider = new ActionProvider<Character?>(pi, LabelRevertCharacter, RevertCharacter);
_revertProviderLock = new ActionProvider<string, uint>(pi, LabelRevertLock, RevertLock);
_revertCharacterProviderLock = new ActionProvider<Character?, uint>(pi, LabelRevertCharacterLock, RevertCharacterLock);
_unlockNameProvider = new FuncProvider<string, uint, bool>(pi, LabelUnlockName, Unlock);
_unlockProvider = new FuncProvider<Character?, uint, bool>(pi, LabelUnlock, Unlock);
_revertToAutomationProvider = new FuncProvider<string, uint, bool>(pi, LabelRevertToAutomation, RevertToAutomation);
_revertToAutomationCharacterProvider =
new FuncProvider<Character?, uint, bool>(pi, LabelRevertToAutomationCharacter, RevertToAutomation);
_stateChangedProvider = new EventProvider<StateChanged.Type, nint, Lazy<string>>(pi, LabelStateChanged);
_gPoseChangedProvider = new EventProvider<bool>(pi, LabelGPoseChanged);
_stateChangedEvent.Subscribe(OnStateChanged, StateChanged.Priority.GlamourerIpc);
_gPose.Subscribe(OnGPoseChanged, GPoseService.Priority.GlamourerIpc);
}
public void Dispose()
{
_apiVersionProvider.Dispose();
_apiVersionsProvider.Dispose();
_getAllCustomizationProvider.Dispose();
_getAllCustomizationFromCharacterProvider.Dispose();
_applyAllProvider.Dispose();
_applyAllToCharacterProvider.Dispose();
_applyOnlyEquipmentProvider.Dispose();
_applyOnlyEquipmentToCharacterProvider.Dispose();
_applyOnlyCustomizationProvider.Dispose();
_applyOnlyCustomizationToCharacterProvider.Dispose();
_applyAllProviderLock.Dispose();
_applyAllToCharacterProviderLock.Dispose();
_applyOnlyEquipmentProviderLock.Dispose();
_applyOnlyEquipmentToCharacterProviderLock.Dispose();
_applyOnlyCustomizationProviderLock.Dispose();
_applyOnlyCustomizationToCharacterProviderLock.Dispose();
_revertProvider.Dispose();
_revertCharacterProvider.Dispose();
_revertProviderLock.Dispose();
_revertCharacterProviderLock.Dispose();
_unlockNameProvider.Dispose();
_unlockProvider.Dispose();
_revertToAutomationProvider.Dispose();
_revertToAutomationCharacterProvider.Dispose();
_stateChangedEvent.Unsubscribe(OnStateChanged);
_stateChangedProvider.Dispose();
_gPose.Unsubscribe(OnGPoseChanged);
_gPoseChangedProvider.Dispose();
}
private IEnumerable<ActorIdentifier> FindActors(string actorName)
{
if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString))
return Array.Empty<ActorIdentifier>();
_objects.Update();
return _objects.Where(i => i.Key is { IsValid: true, Type: IdentifierType.Player } && i.Key.PlayerName == byteString)
.Select(i => i.Key);
}
private IEnumerable<ActorIdentifier> FindActorsRevert(string actorName)
{
if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString))
yield break;
_objects.Update();
foreach (var id in _objects.Where(i => i.Key is { IsValid: true, Type: IdentifierType.Player } && i.Key.PlayerName == byteString)
.Select(i => i.Key))
yield return id;
foreach (var id in _stateManager.Keys.Where(s => s.Type is IdentifierType.Player && s.PlayerName == byteString))
yield return id;
}
private IEnumerable<ActorIdentifier> FindActors(Character? character)
{
var id = _actors.AwaitedService.FromObject(character, true, true, false);
if (!id.IsValid)
yield break;
yield return id;
}
}

View file

@ -1,85 +0,0 @@
using Dalamud.Plugin;
using Glamourer.Api.Api;
using Glamourer.Api.Helpers;
using OtterGui.Services;
using Glamourer.Api.Enums;
namespace Glamourer.Api;
public sealed class IpcProviders : IDisposable, IApiService
{
private readonly List<IDisposable> _providers;
private readonly EventProvider _disposedProvider;
private readonly EventProvider _initializedProvider;
public IpcProviders(IDalamudPluginInterface pi, IGlamourerApi api)
{
_disposedProvider = IpcSubscribers.Disposed.Provider(pi);
_initializedProvider = IpcSubscribers.Initialized.Provider(pi);
_providers =
[
new FuncProvider<(int Major, int Minor)>(pi, "Glamourer.ApiVersions", () => api.ApiVersion), // backward compatibility
new FuncProvider<int>(pi, "Glamourer.ApiVersion", () => api.ApiVersion.Major), // backward compatibility
IpcSubscribers.ApiVersion.Provider(pi, api),
IpcSubscribers.AutoReloadGearEnabled.Provider(pi, api),
IpcSubscribers.GetDesignList.Provider(pi, api.Designs),
IpcSubscribers.GetDesignListExtended.Provider(pi, api.Designs),
IpcSubscribers.GetExtendedDesignData.Provider(pi, api.Designs),
IpcSubscribers.ApplyDesign.Provider(pi, api.Designs),
IpcSubscribers.ApplyDesignName.Provider(pi, api.Designs),
IpcSubscribers.AddDesign.Provider(pi, api.Designs),
IpcSubscribers.DeleteDesign.Provider(pi, api.Designs),
IpcSubscribers.GetDesignBase64.Provider(pi, api.Designs),
IpcSubscribers.GetDesignJObject.Provider(pi, api.Designs),
IpcSubscribers.SetItem.Provider(pi, api.Items),
IpcSubscribers.SetItemName.Provider(pi, api.Items),
// backward compatibility
new FuncProvider<int, byte, ulong, byte, uint, ulong, int>(pi, IpcSubscribers.Legacy.SetItemV2.Label,
(a, b, c, d, e, f) => (int)api.Items.SetItem(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)),
new FuncProvider<string, byte, ulong, byte, uint, ulong, int>(pi, IpcSubscribers.Legacy.SetItemName.Label,
(a, b, c, d, e, f) => (int)api.Items.SetItemName(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)),
IpcSubscribers.SetBonusItem.Provider(pi, api.Items),
IpcSubscribers.SetBonusItemName.Provider(pi, api.Items),
IpcSubscribers.SetMetaState.Provider(pi, api.Items),
IpcSubscribers.SetMetaStateName.Provider(pi, api.Items),
IpcSubscribers.GetState.Provider(pi, api.State),
IpcSubscribers.GetStateName.Provider(pi, api.State),
IpcSubscribers.GetStateBase64.Provider(pi, api.State),
IpcSubscribers.GetStateBase64Name.Provider(pi, api.State),
IpcSubscribers.ApplyState.Provider(pi, api.State),
IpcSubscribers.ApplyStateName.Provider(pi, api.State),
IpcSubscribers.ReapplyState.Provider(pi, api.State),
IpcSubscribers.ReapplyStateName.Provider(pi, api.State),
IpcSubscribers.RevertState.Provider(pi, api.State),
IpcSubscribers.RevertStateName.Provider(pi, api.State),
IpcSubscribers.UnlockState.Provider(pi, api.State),
IpcSubscribers.CanUnlock.Provider(pi, api.State),
IpcSubscribers.UnlockStateName.Provider(pi, api.State),
IpcSubscribers.DeletePlayerState.Provider(pi, api.State),
IpcSubscribers.UnlockAll.Provider(pi, api.State),
IpcSubscribers.RevertToAutomation.Provider(pi, api.State),
IpcSubscribers.RevertToAutomationName.Provider(pi, api.State),
IpcSubscribers.AutoReloadGearChanged.Provider(pi, api.State),
IpcSubscribers.StateChanged.Provider(pi, api.State),
IpcSubscribers.StateChangedWithType.Provider(pi, api.State),
IpcSubscribers.StateFinalized.Provider(pi, api.State),
IpcSubscribers.GPoseChanged.Provider(pi, api.State),
];
_initializedProvider.Invoke();
}
public void Dispose()
{
foreach (var provider in _providers)
provider.Dispose();
_providers.Clear();
_initializedProvider.Dispose();
_disposedProvider.Invoke();
_disposedProvider.Dispose();
}
}

View file

@ -1,217 +0,0 @@
using Glamourer.Api.Api;
using Glamourer.Api.Enums;
using Glamourer.Designs;
using Glamourer.Services;
using Glamourer.State;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Api;
public class ItemsApi(ApiHelpers helpers, ItemManager itemManager, StateManager stateManager) : IGlamourerApiItems, IApiService
{
public GlamourerApiEc SetItem(int objectIndex, ApiEquipSlot slot, ulong itemId, IReadOnlyList<byte> stains, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", itemId, "Stains", stains, "Key", key, "Flags", flags);
if (!ResolveItem(slot, itemId, out var item))
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
if (helpers.FindState(objectIndex) is not { } state)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!state.ModelData.IsHuman)
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key);
stateManager.ChangeEquip(state, (EquipSlot)slot, item, new StainIds(stains), settings);
ApiHelpers.Lock(state, key, flags);
return GlamourerApiEc.Success;
}
public GlamourerApiEc SetItemName(string playerName, ApiEquipSlot slot, ulong itemId, IReadOnlyList<byte> stains, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", itemId, "Stains", stains, "Key", key, "Flags", flags);
if (!ResolveItem(slot, itemId, out var item))
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key);
var anyHuman = false;
var anyFound = false;
var anyUnlocked = false;
foreach (var state in helpers.FindStates(playerName))
{
anyFound = true;
if (!state.ModelData.IsHuman)
continue;
anyHuman = true;
if (!state.CanUnlock(key))
continue;
anyUnlocked = true;
stateManager.ChangeEquip(state, (EquipSlot)slot, item, new StainIds(stains), settings);
ApiHelpers.Lock(state, key, flags);
}
if (!anyFound)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!anyHuman)
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
if (!anyUnlocked)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc SetBonusItem(int objectIndex, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Index", objectIndex, "Slot", slot, "ID", bonusItemId, "Key", key, "Flags", flags);
if (!ResolveBonusItem(slot, bonusItemId, out var item))
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
if (helpers.FindState(objectIndex) is not { } state)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!state.ModelData.IsHuman)
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key);
stateManager.ChangeBonusItem(state, item.Type.ToBonus(), item, settings);
ApiHelpers.Lock(state, key, flags);
return GlamourerApiEc.Success;
}
public GlamourerApiEc SetBonusItemName(string playerName, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Name", playerName, "Slot", slot, "ID", bonusItemId, "Key", key, "Flags", flags);
if (!ResolveBonusItem(slot, bonusItemId, out var item))
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
var settings = new ApplySettings(Source: flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcManual : StateSource.IpcFixed, Key: key);
var anyHuman = false;
var anyFound = false;
var anyUnlocked = false;
foreach (var state in helpers.FindStates(playerName))
{
anyFound = true;
if (!state.ModelData.IsHuman)
continue;
anyHuman = true;
if (!state.CanUnlock(key))
continue;
anyUnlocked = true;
stateManager.ChangeBonusItem(state, item.Type.ToBonus(), item, settings);
ApiHelpers.Lock(state, key, flags);
}
if (!anyFound)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!anyHuman)
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
if (!anyUnlocked)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc SetMetaState(int objectIndex, MetaFlag types, bool newValue, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Index", objectIndex, "MetaTypes", types, "NewValue", newValue, "Key", key, "ApplyFlags", flags);
if (types == 0)
return ApiHelpers.Return(GlamourerApiEc.InvalidState, args);
if (helpers.FindState(objectIndex) is not { } state)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!state.ModelData.IsHuman)
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
// Grab MetaIndices from attached flags, and update the states.
var indices = types.ToIndices();
foreach (var index in indices)
{
stateManager.ChangeMetaState(state, index, newValue, ApplySettings.Manual);
ApiHelpers.Lock(state, key, flags);
}
return GlamourerApiEc.Success;
}
public GlamourerApiEc SetMetaStateName(string playerName, MetaFlag types, bool newValue, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Name", playerName, "MetaTypes", types, "NewValue", newValue, "Key", key, "ApplyFlags", flags);
if (types == 0)
return ApiHelpers.Return(GlamourerApiEc.ItemInvalid, args);
var anyHuman = false;
var anyFound = false;
var anyUnlocked = false;
foreach (var state in helpers.FindStates(playerName))
{
anyFound = true;
if (!state.ModelData.IsHuman)
continue;
anyHuman = true;
if (!state.CanUnlock(key))
continue;
anyUnlocked = true;
// update all MetaStates for this ActorState
foreach (var index in types.ToIndices())
{
stateManager.ChangeMetaState(state, index, newValue, ApplySettings.Manual);
ApiHelpers.Lock(state, key, flags);
}
}
if (!anyFound)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!anyHuman)
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
if (!anyUnlocked)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
private bool ResolveItem(ApiEquipSlot apiSlot, ulong itemId, out EquipItem item)
{
var id = (CustomItemId)itemId;
var slot = (EquipSlot)apiSlot;
if (id.Id == 0)
id = ItemManager.NothingId(slot);
item = itemManager.Resolve(slot, id);
return item.Valid;
}
private bool ResolveBonusItem(ApiBonusSlot apiSlot, ulong itemId, out EquipItem item)
{
var slot = apiSlot switch
{
ApiBonusSlot.Glasses => BonusItemFlag.Glasses,
_ => BonusItemFlag.Unknown,
};
return itemManager.IsBonusItemValid(slot, (BonusItemId)itemId, out item);
}
}

View file

@ -1,452 +0,0 @@
using Glamourer.Api.Api;
using Glamourer.Api.Enums;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Events;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using OtterGui.Services;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using StateChanged = Glamourer.Events.StateChanged;
namespace Glamourer.Api;
public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
{
private readonly ApiHelpers _helpers;
private readonly StateManager _stateManager;
private readonly DesignConverter _converter;
private readonly AutoDesignApplier _autoDesigns;
private readonly ActorObjectManager _objects;
private readonly AutoRedrawChanged _autoRedraw;
private readonly StateChanged _stateChanged;
private readonly StateFinalized _stateFinalized;
private readonly GPoseService _gPose;
public StateApi(ApiHelpers helpers,
StateManager stateManager,
DesignConverter converter,
AutoDesignApplier autoDesigns,
ActorObjectManager objects,
AutoRedrawChanged autoRedraw,
StateChanged stateChanged,
StateFinalized stateFinalized,
GPoseService gPose)
{
_helpers = helpers;
_stateManager = stateManager;
_converter = converter;
_autoDesigns = autoDesigns;
_objects = objects;
_autoRedraw = autoRedraw;
_stateChanged = stateChanged;
_stateFinalized = stateFinalized;
_gPose = gPose;
_autoRedraw.Subscribe(OnAutoRedrawChange, AutoRedrawChanged.Priority.StateApi);
_stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc);
_stateFinalized.Subscribe(OnStateFinalized, Events.StateFinalized.Priority.StateApi);
_gPose.Subscribe(OnGPoseChange, GPoseService.Priority.StateApi);
}
public void Dispose()
{
_autoRedraw.Unsubscribe(OnAutoRedrawChange);
_stateChanged.Unsubscribe(OnStateChanged);
_stateFinalized.Unsubscribe(OnStateFinalized);
_gPose.Unsubscribe(OnGPoseChange);
}
public (GlamourerApiEc, JObject?) GetState(int objectIndex, uint key)
=> Convert(_helpers.FindState(objectIndex), key);
public (GlamourerApiEc, JObject?) GetStateName(string playerName, uint key)
=> Convert(_helpers.FindStates(playerName).FirstOrDefault(), key);
public (GlamourerApiEc, string?) GetStateBase64(int objectIndex, uint key)
=> ConvertBase64(_helpers.FindState(objectIndex), key);
public (GlamourerApiEc, string?) GetStateBase64Name(string objectName, uint key)
=> ConvertBase64(_helpers.FindStates(objectName).FirstOrDefault(), key);
public GlamourerApiEc ApplyState(object applyState, int objectIndex, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags);
if (Convert(applyState, flags, out var version) is not { } design)
return ApiHelpers.Return(GlamourerApiEc.InvalidState, args);
if (_helpers.FindState(objectIndex) is not { } state)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
if (version < 3 && state.ModelData.ModelId != 0)
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
ApplyDesign(state, design, key, flags);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc ApplyStateName(object applyState, string playerName, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags);
if (Convert(applyState, flags, out var version) is not { } design)
return ApiHelpers.Return(GlamourerApiEc.InvalidState, args);
var states = _helpers.FindExistingStates(playerName);
var any = false;
var anyUnlocked = false;
var anyHuman = false;
foreach (var state in states)
{
any = true;
if (!state.CanUnlock(key))
continue;
anyUnlocked = true;
if (version < 3 && state.ModelData.ModelId != 0)
continue;
anyHuman = true;
ApplyDesign(state, design, key, flags);
}
if (any)
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!anyUnlocked)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
if (!anyHuman)
return ApiHelpers.Return(GlamourerApiEc.ActorNotHuman, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc ReapplyState(int objectIndex, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags);
if (_helpers.FindExistingState(objectIndex, out var state) is not GlamourerApiEc.Success)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (state is null)
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
Reapply(_objects.Objects[objectIndex], state, key, flags);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc ReapplyStateName(string playerName, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags);
var states = _helpers.FindExistingStates(playerName);
var any = false;
var anyReapplied = false;
foreach (var state in states)
{
any = true;
if (!state.CanUnlock(key))
continue;
anyReapplied = true;
anyReapplied |= Reapply(state, key, flags) is GlamourerApiEc.Success;
}
if (any)
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!anyReapplied)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc RevertState(int objectIndex, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags);
if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (state == null)
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
Revert(state, key, flags);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc RevertStateName(string playerName, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags);
var states = _helpers.FindExistingStates(playerName);
var any = false;
var anyUnlocked = false;
foreach (var state in states)
{
any = true;
if (!state.CanUnlock(key))
continue;
anyUnlocked = true;
Revert(state, key, flags);
}
if (any)
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!anyUnlocked)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc UnlockState(int objectIndex, uint key)
{
var args = ApiHelpers.Args("Index", objectIndex, "Key", key);
if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (state == null)
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!state.Unlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc CanUnlock(int objectIndex, uint key, out bool isLocked, out bool canUnlock)
{
var args = ApiHelpers.Args("Index", objectIndex, "Key", key);
isLocked = false;
canUnlock = true;
if (_helpers.FindExistingState(objectIndex, out var state) is not GlamourerApiEc.Success)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (state is null)
return ApiHelpers.Return(GlamourerApiEc.Success, args);
isLocked = state.IsLocked;
canUnlock = state.CanUnlock(key);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc UnlockStateName(string playerName, uint key)
{
var args = ApiHelpers.Args("Name", playerName, "Key", key);
var states = _helpers.FindExistingStates(playerName);
var any = false;
var anyUnlocked = false;
foreach (var state in states)
{
any = true;
anyUnlocked |= state.Unlock(key);
}
if (any)
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!anyUnlocked)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc DeletePlayerState(string playerName, ushort worldId, uint key)
{
var args = ApiHelpers.Args("Name", playerName, "World", worldId, "Key", key);
var states = _helpers.FindExistingStates(playerName).ToList();
if (states.Count is 0)
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
var anyLocked = false;
foreach (var state in states)
{
if (state.CanUnlock(key))
_stateManager.DeleteState(state.Identifier);
else
anyLocked = true;
}
return ApiHelpers.Return(anyLocked
? GlamourerApiEc.InvalidKey
: GlamourerApiEc.Success, args);
}
public int UnlockAll(uint key)
=> _stateManager.Values.Count(state => state.Unlock(key));
public GlamourerApiEc RevertToAutomation(int objectIndex, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Index", objectIndex, "Key", key, "Flags", flags);
if (_helpers.FindExistingState(objectIndex, out var state) != GlamourerApiEc.Success)
return ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (state == null)
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!state.CanUnlock(key))
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
RevertToAutomation(_objects.Objects[objectIndex], state, key, flags);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc RevertToAutomationName(string playerName, uint key, ApplyFlag flags)
{
var args = ApiHelpers.Args("Name", playerName, "Key", key, "Flags", flags);
var states = _helpers.FindExistingStates(playerName);
var any = false;
var anyUnlocked = false;
var anyReverted = false;
foreach (var state in states)
{
any = true;
if (!state.CanUnlock(key))
continue;
anyUnlocked = true;
anyReverted |= RevertToAutomation(state, key, flags) is GlamourerApiEc.Success;
}
if (any)
ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
if (!anyReverted)
ApiHelpers.Return(GlamourerApiEc.ActorNotFound, args);
if (!anyUnlocked)
return ApiHelpers.Return(GlamourerApiEc.InvalidKey, args);
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public event Action<bool>? AutoReloadGearChanged;
public event Action<nint>? StateChanged;
public event Action<IntPtr, StateChangeType>? StateChangedWithType;
public event Action<IntPtr, StateFinalizationType>? StateFinalized;
public event Action<bool>? GPoseChanged;
private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags)
{
var once = (flags & ApplyFlag.Once) != 0;
var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true,
ResetMaterials: !once && key != 0, IsFinal: true);
_stateManager.ApplyDesign(state, design, settings);
ApiHelpers.Lock(state, key, flags);
}
private GlamourerApiEc Reapply(ActorState state, uint key, ApplyFlag flags)
{
if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid)
return GlamourerApiEc.ActorNotFound;
foreach (var actor in actors.Objects)
Reapply(actor, state, key, flags);
return GlamourerApiEc.Success;
}
private void Reapply(Actor actor, ActorState state, uint key, ApplyFlag flags)
{
var source = flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcFixed : StateSource.IpcManual;
_stateManager.ReapplyState(actor, state, false, source, true);
ApiHelpers.Lock(state, key, flags);
}
private void Revert(ActorState state, uint key, ApplyFlag flags)
{
var source = flags.HasFlag(ApplyFlag.Once) ? StateSource.IpcFixed : StateSource.IpcManual;
switch (flags & (ApplyFlag.Equipment | ApplyFlag.Customization))
{
case ApplyFlag.Equipment: _stateManager.ResetEquip(state, source, key); break;
case ApplyFlag.Customization: _stateManager.ResetCustomize(state, source, key); break;
case ApplyFlag.Equipment | ApplyFlag.Customization: _stateManager.ResetState(state, source, key, true); break;
}
ApiHelpers.Lock(state, key, flags);
}
private GlamourerApiEc RevertToAutomation(ActorState state, uint key, ApplyFlag flags)
{
if (!_objects.TryGetValue(state.Identifier, out var actors) || !actors.Valid)
return GlamourerApiEc.ActorNotFound;
foreach (var actor in actors.Objects)
RevertToAutomation(actor, state, key, flags);
return GlamourerApiEc.Success;
}
private void RevertToAutomation(Actor actor, ActorState state, uint key, ApplyFlag flags)
{
var source = (flags & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed;
_autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, false, out var forcedRedraw);
_stateManager.ReapplyAutomationState(actor, state, forcedRedraw, true, source);
ApiHelpers.Lock(state, key, flags);
}
private (GlamourerApiEc, JObject?) Convert(ActorState? state, uint key)
{
if (state == null)
return (GlamourerApiEc.ActorNotFound, null);
if (!state.CanUnlock(key))
return (GlamourerApiEc.InvalidKey, null);
return (GlamourerApiEc.Success, _converter.ShareJObject(state, ApplicationRules.All));
}
private (GlamourerApiEc, string?) ConvertBase64(ActorState? state, uint key)
{
var (ec, jObj) = Convert(state, key);
return (ec, jObj != null ? DesignConverter.ToBase64(jObj) : null);
}
private DesignBase? Convert(object? state, ApplyFlag flags, out byte version)
{
version = DesignConverter.Version;
return state switch
{
string s => _converter.FromBase64(s, (flags & ApplyFlag.Customization) != 0, (flags & ApplyFlag.Equipment) != 0, out version),
JObject j => _converter.FromJObject(j, (flags & ApplyFlag.Customization) != 0, (flags & ApplyFlag.Equipment) != 0),
_ => null,
};
}
private void OnAutoRedrawChange(bool autoReload)
=> AutoReloadGearChanged?.Invoke(autoReload);
private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5)
{
Glamourer.Log.Excessive($"[OnStateChanged] State Changed with Type {type} [Affecting {actors.ToLazyString("nothing")}.]");
if (StateChanged != null)
foreach (var actor in actors.Objects)
StateChanged.Invoke(actor.Address);
if (StateChangedWithType != null)
foreach (var actor in actors.Objects)
StateChangedWithType.Invoke(actor.Address, type);
}
private void OnStateFinalized(StateFinalizationType type, ActorData actors)
{
Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]");
if (StateFinalized != null)
foreach (var actor in actors.Objects)
StateFinalized.Invoke(actor.Address, type);
}
private void OnGPoseChange(bool gPose)
=> GPoseChanged?.Invoke(gPose);
}

View file

@ -1,74 +0,0 @@
using Glamourer.Api.Enums;
using Glamourer.Designs;
using Glamourer.GameData;
using Penumbra.GameData.Enums;
namespace Glamourer.Automation;
[Flags]
public enum ApplicationType : byte
{
Armor = 0x01,
Customizations = 0x02,
Weapons = 0x04,
GearCustomization = 0x08,
Accessories = 0x10,
All = Armor | Accessories | Customizations | Weapons | GearCustomization,
}
public static class ApplicationTypeExtensions
{
public static readonly IReadOnlyList<(ApplicationType, string)> Types =
[
(ApplicationType.Customizations,
"Apply all customization changes that are enabled in this design and that are valid in a fixed design and for the given race and gender."),
(ApplicationType.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."),
(ApplicationType.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."),
(ApplicationType.GearCustomization, "Apply all dye and crest changes that are enabled in this design."),
(ApplicationType.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."),
];
public static ApplicationCollection Collection(this ApplicationType type)
{
var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0)
| (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0)
| (type.HasFlag(ApplicationType.Accessories) ? AccessoryFlags : 0)
| (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0);
var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0;
var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0;
var crestFlags = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0;
var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.EarState : 0)
| (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0)
| (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0);
var bonusFlags = type.HasFlag(ApplicationType.Armor) ? BonusExtensions.All : 0;
return new ApplicationCollection(equipFlags, bonusFlags, customizeFlags, crestFlags, parameterFlags, metaFlags);
}
public static ApplicationCollection ApplyWhat(this ApplicationType type, IDesignStandIn designStandIn)
{
if(designStandIn is not DesignBase design)
return type.Collection();
var ret = type.Collection().Restrict(design.Application);
ret.CustomizeRaw = ret.CustomizeRaw.FixApplication(design.CustomizeSet);
return ret;
}
public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand;
public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet;
public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger;
public const EquipFlag StainFlags = EquipFlag.MainhandStain
| EquipFlag.OffhandStain
| EquipFlag.HeadStain
| EquipFlag.BodyStain
| EquipFlag.HandsStain
| EquipFlag.LegsStain
| EquipFlag.FeetStain
| EquipFlag.EarsStain
| EquipFlag.NeckStain
| EquipFlag.WristStain
| EquipFlag.RFingerStain
| EquipFlag.LFingerStain;
}

View file

@ -1,66 +1,102 @@
using Glamourer.Designs;
using Glamourer.Designs.Special;
using Glamourer.GameData;
using System;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Interop.Structs;
using Glamourer.State;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Glamourer.Automation;
public class AutoDesign
{
public IDesignStandIn Design = new RevertDesign();
public JobGroup Jobs;
public ApplicationType Type;
public short GearsetIndex = -1;
public const string RevertName = "Revert";
[Flags]
public enum Type : byte
{
Armor = 0x01,
Customizations = 0x02,
Weapons = 0x04,
Stains = 0x08,
Accessories = 0x10,
All = Armor | Accessories | Customizations | Weapons | Stains,
}
public Design? Design;
public JobGroup Jobs;
public Type ApplicationType;
public string Name(bool incognito)
=> Revert ? RevertName : incognito ? Design!.Incognito : Design!.Name.Text;
public ref DesignData GetDesignData(ActorState state)
=> ref Design == null ? ref state.BaseData : ref Design.DesignData;
public bool Revert
=> Design == null;
public AutoDesign Clone()
=> new()
{
Design = Design,
Type = Type,
Jobs = Jobs,
GearsetIndex = GearsetIndex,
Design = Design,
ApplicationType = ApplicationType,
Jobs = Jobs,
};
public unsafe bool IsActive(Actor actor)
{
if (!actor.IsCharacter)
return false;
var ret = true;
if (GearsetIndex < 0)
ret &= Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob);
else
ret &= AutoDesignApplier.CheckGearset(GearsetIndex);
return ret;
}
=> actor.IsCharacter && Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob);
public JObject Serialize()
{
var ret = new JObject
=> new()
{
["Design"] = Design.SerializeName(),
["Type"] = (uint)Type,
["Conditions"] = CreateConditionObject(),
["Design"] = Design?.Identifier.ToString(),
["ApplicationType"] = (uint)ApplicationType,
["Conditions"] = CreateConditionObject(),
};
Design.AddData(ret);
return ret;
}
private JObject CreateConditionObject()
{
var ret = new JObject
{
["Gearset"] = GearsetIndex,
["JobGroup"] = Jobs.Id.Id,
};
var ret = new JObject();
if (Jobs.Id != 0)
ret["JobGroup"] = Jobs.Id;
return ret;
}
public ApplicationCollection ApplyWhat()
=> Type.ApplyWhat(Design);
public (EquipFlag Equip, CustomizeFlag Customize, bool ApplyHat, bool ApplyVisor, bool ApplyWeapon, bool ApplyWet) ApplyWhat()
{
var equipFlags = (ApplicationType.HasFlag(Type.Weapons) ? WeaponFlags : 0)
| (ApplicationType.HasFlag(Type.Armor) ? ArmorFlags : 0)
| (ApplicationType.HasFlag(Type.Accessories) ? AccessoryFlags : 0)
| (ApplicationType.HasFlag(Type.Stains) ? StainFlags : 0);
var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0;
if (Revert)
return (equipFlags, customizeFlags, ApplicationType.HasFlag(Type.Armor), ApplicationType.HasFlag(Type.Armor),
ApplicationType.HasFlag(Type.Weapons), ApplicationType.HasFlag(Type.Customizations));
return (equipFlags & Design!.ApplyEquip, customizeFlags & Design.ApplyCustomize,
ApplicationType.HasFlag(Type.Armor) && Design.DoApplyHatVisible(),
ApplicationType.HasFlag(Type.Armor) && Design.DoApplyVisorToggle(),
ApplicationType.HasFlag(Type.Weapons) && Design.DoApplyWeaponVisible(),
ApplicationType.HasFlag(Type.Customizations) && Design.DoApplyWetness());
}
public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand;
public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet;
public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger;
public const EquipFlag StainFlags = EquipFlag.MainhandStain
| EquipFlag.OffhandStain
| EquipFlag.HeadStain
| EquipFlag.BodyStain
| EquipFlag.HandsStain
| EquipFlag.LegsStain
| EquipFlag.FeetStain
| EquipFlag.EarsStain
| EquipFlag.NeckStain
| EquipFlag.WristStain
| EquipFlag.RFingerStain
| EquipFlag.LFingerStain;
}

View file

@ -1,118 +1,114 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Designs.Links;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Material;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Unlocks;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Glamourer.Automation;
public sealed class AutoDesignApplier : IDisposable
public class AutoDesignApplier : IDisposable
{
private readonly Configuration _config;
private readonly AutoDesignManager _manager;
private readonly StateManager _state;
private readonly JobService _jobs;
private readonly EquippedGearset _equippedGearset;
private readonly ActorManager _actors;
private readonly AutomationChanged _event;
private readonly ActorObjectManager _objects;
private readonly WeaponLoading _weapons;
private readonly HumanModelList _humans;
private readonly DesignMerger _designMerger;
private readonly IClientState _clientState;
private readonly Configuration _config;
private readonly AutoDesignManager _manager;
private readonly StateManager _state;
private readonly JobService _jobs;
private readonly ActorService _actors;
private readonly CustomizationService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly ItemUnlockManager _itemUnlocks;
private readonly AutomationChanged _event;
private readonly ObjectManager _objects;
private readonly WeaponLoading _weapons;
private readonly HumanModelList _humans;
private readonly IClientState _clientState;
private readonly JobChangeState _jobChangeState;
private ActorState? _jobChangeState;
private readonly Dictionary<FullEquipType, (EquipItem, StateChanged.Source)> _jobChangeMainhand = new();
private readonly Dictionary<FullEquipType, (EquipItem, StateChanged.Source)> _jobChangeOffhand = new();
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, ActorManager actors,
AutomationChanged @event, ActorObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState,
EquippedGearset equippedGearset, DesignMerger designMerger, JobChangeState jobChangeState)
private void ResetJobChange()
{
_config = config;
_manager = manager;
_state = state;
_jobs = jobs;
_actors = actors;
_event = @event;
_objects = objects;
_weapons = weapons;
_humans = humans;
_clientState = clientState;
_equippedGearset = equippedGearset;
_designMerger = designMerger;
_jobChangeState = jobChangeState;
_jobs.JobChanged += OnJobChange;
_event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier);
_weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier);
_equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier);
_jobChangeState = null;
_jobChangeMainhand.Clear();
_jobChangeOffhand.Clear();
}
public void OnEnableAutoDesignsChanged(bool value)
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs,
CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks,
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState)
{
if (value)
return;
foreach (var state in _state.Values)
state.Sources.RemoveFixedDesignSources();
_config = config;
_manager = manager;
_state = state;
_jobs = jobs;
_customizations = customizations;
_actors = actors;
_itemUnlocks = itemUnlocks;
_customizeUnlocks = customizeUnlocks;
_event = @event;
_objects = objects;
_weapons = weapons;
_humans = humans;
_clientState = clientState;
_jobs.JobChanged += OnJobChange;
_event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier);
_weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier);
}
public void Dispose()
{
_weapons.Unsubscribe(OnWeaponLoading);
_event.Unsubscribe(OnAutomationChange);
_equippedGearset.Unsubscribe(OnEquippedGearset);
_jobs.JobChanged -= OnJobChange;
}
private void OnWeaponLoading(Actor actor, EquipSlot slot, ref CharacterWeapon weapon)
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
{
if (!_jobChangeState.HasState || !_config.EnableAutoDesigns)
if (_jobChangeState == null || !_config.EnableAutoDesigns)
return;
var id = actor.GetIdentifier(_actors);
var id = actor.GetIdentifier(_actors.AwaitedService);
if (id == _jobChangeState.Identifier)
{
var state = _jobChangeState.State!;
var current = state.BaseData.Item(slot);
switch (slot)
var current = _jobChangeState.BaseData.Item(slot);
if (slot is EquipSlot.MainHand)
{
case EquipSlot.MainHand:
if (_jobChangeMainhand.TryGetValue(current.Type, out var data))
{
if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
{
Glamourer.Log.Verbose(
$"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}.");
_state.ChangeItem(state, EquipSlot.MainHand, data.Item1, new ApplySettings(Source: data.Item2));
weapon = state.ModelData.Weapon(EquipSlot.MainHand);
}
break;
Glamourer.Log.Verbose($"Changing Mainhand from {_jobChangeState.ModelData.Weapon(EquipSlot.MainHand)} | {_jobChangeState.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}.");
_state.ChangeItem(_jobChangeState, EquipSlot.MainHand, data.Item1, data.Item2);
weapon.Value = _jobChangeState.ModelData.Weapon(EquipSlot.MainHand);
}
case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand():
}
else if (slot is EquipSlot.OffHand && current.Type == _jobChangeState.BaseData.MainhandType.Offhand())
{
if (_jobChangeOffhand.TryGetValue(current.Type, out var data))
{
if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
{
Glamourer.Log.Verbose(
$"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}.");
_state.ChangeItem(state, EquipSlot.OffHand, data.Item1, new ApplySettings(Source: data.Item2));
weapon = state.ModelData.Weapon(EquipSlot.OffHand);
}
_jobChangeState.Reset();
break;
Glamourer.Log.Verbose($"Changing Offhand from {_jobChangeState.ModelData.Weapon(EquipSlot.OffHand)} | {_jobChangeState.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}.");
_state.ChangeItem(_jobChangeState, EquipSlot.OffHand, data.Item1, data.Item2);
weapon.Value = _jobChangeState.ModelData.Weapon(EquipSlot.OffHand);
}
ResetJobChange();
}
}
else
{
_jobChangeState.Reset();
ResetJobChange();
}
}
@ -121,68 +117,6 @@ public sealed class AutoDesignApplier : IDisposable
if (!_config.EnableAutoDesigns || set == null)
return;
switch (type)
{
case AutomationChanged.Type.ToggleSet when !set.Enabled:
case AutomationChanged.Type.DeletedDesign when set.Enabled:
// The automation set was disabled or deleted, no other for those identifiers can be enabled, remove existing Fixed Locks.
RemoveOld(set.Identifiers);
break;
case AutomationChanged.Type.ChangeIdentifier when set.Enabled:
// Remove fixed state from the old identifiers assigned and the old enabled set, if any.
var (oldIds, _, _) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!;
RemoveOld(oldIds);
ApplyNew(set); // Does not need to disable oldSet because same identifiers.
break;
case AutomationChanged.Type.ToggleSet: // Does not need to disable old states because same identifiers.
case AutomationChanged.Type.ChangedBase:
case AutomationChanged.Type.AddedDesign:
case AutomationChanged.Type.MovedDesign:
case AutomationChanged.Type.ChangedDesign:
case AutomationChanged.Type.ChangedConditions:
case AutomationChanged.Type.ChangedType:
case AutomationChanged.Type.ChangedData:
ApplyNew(set);
break;
}
return;
void ApplyNew(AutoDesignSet? newSet)
{
if (newSet is not { Enabled: true })
return;
foreach (var id in newSet.Identifiers)
{
if (_objects.TryGetValue(id, out var data))
{
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
foreach (var actor in data.Objects)
_state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
}
}
else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data))
{
foreach (var actor in data.Objects)
{
var specificId = actor.GetIdentifier(_actors);
if (_state.GetOrCreate(specificId, actor, out var state))
{
Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
_state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
}
}
}
else if (_state.TryGetValue(id, out var state))
{
state.Sources.RemoveFixedDesignSources();
}
}
}
void RemoveOld(ActorIdentifier[]? identifiers)
{
if (identifiers == null)
@ -192,26 +126,86 @@ public sealed class AutoDesignApplier : IDisposable
{
if (id.Type is IdentifierType.Player && id.HomeWorld == WorldId.AnyWorld)
foreach (var state in _state.Where(kvp => kvp.Key.PlayerName == id.PlayerName).Select(kvp => kvp.Value))
state.Sources.RemoveFixedDesignSources();
state.RemoveFixedDesignSources();
else if (_state.TryGetValue(id, out var state))
state.Sources.RemoveFixedDesignSources();
state.RemoveFixedDesignSources();
}
}
void ApplyNew(AutoDesignSet? newSet)
{
if (newSet is not { Enabled: true })
return;
_objects.Update();
foreach (var id in newSet.Identifiers)
{
if (_objects.TryGetValue(id, out var data))
{
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
Reduce(data.Objects[0], state, newSet, false, false);
foreach (var actor in data.Objects)
_state.ReapplyState(actor);
}
}
else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data))
{
foreach (var actor in data.Objects)
{
var specificId = actor.GetIdentifier(_actors.AwaitedService);
if (_state.GetOrCreate(specificId, actor, out var state))
{
Reduce(actor, state, newSet, false, false);
_state.ReapplyState(actor);
}
}
}
else if (_state.TryGetValue(id, out var state))
{
state.RemoveFixedDesignSources();
}
}
}
switch (type)
{
case AutomationChanged.Type.ToggleSet when !set.Enabled:
case AutomationChanged.Type.DeletedDesign when set.Enabled:
// The automation set was disabled or deleted, no other for those identifiers can be enabled, remove existing Fixed Locks.
RemoveOld(set.Identifiers);
break;
case AutomationChanged.Type.ChangeIdentifier when set.Enabled:
// Remove fixed state from the old identifiers assigned and the old enabled set, if any.
var (oldIds, _, oldSet) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!;
RemoveOld(oldIds);
ApplyNew(set); // Does not need to disable oldSet because same identifiers.
break;
case AutomationChanged.Type.ToggleSet: // Does not need to disable old states because same identifiers.
case AutomationChanged.Type.ChangedBase:
case AutomationChanged.Type.AddedDesign:
case AutomationChanged.Type.MovedDesign:
case AutomationChanged.Type.ChangedDesign:
case AutomationChanged.Type.ChangedConditions:
case AutomationChanged.Type.ChangedType:
ApplyNew(set);
break;
}
}
private void OnJobChange(Actor actor, Job oldJob, Job newJob)
{
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id))
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id))
return;
if (!GetPlayerSet(id, out var set))
{
if (_state.TryGetValue(id, out var s))
s.LastJob = newJob.Id;
s.LastJob = (byte)newJob.Id;
return;
}
if (!_state.GetOrCreate(actor, out var state))
if (!_state.TryGetValue(id, out var state))
return;
if (oldJob.Id == newJob.Id && state.LastJob == newJob.Id)
@ -219,21 +213,19 @@ public sealed class AutoDesignApplier : IDisposable
var respectManual = state.LastJob == newJob.Id;
state.LastJob = actor.Job;
Reduce(actor, state, set, respectManual, true, true, out var forcedRedraw);
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
Reduce(actor, state, set, respectManual, true);
_state.ReapplyState(actor);
}
public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state, bool reset, bool forcedNew, out bool forcedRedraw)
public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state)
{
forcedRedraw = false;
if (!_config.EnableAutoDesigns)
return;
if (reset)
_state.ResetState(state, StateSource.Game);
if (!GetPlayerSet(identifier, out var set))
return;
if (GetPlayerSet(identifier, out var set))
Reduce(actor, state, set, false, false, forcedNew, out forcedRedraw);
Reduce(actor, state, set, false, false);
}
public bool Reduce(Actor actor, ActorIdentifier identifier, [NotNullWhen(true)] out ActorState? state)
@ -241,6 +233,9 @@ public sealed class AutoDesignApplier : IDisposable
AutoDesignSet set;
if (!_state.TryGetValue(identifier, out state))
{
if (!_config.EnableAutoDesigns)
return false;
if (!GetPlayerSet(identifier, out set!))
return false;
@ -250,91 +245,70 @@ public sealed class AutoDesignApplier : IDisposable
else if (!GetPlayerSet(identifier, out set!))
{
if (state.UpdateTerritory(_clientState.TerritoryType) && _config.RevertManualChangesOnZoneChange)
_state.ResetState(state, StateSource.Game);
_state.ResetState(state, StateChanged.Source.Game);
return true;
}
var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange;
if (!respectManual)
_state.ResetState(state, StateSource.Game);
Reduce(actor, state, set, respectManual, false, false, out _);
_state.ResetState(state, StateChanged.Source.Game);
Reduce(actor, state, set, respectManual, false);
return true;
}
private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange, bool newApplication,
out bool forcedRedraw)
private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange)
{
if (set.BaseState is AutoDesignSet.Base.Game)
{
_state.ResetStateFixed(state, respectManual);
}
EquipFlag totalEquipFlags = 0;
CustomizeFlag totalCustomizeFlags = 0;
byte totalMetaFlags = 0;
if (set.BaseState == AutoDesignSet.Base.Game)
_state.ResetStateFixed(state);
else if (!respectManual)
{
state.Sources.RemoveFixedDesignSources();
for (var i = 0; i < state.Materials.Values.Count; ++i)
{
var (key, value) = state.Materials.Values[i];
if (value.Source is StateSource.Fixed)
state.Materials.UpdateValue(key, new MaterialValueState(value.Game, value.Model, value.DrawData, StateSource.Manual),
out _);
}
}
state.RemoveFixedDesignSources();
forcedRedraw = false;
if (!_humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId))
if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId))
return;
if (actor.IsTransformed)
return;
var mergedDesign = _designMerger.Merge(
set.Designs.Where(d => d.IsActive(actor))
.SelectMany(d => d.Design.AllLinks(newApplication).Select(l => (l.Design, l.Flags & d.Type, d.Jobs.Flags))),
state.ModelData.Customize, state.BaseData, true, _config.AlwaysApplyAssociatedMods);
if (_objects.IsInGPose && actor.IsGPoseOrCutscene)
foreach (var design in set.Designs)
{
mergedDesign.ResetTemporarySettings = false;
mergedDesign.AssociatedMods.Clear();
}
else if (set.ResetTemporarySettings)
{
mergedDesign.ResetTemporarySettings = true;
if (!design.IsActive(actor))
continue;
if (design.ApplicationType is 0)
continue;
ref var data = ref design.GetDesignData(state);
var source = design.Revert ? StateChanged.Source.Game : StateChanged.Source.Fixed;
if (!data.IsHuman)
continue;
var (equipFlags, customizeFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat();
ReduceMeta(state, data, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags, respectManual, source);
ReduceCustomize(state, data, customizeFlags, ref totalCustomizeFlags, respectManual, source);
ReduceEquip(state, data, equipFlags, ref totalEquipFlags, respectManual, source, fromJobChange);
}
_state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false, false, false));
forcedRedraw = mergedDesign.ForcedRedraw;
if (totalCustomizeFlags != 0)
state.ModelData.ModelId = 0;
}
/// <summary> Get world-specific first and all-world afterward. </summary>
/// <summary> Get world-specific first and all-world afterwards. </summary>
private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set)
{
if (!_config.EnableAutoDesigns)
{
set = null;
return false;
}
switch (identifier.Type)
{
case IdentifierType.Player:
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld);
identifier = _actors.AwaitedService.CreatePlayer(identifier.PlayerName, ushort.MaxValue);
return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Retainer:
case IdentifierType.Npc:
return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Owned:
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreateOwned(identifier.PlayerName, WorldId.AnyWorld, identifier.Kind, identifier.DataId);
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId);
identifier = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId);
return _manager.EnabledSets.TryGetValue(identifier, out set);
default:
set = null;
@ -342,37 +316,182 @@ public sealed class AutoDesignApplier : IDisposable
}
}
internal static int NewGearsetId = -1;
private void OnEquippedGearset(string name, int id, int prior, byte _, byte job)
private void ReduceEquip(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags, bool respectManual,
StateChanged.Source source, bool fromJobChange)
{
if (!_config.EnableAutoDesigns)
equipFlags &= ~totalEquipFlags;
if (equipFlags == 0)
return;
var (player, data) = _objects.PlayerData;
if (!player.IsValid)
return;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var flag = slot.ToFlag();
if (equipFlags.HasFlag(flag))
{
var item = design.Item(slot);
if (!_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _))
{
if (!respectManual || state[slot, false] is not StateChanged.Source.Manual)
_state.ChangeItem(state, slot, item, source);
totalEquipFlags |= flag;
}
}
if (!GetPlayerSet(player, out var set) || !_state.TryGetValue(player, out var state))
return;
var stainFlag = slot.ToStainFlag();
if (equipFlags.HasFlag(stainFlag))
{
if (!respectManual || state[slot, true] is not StateChanged.Source.Manual)
_state.ChangeStain(state, slot, design.Stain(slot), source);
totalEquipFlags |= stainFlag;
}
}
var respectManual = prior == id;
NewGearsetId = id;
Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, prior == id, out var forcedRedraw);
NewGearsetId = -1;
foreach (var actor in data.Objects)
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
if (equipFlags.HasFlag(EquipFlag.Mainhand))
{
var item = design.Item(EquipSlot.MainHand);
var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _);
var checkState = !respectManual || state[EquipSlot.MainHand, false] is not StateChanged.Source.Manual;
if (checkUnlock && checkState)
{
if (fromJobChange)
{
_jobChangeMainhand.TryAdd(item.Type, (item, source));
_jobChangeState = state;
}
else if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type)
{
_state.ChangeItem(state, EquipSlot.MainHand, item, source);
totalEquipFlags |= EquipFlag.Mainhand;
}
}
}
if (equipFlags.HasFlag(EquipFlag.Offhand))
{
var item = design.Item(EquipSlot.OffHand);
var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _);
var checkState = !respectManual || state[EquipSlot.OffHand, false] is not StateChanged.Source.Manual;
if (checkUnlock && checkState)
{
if (fromJobChange)
{
_jobChangeOffhand.TryAdd(item.Type, (item, source));
_jobChangeState = state;
}
else if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type)
{
_state.ChangeItem(state, EquipSlot.OffHand, item, source);
totalEquipFlags |= EquipFlag.Offhand;
}
}
}
if (equipFlags.HasFlag(EquipFlag.MainhandStain))
{
if (!respectManual || state[EquipSlot.MainHand, true] is not StateChanged.Source.Manual)
_state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), source);
totalEquipFlags |= EquipFlag.MainhandStain;
}
if (equipFlags.HasFlag(EquipFlag.OffhandStain))
{
if (!respectManual || state[EquipSlot.OffHand, true] is not StateChanged.Source.Manual)
_state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), source);
totalEquipFlags |= EquipFlag.OffhandStain;
}
}
public static unsafe bool CheckGearset(short check)
private void ReduceCustomize(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags,
bool respectManual, StateChanged.Source source)
{
if (NewGearsetId != -1)
return check == NewGearsetId;
customizeFlags &= ~totalCustomizeFlags;
if (customizeFlags == 0)
return;
var module = RaptureGearsetModule.Instance();
if (module == null)
return false;
var customize = state.ModelData.Customize;
CustomizeFlag fixFlags = 0;
return check == module->CurrentGearsetIndex;
// Skip anything not human.
if (!state.ModelData.IsHuman || !design.IsHuman)
return;
if (customizeFlags.HasFlag(CustomizeFlag.Clan))
{
if (!respectManual || state[CustomizeIndex.Clan] is not StateChanged.Source.Manual)
fixFlags |= _customizations.ChangeClan(ref customize, design.Customize.Clan);
customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race);
totalCustomizeFlags |= CustomizeFlag.Clan | CustomizeFlag.Race;
}
if (customizeFlags.HasFlag(CustomizeFlag.Gender))
{
if (!respectManual || state[CustomizeIndex.Gender] is not StateChanged.Source.Manual)
fixFlags |= _customizations.ChangeGender(ref customize, design.Customize.Gender);
customizeFlags &= ~CustomizeFlag.Gender;
totalCustomizeFlags |= CustomizeFlag.Gender;
}
if (fixFlags != 0)
_state.ChangeCustomize(state, customize, fixFlags, source);
if (customizeFlags.HasFlag(CustomizeFlag.Face))
{
if (!respectManual || state[CustomizeIndex.Face] is not StateChanged.Source.Manual)
_state.ChangeCustomize(state, CustomizeIndex.Face, design.Customize.Face, source);
customizeFlags &= ~CustomizeFlag.Face;
totalCustomizeFlags |= CustomizeFlag.Face;
}
var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender);
var face = state.ModelData.Customize.Face;
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
var flag = index.ToFlag();
if (!customizeFlags.HasFlag(flag))
continue;
var value = design.Customize[index];
if (CustomizationService.IsCustomizationValid(set, face, index, value, out var data))
{
if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _))
continue;
if (!respectManual || state[index] is not StateChanged.Source.Manual)
_state.ChangeCustomize(state, index, value, source);
totalCustomizeFlags |= flag;
}
}
}
private void ReduceMeta(ActorState state, in DesignData design, bool applyHat, bool applyVisor, bool applyWeapon, bool applyWet,
ref byte totalMetaFlags, bool respectManual, StateChanged.Source source)
{
if (applyHat && (totalMetaFlags & 0x01) == 0)
{
if (!respectManual || state[ActorState.MetaIndex.HatState] is not StateChanged.Source.Manual)
_state.ChangeHatState(state, design.IsHatVisible(), source);
totalMetaFlags |= 0x01;
}
if (applyVisor && (totalMetaFlags & 0x02) == 0)
{
if (!respectManual || state[ActorState.MetaIndex.VisorState] is not StateChanged.Source.Manual)
_state.ChangeVisorState(state, design.IsVisorToggled(), source);
totalMetaFlags |= 0x02;
}
if (applyWeapon && (totalMetaFlags & 0x04) == 0)
{
if (!respectManual || state[ActorState.MetaIndex.WeaponState] is not StateChanged.Source.Manual)
_state.ChangeWeaponState(state, design.IsWeaponVisible(), source);
totalMetaFlags |= 0x04;
}
if (applyWet && (totalMetaFlags & 0x08) == 0)
{
if (!respectManual || state[ActorState.MetaIndex.Wetness] is not StateChanged.Source.Manual)
_state.ChangeWetness(state, design.IsWet(), source);
totalMetaFlags |= 0x08;
}
}
}

View file

@ -1,20 +1,22 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.ImGuiNotification;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Designs.Special;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Filesystem;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Automation;
@ -24,32 +26,27 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
private readonly SaveService _saveService;
private readonly JobService _jobs;
private readonly DesignManager _designs;
private readonly ActorManager _actors;
private readonly AutomationChanged _event;
private readonly DesignChanged _designEvent;
private readonly RandomDesignGenerator _randomDesigns;
private readonly QuickSelectedDesign _quickSelectedDesign;
private readonly JobService _jobs;
private readonly DesignManager _designs;
private readonly ActorService _actors;
private readonly AutomationChanged _event;
private readonly DesignChanged _designEvent;
private readonly List<AutoDesignSet> _data = [];
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = [];
private readonly List<AutoDesignSet> _data = new();
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = new();
public IReadOnlyDictionary<ActorIdentifier, AutoDesignSet> EnabledSets
=> _enabled;
public AutoDesignManager(JobService jobs, ActorManager actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent, RandomDesignGenerator randomDesigns,
QuickSelectedDesign quickSelectedDesign)
public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent)
{
_jobs = jobs;
_actors = actors;
_saveService = saveService;
_designs = designs;
_event = @event;
_designEvent = designEvent;
_randomDesigns = randomDesigns;
_quickSelectedDesign = quickSelectedDesign;
_jobs = jobs;
_actors = actors;
_saveService = saveService;
_designs = designs;
_event = @event;
_designEvent = designEvent;
_designEvent.Subscribe(OnDesignChange, DesignChanged.Priority.AutoDesignManager);
Load();
migrator.ConsumeMigratedData(_actors, fileSystem, this);
@ -235,34 +232,18 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
_event.Invoke(AutomationChanged.Type.ChangedBase, set, (old, newBase));
}
public void ChangeResetSettings(int whichSet, bool newValue)
{
if (whichSet >= _data.Count || whichSet < 0)
return;
var set = _data[whichSet];
if (newValue == set.ResetTemporarySettings)
return;
var old = set.ResetTemporarySettings;
set.ResetTemporarySettings = newValue;
Save();
Glamourer.Log.Debug($"Changed resetting of temporary settings of set {whichSet + 1} from {old} to {newValue}.");
_event.Invoke(AutomationChanged.Type.ChangedTemporarySettingsReset, set, newValue);
}
public void AddDesign(AutoDesignSet set, IDesignStandIn design)
public void AddDesign(AutoDesignSet set, Design? design)
{
var newDesign = new AutoDesign()
{
Design = design,
Type = ApplicationType.All,
Jobs = _jobs.JobGroups[1],
Design = design,
ApplicationType = AutoDesign.Type.All,
Jobs = _jobs.JobGroups[1],
};
set.Designs.Add(newDesign);
Save();
Glamourer.Log.Debug(
$"Added new associated design {design.ResolveName(true)} as design {set.Designs.Count} to design set.");
$"Added new associated design {design?.Identifier.ToString() ?? "Reverter"} as design {set.Designs.Count} to design set.");
_event.Invoke(AutomationChanged.Type.AddedDesign, set, set.Designs.Count - 1);
}
@ -302,20 +283,20 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
_event.Invoke(AutomationChanged.Type.MovedDesign, set, (from, to));
}
public void ChangeDesign(AutoDesignSet set, int which, IDesignStandIn newDesign)
public void ChangeDesign(AutoDesignSet set, int which, Design? newDesign)
{
if (which >= set.Designs.Count || which < 0)
return;
var design = set.Designs[which];
if (design.Design.Equals(newDesign))
if (design.Design?.Identifier == newDesign?.Identifier)
return;
var old = design.Design;
design.Design = newDesign;
Save();
Glamourer.Log.Debug(
$"Changed linked design from {old.ResolveName(true)} to {newDesign.ResolveName(true)} for associated design {which + 1} in design set.");
$"Changed linked design from {old?.Identifier.ToString() ?? "Reverter"} to {newDesign?.Identifier.ToString() ?? "Reverter"} for associated design {which + 1} in design set.");
_event.Invoke(AutomationChanged.Type.ChangedDesign, set, (which, old, newDesign));
}
@ -325,7 +306,6 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
return;
var design = set.Designs[which];
if (design.Jobs.Id == jobs.Id)
return;
@ -336,51 +316,21 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
_event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, jobs));
}
public void ChangeGearsetCondition(AutoDesignSet set, int which, short index)
public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type)
{
if (which >= set.Designs.Count || which < 0)
return;
type &= AutoDesign.Type.All;
var design = set.Designs[which];
if (design.GearsetIndex == index)
if (design.ApplicationType == type)
return;
var old = design.GearsetIndex;
design.GearsetIndex = index;
var old = design.ApplicationType;
design.ApplicationType = type;
Save();
Glamourer.Log.Debug($"Changed gearset condition from {old} to {index} for associated design {which + 1} in design set.");
_event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, index));
}
public void ChangeApplicationType(AutoDesignSet set, int which, ApplicationType applicationType)
{
if (which >= set.Designs.Count || which < 0)
return;
applicationType &= ApplicationType.All;
var design = set.Designs[which];
if (design.Type == applicationType)
return;
var old = design.Type;
design.Type = applicationType;
Save();
Glamourer.Log.Debug($"Changed application type from {old} to {applicationType} for associated design {which + 1} in design set.");
_event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, applicationType));
}
public void ChangeData(AutoDesignSet set, int which, object data)
{
if (which >= set.Designs.Count || which < 0)
return;
var design = set.Designs[which];
if (!design.Design.ChangeData(data))
return;
Save();
Glamourer.Log.Debug($"Changed additional design data for associated design {which + 1} in design set.");
_event.Invoke(AutomationChanged.Type.ChangedData, set, (which, data));
Glamourer.Log.Debug($"Changed application type from {old} to {type} for associated design {which + 1} in design set.");
_event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, type));
}
public string ToFilename(FilenameService fileNames)
@ -388,8 +338,10 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
public void Save(StreamWriter writer)
{
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
using var j = new JsonTextWriter(writer)
{
Formatting = Formatting.Indented,
};
Serialize().WriteTo(j);
}
@ -452,7 +404,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
continue;
}
var id = _actors.FromJson(obj["Identifier"] as JObject);
var id = _actors.AwaitedService.FromJson(obj["Identifier"] as JObject);
if (!IdentifierValid(id, out var group))
{
Glamourer.Messager.NotificationMessage("Skipped loading Automation Set: Invalid Identifier.", NotificationType.Warning);
@ -461,9 +413,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
var set = new AutoDesignSet(name, group)
{
Enabled = obj["Enabled"]?.ToObject<bool>() ?? false,
ResetTemporarySettings = obj["ResetTemporarySettings"]?.ToObject<bool>() ?? false,
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? AutoDesignSet.Base.Current,
Enabled = obj["Enabled"]?.ToObject<bool>() ?? false,
BaseState = obj["BaseState"]?.ToObject<AutoDesignSet.Base>() ?? AutoDesignSet.Base.Current,
};
if (set.Enabled)
@ -489,7 +440,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
continue;
}
if (ToDesignObject(set.Name, j) is { } design)
var design = ToDesignObject(set.Name, j);
if (design != null)
set.Designs.Add(design);
}
}
@ -497,85 +449,58 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
private AutoDesign? ToDesignObject(string setName, JObject jObj)
{
var designIdentifier = jObj["Design"]?.ToObject<string?>();
IDesignStandIn? design;
// designIdentifier == null means Revert-Design for backwards compatibility
if (designIdentifier is null or RevertDesign.SerializedName)
{
design = new RevertDesign();
}
else if (designIdentifier is RandomDesign.SerializedName)
{
design = new RandomDesign(_randomDesigns);
}
else if (designIdentifier is QuickSelectedDesign.SerializedName)
{
design = _quickSelectedDesign;
}
else
var designIdentifier = jObj["Design"]?.ToObject<string?>();
Design? design = null;
// designIdentifier == null means Revert-Design.
if (designIdentifier != null)
{
if (designIdentifier.Length == 0)
{
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.",
NotificationType.Warning);
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.", NotificationType.Warning);
return null;
}
if (!Guid.TryParse(designIdentifier, out var guid))
{
Glamourer.Messager.NotificationMessage(
$"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.",
NotificationType.Warning);
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.", NotificationType.Warning);
return null;
}
if (!_designs.Designs.TryGetValue(guid, out var d))
design = _designs.Designs.FirstOrDefault(d => d.Identifier == guid);
if (design == null)
{
Glamourer.Messager.NotificationMessage(
$"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.",
NotificationType.Warning);
$"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", NotificationType.Warning);
return null;
}
design = d;
}
design.ParseData(jObj);
var applicationType = (AutoDesign.Type)(jObj["ApplicationType"]?.ToObject<uint>() ?? 0);
// ApplicationType is a migration from an older property name.
var applicationType = (ApplicationType)(jObj["Type"]?.ToObject<uint>() ?? jObj["ApplicationType"]?.ToObject<uint>() ?? 0);
var ret = new AutoDesign
var ret = new AutoDesign()
{
Design = design,
Type = applicationType & ApplicationType.All,
Design = design,
ApplicationType = applicationType & AutoDesign.Type.All,
};
return ParseConditions(setName, jObj, ret) ? ret : null;
}
private bool ParseConditions(string setName, JObject jObj, AutoDesign ret)
{
var conditions = jObj["Conditions"];
if (conditions == null)
return true;
return ret;
var jobs = conditions["JobGroup"]?.ToObject<int>() ?? -1;
if (jobs >= 0)
{
if (!_jobs.JobGroups.TryGetValue((JobGroupId)jobs, out var jobGroup))
if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup))
{
Glamourer.Messager.NotificationMessage(
$"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.",
NotificationType.Warning);
return false;
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.", NotificationType.Warning);
return null;
}
ret.Jobs = jobGroup;
}
ret.GearsetIndex = conditions["Gearset"]?.ToObject<short>() ?? -1;
return true;
return ret;
}
private void Save()
@ -588,13 +513,12 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
IdentifierType.Player => true,
IdentifierType.Retainer => true,
IdentifierType.Npc => true,
IdentifierType.Owned => true,
_ => false,
};
if (!validType)
{
group = [];
group = Array.Empty<ActorIdentifier>();
return false;
}
@ -605,42 +529,42 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
private ActorIdentifier[] GetGroup(ActorIdentifier identifier)
{
if (!identifier.IsValid)
return [];
return identifier.Type switch
{
IdentifierType.Player =>
[
identifier.CreatePermanent(),
],
IdentifierType.Retainer =>
[
_actors.CreateRetainer(identifier.PlayerName,
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
? ActorIdentifier.RetainerType.Mannequin
: ActorIdentifier.RetainerType.Bell).CreatePermanent(),
],
IdentifierType.Npc => CreateNpcs(_actors, identifier),
IdentifierType.Owned => CreateNpcs(_actors, identifier),
_ => [],
};
return Array.Empty<ActorIdentifier>();
static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier)
{
var name = manager.Data.ToName(identifier.Kind, identifier.DataId);
var table = identifier.Kind switch
{
ObjectKind.BattleNpc => (IReadOnlyDictionary<NpcId, string>)manager.Data.BNpcs,
ObjectKind.BattleNpc => manager.Data.BNpcs,
ObjectKind.EventNpc => manager.Data.ENpcs,
_ => new Dictionary<NpcId, string>(),
_ => new Dictionary<uint, string>(),
};
return table.Where(kvp => kvp.Value == name)
.Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id,
identifier.Kind, kvp.Key)).ToArray();
identifier.Kind,
kvp.Key)).ToArray();
}
return identifier.Type switch
{
IdentifierType.Player => new[]
{
identifier.CreatePermanent(),
},
IdentifierType.Retainer => new[]
{
_actors.AwaitedService.CreateRetainer(identifier.PlayerName,
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
? ActorIdentifier.RetainerType.Mannequin
: ActorIdentifier.RetainerType.Bell).CreatePermanent(),
},
IdentifierType.Npc => CreateNpcs(_actors.AwaitedService, identifier),
_ => Array.Empty<ActorIdentifier>(),
};
}
private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _)
private void OnDesignChange(DesignChanged.Type type, Design design, object? data)
{
if (type is not DesignChanged.Type.Deleted)
return;

View file

@ -1,17 +1,17 @@
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Actors;
namespace Glamourer.Automation;
public class AutoDesignSet(string name, ActorIdentifier[] identifiers, List<AutoDesign> designs)
public class AutoDesignSet
{
public readonly List<AutoDesign> Designs = designs;
public readonly List<AutoDesign> Designs;
public string Name = name;
public ActorIdentifier[] Identifiers = identifiers;
public string Name;
public ActorIdentifier[] Identifiers;
public bool Enabled;
public Base BaseState = Base.Current;
public bool ResetTemporarySettings = false;
public Base BaseState = Base.Current;
public JObject Serialize()
{
@ -21,19 +21,25 @@ public class AutoDesignSet(string name, ActorIdentifier[] identifiers, List<Auto
return new JObject()
{
["Name"] = Name,
["Identifier"] = Identifiers[0].ToJson(),
["Enabled"] = Enabled,
["BaseState"] = BaseState.ToString(),
["ResetTemporarySettings"] = ResetTemporarySettings.ToString(),
["Designs"] = list,
["Name"] = Name,
["Identifier"] = Identifiers[0].ToJson(),
["Enabled"] = Enabled,
["BaseState"] = BaseState.ToString(),
["Designs"] = list,
};
}
public AutoDesignSet(string name, params ActorIdentifier[] identifiers)
: this(name, identifiers, [])
: this(name, identifiers, new List<AutoDesign>())
{ }
public AutoDesignSet(string name, ActorIdentifier[] identifiers, List<AutoDesign> designs)
{
Name = name;
Identifiers = identifiers;
Designs = designs;
}
public enum Base : byte
{
Current,

View file

@ -1,51 +1,61 @@
using Dalamud.Interface.ImGuiNotification;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Designs;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Structs;
using Penumbra.String;
namespace Glamourer.Automation;
public class FixedDesignMigrator(JobService jobs)
public class FixedDesignMigrator
{
private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData;
private readonly JobService _jobs;
private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData;
public void ConsumeMigratedData(ActorManager actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager)
public FixedDesignMigrator(JobService jobs)
=> _jobs = jobs;
public void ConsumeMigratedData(ActorService actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager)
{
if (_migratedData == null)
return;
foreach (var (name, data) in _migratedData)
foreach (var data in _migratedData)
{
var allEnabled = true;
var name = data.Name;
if (autoManager.Any(d => name == d.Name))
continue;
var id = ActorIdentifier.Invalid;
if (ByteString.FromString(name, out var byteString))
if (ByteString.FromString(data.Name, out var byteString, false))
{
id = actors.CreatePlayer(byteString, ushort.MaxValue);
id = actors.AwaitedService.CreatePlayer(byteString, ushort.MaxValue);
if (!id.IsValid)
id = actors.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both);
id = actors.AwaitedService.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both);
}
if (!id.IsValid)
{
byteString = ByteString.FromSpanUnsafe("Mig Ration"u8, true, false, true);
id = actors.CreatePlayer(byteString, actors.Data.Worlds.First().Key);
id = actors.AwaitedService.CreatePlayer(byteString, actors.AwaitedService.Data.Worlds.First().Key);
if (!id.IsValid)
{
Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {name}.", NotificationType.Error);
Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {data.Name}.", NotificationType.Error);
allEnabled = false;
continue;
}
}
autoManager.AddDesignSet(name, id);
autoManager.SetState(autoManager.Count - 1, true);
autoManager.SetState(autoManager.Count - 1, allEnabled);
var set = autoManager[^1];
foreach (var design in data.AsEnumerable().Reverse())
foreach (var design in data.Data.AsEnumerable().Reverse())
{
if (!designFileSystem.Find(design.Item1, out var child) || child is not DesignFileSystem.Leaf leaf)
{
@ -56,7 +66,7 @@ public class FixedDesignMigrator(JobService jobs)
autoManager.AddDesign(set, leaf.Value);
autoManager.ChangeJobCondition(set, set.Designs.Count - 1, design.Item2);
autoManager.ChangeApplicationType(set, set.Designs.Count - 1, design.Item3 ? ApplicationType.All : 0);
autoManager.ChangeApplicationType(set, set.Designs.Count - 1, design.Item3 ? AutoDesign.Type.All : 0);
}
}
}
@ -86,7 +96,7 @@ public class FixedDesignMigrator(JobService jobs)
}
var job = obj["JobGroups"]?.ToObject<int>() ?? -1;
if (job < 0 || !jobs.JobGroups.TryGetValue((JobGroupId)job, out var group))
if (job < 0 || !_jobs.JobGroups.TryGetValue((ushort)job, out var group))
{
Glamourer.Messager.NotificationMessage("Could not semi-migrate fixed design: Invalid job group specified.",
NotificationType.Warning);

View file

@ -1,97 +1,51 @@
using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface.ImGuiNotification;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Configuration;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Designs;
using Glamourer.Gui;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Services;
using Newtonsoft.Json;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Filesystem;
using OtterGui.Widgets;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Glamourer;
public enum HeightDisplayType
{
None,
Centimetre,
Metre,
Wrong,
WrongFoot,
Corgi,
OlympicPool,
}
public class DefaultDesignSettings
{
public bool AlwaysForceRedrawing = false;
public bool ResetAdvancedDyes = false;
public bool ShowQuickDesignBar = true;
public bool ResetTemporarySettings = false;
public bool Locked = false;
}
public class Configuration : IPluginConfiguration, ISavable
{
[JsonIgnore]
public readonly EphemeralConfig Ephemeral;
public bool Enabled { get; set; } = true;
public bool UseRestrictedGearProtection { get; set; } = false;
public bool OpenFoldersByDefault { get; set; } = false;
public bool AutoRedrawEquipOnChanges { get; set; } = false;
public bool EnableAutoDesigns { get; set; } = true;
public bool IncognitoMode { get; set; } = false;
public bool UnlockDetailMode { get; set; } = true;
public bool HideApplyCheckmarks { get; set; } = false;
public bool SmallEquip { get; set; } = false;
public bool UnlockedItemMode { get; set; } = false;
public byte DisableFestivals { get; set; } = 1;
public bool EnableGameContextMenu { get; set; } = true;
public bool HideWindowInCutscene { get; set; } = false;
public bool ShowAutomationSetEditing { get; set; } = true;
public bool ShowAllAutomatedApplicationRules { get; set; } = true;
public bool ShowUnlockedItemWarnings { get; set; } = true;
public bool RevertManualChangesOnZoneChange { get; set; } = false;
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public bool AttachToPcp { get; set; } = true;
public bool UseRestrictedGearProtection { get; set; } = false;
public bool OpenFoldersByDefault { get; set; } = false;
public bool AutoRedrawEquipOnChanges { get; set; } = false;
public bool EnableAutoDesigns { get; set; } = true;
public bool HideApplyCheckmarks { get; set; } = false;
public bool SmallEquip { get; set; } = false;
public bool UnlockedItemMode { get; set; } = false;
public byte DisableFestivals { get; set; } = 1;
public bool EnableGameContextMenu { get; set; } = true;
public bool HideWindowInCutscene { get; set; } = false;
public bool ShowAutomationSetEditing { get; set; } = true;
public bool ShowAllAutomatedApplicationRules { get; set; } = true;
public bool ShowUnlockedItemWarnings { get; set; } = true;
public bool RevertManualChangesOnZoneChange { get; set; } = false;
public bool ShowQuickBarInTabs { get; set; } = true;
public bool OpenWindowAtStart { get; set; } = false;
public bool ShowWindowWhenUiHidden { get; set; } = false;
public bool KeepAdvancedDyesAttached { get; set; } = true;
public bool ShowPalettePlusImport { get; set; } = true;
public bool UseFloatForColors { get; set; } = true;
public bool UseRgbForColors { get; set; } = true;
public bool ShowColorConfig { get; set; } = true;
public bool ChangeEntireItem { get; set; } = false;
public bool AlwaysApplyAssociatedMods { get; set; } = true;
public bool UseTemporarySettings { get; set; } = true;
public bool AllowDoubleClickToApply { get; set; } = false;
public bool RespectManualOnAutomationUpdate { get; set; } = false;
public bool PreventRandomRepeats { get; set; } = false;
public string PcpFolder { get; set; } = "PCP";
public string PcpColor { get; set; } = "";
public DesignPanelFlag HideDesignPanel { get; set; } = 0;
public DesignPanelFlag AutoExpandDesignPanel { get; set; } = 0;
public DefaultDesignSettings DefaultDesignSettings { get; set; } = new();
public HeightDisplayType HeightDisplayType { get; set; } = HeightDisplayType.Centimetre;
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
public ModifiableHotkey ToggleQuickDesignBar { get; set; } = new(VirtualKey.NO_KEY);
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control);
public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion;
public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New;
public QdbButtons QdbButtons { get; set; } =
QdbButtons.ApplyDesign | QdbButtons.RevertAll | QdbButtons.RevertAutomation | QdbButtons.RevertAdvancedDyes;
[JsonConverter(typeof(SortModeConverter))]
[JsonProperty(Order = int.MaxValue)]
public ISortMode<Design> SortMode { get; set; } = ISortMode<Design>.FoldersFirst;
public List<(string Code, bool Enabled)> Codes { get; set; } = [];
public List<(string Code, bool Enabled)> Codes { get; set; } = new();
#if DEBUG
public bool DebugMode { get; set; } = true;
@ -107,18 +61,24 @@ public class Configuration : IPluginConfiguration, ISavable
[JsonIgnore]
private readonly SaveService _saveService;
public Configuration(SaveService saveService, ConfigMigrationService migrator, EphemeralConfig ephemeral)
public Configuration(SaveService saveService, ConfigMigrationService migrator)
{
_saveService = saveService;
Ephemeral = ephemeral;
Load(migrator);
}
public void Save()
=> _saveService.DelaySave(this);
private void Load(ConfigMigrationService migrator)
public void Load(ConfigMigrationService migrator)
{
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
{
Glamourer.Log.Error(
$"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
errorArgs.ErrorContext.Handled = true;
}
if (!File.Exists(_saveService.FileNames.ConfigFile))
return;
@ -139,14 +99,6 @@ public class Configuration : IPluginConfiguration, ISavable
}
migrator.Migrate(this);
return;
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
{
Glamourer.Log.Error(
$"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
errorArgs.ErrorContext.Handled = true;
}
}
public string ToFilename(FilenameService fileNames)
@ -154,18 +106,17 @@ public class Configuration : IPluginConfiguration, ISavable
public void Save(StreamWriter writer)
{
using var jWriter = new JsonTextWriter(writer);
jWriter.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}
public static class Constants
{
public const int CurrentVersion = 8;
public const int CurrentVersion = 4;
public static readonly ISortMode<Design>[] ValidSortModes =
[
{
ISortMode<Design>.FoldersFirst,
ISortMode<Design>.Lexicographical,
new DesignFileSystem.CreationDate(),
@ -178,7 +129,7 @@ public class Configuration : IPluginConfiguration, ISavable
ISortMode<Design>.InverseFoldersLast,
ISortMode<Design>.InternalOrder,
ISortMode<Design>.InverseInternalOrder,
];
};
}
/// <summary> Convert SortMode Types to their name. </summary>

View file

@ -1,96 +0,0 @@
using Glamourer.Designs;
using Dalamud.Bindings.ImGui;
using OtterGui.Text;
using OtterGui.Text.EndObjects;
namespace Glamourer;
[Flags]
public enum DesignPanelFlag : uint
{
Customization = 0x0001,
Equipment = 0x0002,
AdvancedCustomizations = 0x0004,
AdvancedDyes = 0x0008,
AppearanceDetails = 0x0010,
DesignDetails = 0x0020,
ModAssociations = 0x0040,
DesignLinks = 0x0080,
ApplicationRules = 0x0100,
DebugData = 0x0200,
}
public static class DesignPanelFlagExtensions
{
public static ReadOnlySpan<byte> ToName(this DesignPanelFlag flag)
=> flag switch
{
DesignPanelFlag.Customization => "Customization"u8,
DesignPanelFlag.Equipment => "Equipment"u8,
DesignPanelFlag.AdvancedCustomizations => "Advanced Customization"u8,
DesignPanelFlag.AdvancedDyes => "Advanced Dyes"u8,
DesignPanelFlag.DesignDetails => "Design Details"u8,
DesignPanelFlag.ApplicationRules => "Application Rules"u8,
DesignPanelFlag.ModAssociations => "Mod Associations"u8,
DesignPanelFlag.DesignLinks => "Design Links"u8,
DesignPanelFlag.DebugData => "Debug Data"u8,
DesignPanelFlag.AppearanceDetails => "Appearance Details"u8,
_ => ""u8,
};
public static CollapsingHeader Header(this DesignPanelFlag flag, Configuration config)
{
if (config.HideDesignPanel.HasFlag(flag))
return new CollapsingHeader()
{
Disposed = true,
};
var expand = config.AutoExpandDesignPanel.HasFlag(flag);
return ImUtf8.CollapsingHeaderId(flag.ToName(), expand ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None);
}
public static void DrawTable(ReadOnlySpan<byte> label, DesignPanelFlag hidden, DesignPanelFlag expanded, Action<DesignPanelFlag> setterHide,
Action<DesignPanelFlag> setterExpand)
{
var checkBoxWidth = Math.Max(ImGui.GetFrameHeight(), ImUtf8.CalcTextSize("Expand"u8).X);
var textWidth = ImUtf8.CalcTextSize(DesignPanelFlag.AdvancedCustomizations.ToName()).X;
var tableSize = 2 * (textWidth + 2 * checkBoxWidth) + 10 * ImGui.GetStyle().CellPadding.X + 2 * ImGui.GetStyle().WindowPadding.X + 2 * ImGui.GetStyle().FrameBorderSize;
using var table = ImUtf8.Table(label, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders, new Vector2(tableSize, 6 * ImGui.GetFrameHeight()));
if (!table)
return;
var headerColor = ImGui.GetColorU32(ImGuiCol.TableHeaderBg);
var checkBoxOffset = (checkBoxWidth - ImGui.GetFrameHeight()) / 2;
ImUtf8.TableSetupColumn("Panel##1"u8, ImGuiTableColumnFlags.WidthFixed, textWidth);
ImUtf8.TableSetupColumn("Show##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
ImUtf8.TableSetupColumn("Expand##1"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
ImUtf8.TableSetupColumn("Panel##2"u8, ImGuiTableColumnFlags.WidthFixed, textWidth);
ImUtf8.TableSetupColumn("Show##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
ImUtf8.TableSetupColumn("Expand##2"u8, ImGuiTableColumnFlags.WidthFixed, checkBoxWidth);
ImGui.TableHeadersRow();
foreach (var panel in Enum.GetValues<DesignPanelFlag>())
{
using var id = ImUtf8.PushId((int)panel);
ImGui.TableNextColumn();
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, headerColor);
ImUtf8.TextFrameAligned(panel.ToName());
var isShown = !hidden.HasFlag(panel);
var isExpanded = expanded.HasFlag(panel);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset);
if (ImUtf8.Checkbox("##show"u8, ref isShown))
setterHide.Invoke(isShown ? hidden & ~panel : hidden | panel);
ImUtf8.HoverTooltip(
"Show this panel and associated functionality in all relevant tabs.\n\nToggling this off does NOT disable any functionality, just the display of it, so hide panels at your own risk."u8);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + checkBoxOffset);
if (ImUtf8.Checkbox("##expand"u8, ref isExpanded))
setterExpand.Invoke(isExpanded ? expanded | panel : expanded & ~panel);
ImUtf8.HoverTooltip("Expand this panel by default in all relevant tabs."u8);
}
}
}

View file

@ -1,68 +0,0 @@
using Glamourer.Api.Enums;
using Glamourer.GameData;
using Dalamud.Bindings.ImGui;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public record struct ApplicationCollection(
EquipFlag Equip,
BonusItemFlag BonusItem,
CustomizeFlag CustomizeRaw,
CrestFlag Crest,
CustomizeParameterFlag Parameters,
MetaFlag Meta)
{
public static readonly ApplicationCollection All = new(EquipFlagExtensions.All, BonusExtensions.All,
CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All);
public static readonly ApplicationCollection None = new(0, 0, CustomizeFlag.BodyType, 0, 0, 0);
public static readonly ApplicationCollection Equipment = new(EquipFlagExtensions.All, BonusExtensions.All,
CustomizeFlag.BodyType, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState | MetaFlag.EarState);
public static readonly ApplicationCollection Customizations = new(0, 0, CustomizeFlagExtensions.AllRelevant, 0,
CustomizeParameterExtensions.All, MetaFlag.Wetness);
public static readonly ApplicationCollection Default = new(EquipFlagExtensions.All, BonusExtensions.All,
CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState);
public static ApplicationCollection FromKeys()
=> (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch
{
(false, false) => All,
(true, true) => All,
(true, false) => Equipment,
(false, true) => Customizations,
};
public CustomizeFlag Customize
{
get => CustomizeRaw;
set => CustomizeRaw = value | CustomizeFlag.BodyType;
}
public void RemoveEquip()
{
Equip = 0;
BonusItem = 0;
Crest = 0;
Meta &= ~(MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState);
}
public void RemoveCustomize()
{
Customize = 0;
Parameters = 0;
Meta &= MetaFlag.Wetness;
}
public ApplicationCollection Restrict(ApplicationCollection old)
=> new(old.Equip & Equip, old.BonusItem & BonusItem, (old.Customize & Customize) | CustomizeFlag.BodyType, old.Crest & Crest,
old.Parameters & Parameters, old.Meta & Meta);
public ApplicationCollection CloneSecure()
=> new(Equip & EquipFlagExtensions.All, BonusItem & BonusExtensions.All,
(Customize & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType, Crest & CrestExtensions.AllRelevant,
Parameters & CustomizeParameterExtensions.All, Meta & MetaExtensions.All);
}

View file

@ -1,71 +0,0 @@
using Glamourer.Api.Enums;
using Glamourer.GameData;
using Glamourer.State;
using Dalamud.Bindings.ImGui;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public readonly struct ApplicationRules(ApplicationCollection application, bool materials)
{
public static readonly ApplicationRules All = new(ApplicationCollection.All, true);
public static ApplicationRules FromModifiers(ActorState state)
=> FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
public static ApplicationRules NpcFromModifiers()
=> NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
public static ApplicationRules AllButParameters(ActorState state)
=> new(ApplicationCollection.All with { Parameters = ComputeParameters(state.ModelData, state.BaseData, All.Parameters) }, true);
public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift)
{
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
var visor = equip != 0 ? MetaFlag.VisorState : 0;
return new ApplicationRules(new ApplicationCollection(equip, 0, customize, 0, 0, visor), false);
}
public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift)
{
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
var bonus = equip == 0 ? 0 : BonusExtensions.All;
var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant;
var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All;
var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0;
if (equip != 0)
meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState;
var collection = new ApplicationCollection(equip, bonus, customize, crest,
ComputeParameters(state.ModelData, state.BaseData, parameters), meta);
return new ApplicationRules(collection, equip != 0);
}
public void Apply(DesignBase design)
=> design.Application = application;
public EquipFlag Equip
=> application.Equip & EquipFlagExtensions.All;
public CustomizeParameterFlag Parameters
=> application.Parameters & CustomizeParameterExtensions.All;
public bool Materials
=> materials;
private static CustomizeParameterFlag ComputeParameters(in DesignData model, in DesignData game,
CustomizeParameterFlag baseFlags = CustomizeParameterExtensions.All)
{
foreach (var flag in baseFlags.Iterate())
{
var modelValue = model.Parameters[flag];
var gameValue = game.Parameters[flag];
if (modelValue.NearEqual(gameValue))
baseFlags &= ~flag;
}
return baseFlags;
}
}

View file

@ -1,24 +1,21 @@
using Dalamud.Interface.ImGuiNotification;
using Glamourer.Automation;
using Glamourer.Designs.Links;
using Glamourer.Interop.Material;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Structs;
using Notification = OtterGui.Classes.Notification;
namespace Glamourer.Designs;
public sealed class Design : DesignBase, ISavable, IDesignStandIn
public sealed class Design : DesignBase, ISavable
{
#region Data
internal Design(CustomizeService customize, ItemManager items)
: base(customize, items)
internal Design(CustomizationService customize, ItemManager items)
: base(items)
{ }
internal Design(DesignBase other)
@ -28,101 +25,47 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
internal Design(Design other)
: base(other)
{
Tags = [.. other.Tags];
Description = other.Description;
QuickDesign = other.QuickDesign;
ForcedRedraw = other.ForcedRedraw;
ResetAdvancedDyes = other.ResetAdvancedDyes;
ResetTemporarySettings = other.ResetTemporarySettings;
Color = other.Color;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
Links = Links.Clone();
Tags = Tags.ToArray();
Description = Description;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
}
// Metadata
public new const int FileVersion = 2;
public new const int FileVersion = 1;
public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; internal init; }
public DateTimeOffset LastEdit { get; internal set; }
public LowerString Name { get; internal set; } = LowerString.Empty;
public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; internal set; } = [];
public int Index { get; internal set; }
public bool ForcedRedraw { get; internal set; }
public bool ResetAdvancedDyes { get; internal set; }
public bool ResetTemporarySettings { get; internal set; }
public bool QuickDesign { get; internal set; } = true;
public string Color { get; internal set; } = string.Empty;
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = [];
public LinkContainer Links { get; private set; } = [];
public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; internal init; }
public DateTimeOffset LastEdit { get; internal set; }
public LowerString Name { get; internal set; } = LowerString.Empty;
public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; internal set; } = Array.Empty<string>();
public int Index { get; internal set; }
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = new();
public string Incognito
=> Identifier.ToString()[..8];
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
=> LinkContainer.GetAllLinks(this).Select(t => ((IDesignStandIn)t.Link.Link, t.Link.Type, JobFlag.All));
#endregion
#region IDesignStandIn
public string ResolveName(bool incognito)
=> incognito ? Incognito : Name.Text;
public string SerializeName()
=> Identifier.ToString();
public ref readonly DesignData GetDesignData(in DesignData baseData)
=> ref GetDesignDataRef();
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData()
=> Materials;
public bool Equals(IDesignStandIn? other)
=> other is Design d && d.Identifier == Identifier;
public StateSource AssociatedSource()
=> StateSource.Manual;
public void AddData(JObject _)
{ }
public void ParseData(JObject _)
{ }
public bool ChangeData(object data)
=> false;
#endregion
#region Serialization
public new JObject JsonSerialize()
{
var ret = new JObject
{
["FileVersion"] = FileVersion,
["Identifier"] = Identifier,
["CreationDate"] = CreationDate,
["LastEdit"] = LastEdit,
["Name"] = Name.Text,
["Description"] = Description,
["ForcedRedraw"] = ForcedRedraw,
["ResetAdvancedDyes"] = ResetAdvancedDyes,
["ResetTemporarySettings"] = ResetTemporarySettings,
["Color"] = Color,
["QuickDesign"] = QuickDesign,
["Tags"] = JArray.FromObject(Tags),
["WriteProtected"] = WriteProtected(),
["Equipment"] = SerializeEquipment(),
["Bonus"] = SerializeBonusItems(),
["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
["Mods"] = SerializeMods(),
["Links"] = Links.Serialize(),
};
var ret = new JObject()
{
["FileVersion"] = FileVersion,
["Identifier"] = Identifier,
["CreationDate"] = CreationDate,
["LastEdit"] = LastEdit,
["Name"] = Name.Text,
["Description"] = Description,
["Tags"] = JArray.FromObject(Tags),
["WriteProtected"] = WriteProtected(),
["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(),
["Mods"] = SerializeMods(),
}
;
return ret;
}
@ -131,17 +74,12 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
var ret = new JArray();
foreach (var (mod, settings) in AssociatedMods)
{
var obj = new JObject
var obj = new JObject()
{
["Name"] = mod.Name,
["Directory"] = mod.DirectoryName,
["Enabled"] = settings.Enabled,
};
if (settings.Remove)
obj["Remove"] = true;
else if (settings.ForceInherit)
obj["Inherit"] = true;
else
obj["Enabled"] = settings.Enabled;
if (settings.Enabled)
{
obj["Priority"] = settings.Priority;
@ -158,84 +96,24 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
#region Deserialization
public static Design LoadDesign(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader,
JObject json)
public static Design LoadDesign(CustomizationService customizations, ItemManager items, JObject json)
{
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch
{
1 => LoadDesignV1(saveService, customizations, items, linkLoader, json),
FileVersion => LoadDesignV2(customizations, items, linkLoader, json),
FileVersion => LoadDesignV1(customizations, items, json),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
}
/// <summary> The values for gloss and specular strength were switched. Swap them for all appropriate designs. </summary>
private static Design LoadDesignV1(SaveService saveService, CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader,
JObject json)
private static Design LoadDesignV1(CustomizationService customizations, ItemManager items, JObject json)
{
var design = LoadDesignV2(customizations, items, linkLoader, json);
var materialDesignData = design.GetMaterialDataRef();
if (materialDesignData.Values.Count == 0)
return design;
var materialData = materialDesignData.Clone();
// Guesstimate whether to migrate material rows:
// Update 1.3.0.10 released at that time, so any design last updated before that can be migrated.
if (design.LastEdit <= new DateTime(2024, 8, 7, 16, 0, 0, DateTimeKind.Utc))
static string[] ParseTags(JObject json)
{
Migrate("because it was saved the wrong way around before 1.3.0.10, and this design was not changed since that release.");
}
else
{
var hasNegativeGloss = false;
var hasNonPositiveGloss = false;
var specularLarger = 0;
foreach (var (key, value) in materialData.GetValues(MaterialValueIndex.Min(), MaterialValueIndex.Max()))
{
hasNegativeGloss |= value.Value.GlossStrength < 0;
hasNonPositiveGloss |= value.Value.GlossStrength <= 0;
if (value.Value.SpecularStrength > value.Value.GlossStrength)
++specularLarger;
}
// If there is any negative gloss, this is wrong and can be migrated.
if (hasNegativeGloss)
Migrate("because it had a negative Gloss value, which is not supported and thus probably outdated.");
// If there is any non-positive Gloss and some specular values that are larger, it is probably wrong and can be migrated.
else if (hasNonPositiveGloss && specularLarger > 0)
Migrate("because it had a zero Gloss value, and at least one Specular Strength larger than the Gloss, which is unusual.");
// If most of the specular strengths are larger, it is probably wrong and can be migrated.
else if (specularLarger > materialData.Values.Count / 2)
Migrate("because most of its Specular Strength values were larger than the Gloss values, which is unusual.");
var tags = json["Tags"]?.ToObject<string[]>() ?? Array.Empty<string>();
return tags.OrderBy(t => t).Distinct().ToArray();
}
return design;
void Migrate(string reason)
{
materialDesignData.Clear();
foreach (var (key, value) in materialData.GetValues(MaterialValueIndex.Min(), MaterialValueIndex.Max()))
{
var gloss = Math.Clamp(value.Value.SpecularStrength, 0, (float)Half.MaxValue);
var specularStrength = Math.Clamp(value.Value.GlossStrength, 0, (float)Half.MaxValue);
var colorRow = value.Value with
{
GlossStrength = gloss,
SpecularStrength = specularStrength,
};
materialDesignData.AddOrUpdateValue(MaterialValueIndex.FromKey(key), value with { Value = colorRow });
}
Glamourer.Messager.AddMessage(new Notification(
$"Swapped Gloss and Specular Strength in {materialDesignData.Values.Count} Rows in design {design.Incognito} {reason}",
NotificationType.Info));
saveService.Save(SaveType.ImmediateSync, design);
}
}
private static Design LoadDesignV2(CustomizeService customizations, ItemManager items, DesignLinkLoader linkLoader, JObject json)
{
var creationDate = json["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
var design = new Design(customizations, items)
@ -246,29 +124,14 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
Description = json["Description"]?.ToObject<string>() ?? string.Empty,
Tags = ParseTags(json),
LastEdit = json["LastEdit"]?.ToObject<DateTimeOffset>() ?? creationDate,
QuickDesign = json["QuickDesign"]?.ToObject<bool>() ?? true,
};
if (design.LastEdit < creationDate)
design.LastEdit = creationDate;
design.SetWriteProtected(json["WriteProtected"]?.ToObject<bool>() ?? false);
LoadCustomize(customizations, json["Customize"], design, design.Name, true, false);
LoadEquip(items, json["Equipment"], design, design.Name, true);
LoadBonus(items, design, json["Bonus"]);
LoadMods(json["Mods"], design);
LoadParameters(json["Parameters"], design, design.Name);
LoadMaterials(json["Materials"], design, design.Name);
LoadLinks(linkLoader, json["Links"], design);
design.Color = json["Color"]?.ToObject<string>() ?? string.Empty;
design.ForcedRedraw = json["ForcedRedraw"]?.ToObject<bool>() ?? false;
design.ResetAdvancedDyes = json["ResetAdvancedDyes"]?.ToObject<bool>() ?? false;
design.ResetTemporarySettings = json["ResetTemporarySettings"]?.ToObject<bool>() ?? false;
return design;
static string[] ParseTags(JObject json)
{
var tags = json["Tags"]?.ToObject<string[]>() ?? [];
return tags.OrderBy(t => t).Distinct().ToArray();
}
}
private static void LoadMods(JToken? mods, Design design)
@ -287,42 +150,16 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
continue;
}
var forceInherit = tok["Inherit"]?.ToObject<bool>() ?? false;
var removeSetting = tok["Remove"]?.ToObject<bool>() ?? false;
var settingsDict = tok["Settings"]?.ToObject<Dictionary<string, List<string>>>() ?? [];
var settings = new Dictionary<string, List<string>>(settingsDict.Count);
var settingsDict = tok["Settings"]?.ToObject<Dictionary<string, string[]>>() ?? new Dictionary<string, string[]>();
var settings = new SortedList<string, IList<string>>(settingsDict.Count);
foreach (var (key, value) in settingsDict)
settings.Add(key, value);
var priority = tok["Priority"]?.ToObject<int>() ?? 0;
if (!design.AssociatedMods.TryAdd(new Mod(name, directory),
new ModSettings(settings, priority, enabled.Value, forceInherit, removeSetting)))
if (!design.AssociatedMods.TryAdd(new Mod(name, directory), new ModSettings(settings, priority, enabled.Value)))
Glamourer.Messager.NotificationMessage("The loaded design contains a mod more than once, skipped.", NotificationType.Warning);
}
}
private static void LoadLinks(DesignLinkLoader linkLoader, JToken? links, Design design)
{
if (links is not JObject obj)
return;
Parse(obj["Before"] as JArray, LinkOrder.Before);
Parse(obj["After"] as JArray, LinkOrder.After);
return;
void Parse(JArray? array, LinkOrder order)
{
if (array == null)
return;
foreach (var jObj in array.OfType<JObject>())
{
var identifier = jObj["Design"]?.ToObject<Guid>() ?? throw new ArgumentNullException(nameof(design));
var type = (ApplicationType)(jObj["Type"]?.ToObject<uint>() ?? 0);
linkLoader.AddObject(design, new LinkData(identifier, type, order));
}
}
}
#endregion
#region ISavable

View file

@ -1,12 +1,14 @@
using Dalamud.Interface.ImGuiNotification;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using System;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.DataContainers;
namespace Glamourer.Designs;
@ -14,215 +16,172 @@ public class DesignBase
{
public const int FileVersion = 1;
private DesignData _designData = new();
private readonly DesignMaterialManager _materials = new();
/// <summary> For read-only information about custom material color changes. </summary>
public IReadOnlyList<(uint, MaterialValueDesign)> Materials
=> _materials.Values;
/// <summary> To make it clear something is edited here. </summary>
public DesignMaterialManager GetMaterialDataRef()
=> _materials;
/// <summary> For read-only information about the actual design. </summary>
public ref readonly DesignData DesignData
=> ref _designData;
/// <summary> To make it clear that something is edited here. </summary>
public ref DesignData GetDesignDataRef()
=> ref _designData;
internal DesignBase(CustomizeService customize, ItemManager items)
internal DesignBase(ItemManager items)
{
_designData.SetDefaultEquipment(items);
CustomizeSet = SetCustomizationSet(customize);
}
/// <summary> Used when importing .cma or .chara files. </summary>
internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags,
BonusItemFlag bonusFlags)
{
_designData = designData;
ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
Application.Equip = equipFlags & EquipFlagExtensions.All;
Application.BonusItem = bonusFlags & BonusExtensions.All;
Application.Meta = 0;
CustomizeSet = SetCustomizationSet(customize);
DesignData.SetDefaultEquipment(items);
}
internal DesignBase(DesignBase clone)
{
_designData = clone._designData;
_materials = clone._materials.Clone();
CustomizeSet = clone.CustomizeSet;
Application = clone.Application.CloneSecure();
DesignData = clone.DesignData;
ApplyCustomize = clone.ApplyCustomize & CustomizeFlagExtensions.AllRelevant;
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(CustomizeService customize, in DesignData other)
{
_designData = other;
CustomizeSet = SetCustomizationSet(customize);
}
internal DesignData DesignData = new();
#region Application Data
public CustomizeSet CustomizeSet { get; private set; }
public ApplicationCollection Application = ApplicationCollection.Default;
internal CustomizeFlag ApplyCustomize
[Flags]
private enum DesignFlags : byte
{
get => Application.Customize.FixApplication(CustomizeSet);
set => Application.Customize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType;
ApplyHatVisible = 0x01,
ApplyVisorState = 0x02,
ApplyWeaponVisible = 0x04,
ApplyWetness = 0x08,
WriteProtected = 0x10,
}
internal CustomizeFlag ApplyCustomizeExcludingBodyType
=> Application.Customize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType;
internal CustomizeFlag ApplyCustomize = CustomizeFlagExtensions.AllRelevant;
internal EquipFlag ApplyEquip = EquipFlagExtensions.All;
private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible;
private bool _writeProtected;
public bool DoApplyHatVisible()
=> _designFlags.HasFlag(DesignFlags.ApplyHatVisible);
public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize)
public bool DoApplyVisorToggle()
=> _designFlags.HasFlag(DesignFlags.ApplyVisorState);
public bool DoApplyWeaponVisible()
=> _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible);
public bool DoApplyWetness()
=> _designFlags.HasFlag(DesignFlags.ApplyWetness);
public bool WriteProtected()
=> _designFlags.HasFlag(DesignFlags.WriteProtected);
public bool SetApplyHatVisible(bool value)
{
if (customize.Equals(_designData.Customize))
var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible;
if (newFlag == _designFlags)
return false;
_designData.Customize = customize;
CustomizeSet = customizeService.Manager.GetSet(customize.Clan, customize.Gender);
_designFlags = newFlag;
return true;
}
public bool DoApplyMeta(MetaIndex index)
=> Application.Meta.HasFlag(index.ToFlag());
public bool WriteProtected()
=> _writeProtected;
public bool SetApplyMeta(MetaIndex index, bool value)
public bool SetApplyVisorToggle(bool value)
{
var newFlag = value ? Application.Meta | index.ToFlag() : Application.Meta & ~index.ToFlag();
if (newFlag == Application.Meta)
var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState;
if (newFlag == _designFlags)
return false;
Application.Meta = newFlag;
_designFlags = newFlag;
return true;
}
public bool SetApplyWeaponVisible(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyWetness(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyWetness : _designFlags & ~DesignFlags.ApplyWetness;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetWriteProtected(bool value)
{
if (value == _writeProtected)
var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected;
if (newFlag == _designFlags)
return false;
_writeProtected = value;
_designFlags = newFlag;
return true;
}
public bool DoApplyEquip(EquipSlot slot)
=> Application.Equip.HasFlag(slot.ToFlag());
=> ApplyEquip.HasFlag(slot.ToFlag());
public bool DoApplyStain(EquipSlot slot)
=> Application.Equip.HasFlag(slot.ToStainFlag());
=> ApplyEquip.HasFlag(slot.ToStainFlag());
public bool DoApplyCustomize(CustomizeIndex idx)
=> Application.Customize.HasFlag(idx.ToFlag());
public bool DoApplyCrest(CrestFlag slot)
=> Application.Crest.HasFlag(slot);
public bool DoApplyParameter(CustomizeParameterFlag flag)
=> Application.Parameters.HasFlag(flag);
public bool DoApplyBonusItem(BonusItemFlag slot)
=> Application.BonusItem.HasFlag(slot);
=> idx is not CustomizeIndex.Race and not CustomizeIndex.BodyType && ApplyCustomize.HasFlag(idx.ToFlag());
internal bool SetApplyEquip(EquipSlot slot, bool value)
{
var newValue = value ? Application.Equip | slot.ToFlag() : Application.Equip & ~slot.ToFlag();
if (newValue == Application.Equip)
var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag();
if (newValue == ApplyEquip)
return false;
Application.Equip = newValue;
return true;
}
internal bool SetApplyBonusItem(BonusItemFlag slot, bool value)
{
var newValue = value ? Application.BonusItem | slot : Application.BonusItem & ~slot;
if (newValue == Application.BonusItem)
return false;
Application.BonusItem = newValue;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyStain(EquipSlot slot, bool value)
{
var newValue = value ? Application.Equip | slot.ToStainFlag() : Application.Equip & ~slot.ToStainFlag();
if (newValue == Application.Equip)
var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag();
if (newValue == ApplyEquip)
return false;
Application.Equip = newValue;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{
var newValue = value ? Application.Customize | idx.ToFlag() : Application.Customize & ~idx.ToFlag();
if (newValue == Application.Customize)
var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag();
if (newValue == ApplyCustomize)
return false;
Application.Customize = newValue;
ApplyCustomize = newValue;
return true;
}
internal bool SetApplyCrest(CrestFlag slot, bool value)
{
var newValue = value ? Application.Crest | slot : Application.Crest & ~slot;
if (newValue == Application.Crest)
return false;
public void FixCustomizeApplication(CustomizationService service, CustomizeFlag flags)
=> FixCustomizeApplication(service.AwaitedService.GetList(DesignData.Customize.Clan, DesignData.Customize.Gender), flags);
Application.Crest = newValue;
return true;
}
public void FixCustomizeApplication(CustomizationSet set, CustomizeFlag flags)
=> ApplyCustomize = flags.FixApplication(set);
internal bool SetApplyParameter(CustomizeParameterFlag flag, bool value)
{
var newValue = value ? Application.Parameters | flag : Application.Parameters & ~flag;
if (newValue == Application.Parameters)
return false;
Application.Parameters = newValue;
return true;
}
public IEnumerable<string> FilteredItemNames
=> _designData.FilteredItemNames(Application.Equip, Application.BonusItem);
internal FlagRestrictionResetter TemporarilyRestrictApplication(ApplicationCollection restrictions)
=> new(this, restrictions);
internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags)
=> new(this, equipFlags, customizeFlags);
internal readonly struct FlagRestrictionResetter : IDisposable
{
private readonly DesignBase _design;
private readonly ApplicationCollection _oldFlags;
private readonly DesignBase _design;
private readonly EquipFlag _oldEquipFlags;
private readonly CustomizeFlag _oldCustomizeFlags;
public FlagRestrictionResetter(DesignBase d, ApplicationCollection restrictions)
public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
_design = d;
_oldFlags = d.Application;
_design.Application = restrictions.Restrict(_oldFlags);
_design = d;
_oldEquipFlags = d.ApplyEquip;
_oldCustomizeFlags = d.ApplyCustomize;
d.ApplyEquip &= equipFlags;
d.ApplyCustomize &= customizeFlags;
}
public void Dispose()
=> _design.Application = _oldFlags;
{
_design.ApplyEquip = _oldEquipFlags;
_design.ApplyCustomize = _oldCustomizeFlags;
}
}
private CustomizeSet SetCustomizationSet(CustomizeService customize)
=> !_designData.IsHuman
? customize.Manager.GetSet(SubRace.Midlander, Gender.Male)
: customize.Manager.GetSet(_designData.Customize.Clan, _designData.Customize.Gender);
#endregion
#region Serialization
@ -233,62 +192,39 @@ public class DesignBase
{
["FileVersion"] = FileVersion,
["Equipment"] = SerializeEquipment(),
["Bonus"] = SerializeBonusItems(),
["Customize"] = SerializeCustomize(),
["Parameters"] = SerializeParameters(),
["Materials"] = SerializeMaterials(),
};
return ret;
}
protected JObject SerializeEquipment()
{
static JObject Serialize(CustomItemId id, StainId stain, bool apply, bool applyStain)
=> new()
{
["ItemId"] = id.Id,
["Stain"] = stain.Id,
["Apply"] = apply,
["ApplyStain"] = applyStain,
};
var ret = new JObject();
if (_designData.IsHuman)
if (DesignData.IsHuman)
{
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
var item = _designData.Item(slot);
var stains = _designData.Stain(slot);
var crestSlot = slot.ToCrestFlag();
var crest = _designData.Crest(crestSlot);
ret[slot.ToString()] = Serialize(item.Id, stains, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot));
var item = DesignData.Item(slot);
var stain = DesignData.Stain(slot);
ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot));
}
ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply");
ret["VieraEars"] = new QuadBool(_designData.AreEarsVisible(), DoApplyMeta(MetaIndex.EarState)).ToJObject("Show", "Apply");
ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply");
ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).ToJObject("Show", "Apply");
ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply");
ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply");
ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply");
}
else
{
ret["Array"] = _designData.WriteEquipmentBytesBase64();
}
return ret;
static JObject Serialize(CustomItemId id, StainIds stains, bool crest, bool apply, bool applyStain, bool applyCrest)
=> stains.AddToObject(new JObject
{
["ItemId"] = id.Id,
["Crest"] = crest,
["Apply"] = apply,
["ApplyStain"] = applyStain,
["ApplyCrest"] = applyCrest,
});
}
protected JObject SerializeBonusItems()
{
var ret = new JObject();
foreach (var slot in BonusExtensions.AllFlags)
{
var item = _designData.BonusItem(slot);
ret[slot.ToString()] = new JObject()
{
["BonusId"] = item.Id.Id,
["Apply"] = DoApplyBonusItem(slot),
};
ret["Array"] = DesignData.WriteEquipmentBytesBase64();
}
return ret;
@ -298,17 +234,17 @@ public class DesignBase
{
var ret = new JObject()
{
["ModelId"] = _designData.ModelId,
["ModelId"] = DesignData.ModelId,
};
var customize = _designData.Customize;
if (_designData.IsHuman)
var customize = DesignData.Customize;
if (DesignData.IsHuman)
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
ret[idx.ToString()] = new JObject()
{
["Value"] = customize[idx].Value,
["Apply"] = Application.Customize.HasFlag(idx.ToFlag()),
["Apply"] = DoApplyCustomize(idx),
};
}
else
@ -316,105 +252,18 @@ public class DesignBase
ret["Wetness"] = new JObject()
{
["Value"] = _designData.IsWet(),
["Apply"] = DoApplyMeta(MetaIndex.Wetness),
["Value"] = DesignData.IsWet(),
["Apply"] = DoApplyWetness(),
};
return ret;
}
protected JObject SerializeParameters()
{
var ret = new JObject();
foreach (var flag in CustomizeParameterExtensions.ValueFlags)
{
ret[flag.ToString()] = new JObject()
{
["Value"] = DesignData.Parameters[flag][0],
["Apply"] = DoApplyParameter(flag),
};
}
foreach (var flag in CustomizeParameterExtensions.PercentageFlags)
{
ret[flag.ToString()] = new JObject()
{
["Percentage"] = DesignData.Parameters[flag][0],
["Apply"] = DoApplyParameter(flag),
};
}
foreach (var flag in CustomizeParameterExtensions.RgbFlags)
{
ret[flag.ToString()] = new JObject()
{
["Red"] = DesignData.Parameters[flag][0],
["Green"] = DesignData.Parameters[flag][1],
["Blue"] = DesignData.Parameters[flag][2],
["Apply"] = DoApplyParameter(flag),
};
}
foreach (var flag in CustomizeParameterExtensions.RgbaFlags)
{
ret[flag.ToString()] = new JObject()
{
["Red"] = DesignData.Parameters[flag][0],
["Green"] = DesignData.Parameters[flag][1],
["Blue"] = DesignData.Parameters[flag][2],
["Alpha"] = DesignData.Parameters[flag][3],
["Apply"] = DoApplyParameter(flag),
};
}
return ret;
}
protected JObject SerializeMaterials()
{
var ret = new JObject();
foreach (var (key, value) in Materials)
ret[key.ToString("X16")] = JToken.FromObject(value);
return ret;
}
protected static void LoadMaterials(JToken? materials, DesignBase design, string name)
{
if (materials is not JObject obj)
return;
design.GetMaterialDataRef().Clear();
foreach (var (key, value) in obj.Properties().Zip(obj.PropertyValues()))
{
try
{
var k = uint.Parse(key.Name, NumberStyles.HexNumber);
var v = value.ToObject<MaterialValueDesign>();
if (!MaterialValueIndex.FromKey(k, out _))
{
Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.",
NotificationType.Warning);
continue;
}
if (!design.GetMaterialDataRef().TryAddValue(MaterialValueIndex.FromKey(k), v))
Glamourer.Messager.NotificationMessage($"Duplicate material value key {k} for design {name}, skipped.",
NotificationType.Warning);
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, $"Error parsing material value for design {name}, skipped",
NotificationType.Warning);
}
}
}
#endregion
#region Deserialization
public static DesignBase LoadDesignBase(CustomizeService customizations, ItemManager items, JObject json)
public static DesignBase LoadDesignBase(CustomizationService customizations, ItemManager items, JObject json)
{
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch
@ -424,219 +273,99 @@ public class DesignBase
};
}
private static DesignBase LoadDesignV1Base(CustomizeService customizations, ItemManager items, JObject json)
private static DesignBase LoadDesignV1Base(CustomizationService customizations, ItemManager items, JObject json)
{
var ret = new DesignBase(customizations, items);
var ret = new DesignBase(items);
LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true);
LoadEquip(items, json["Equipment"], ret, "Temporary Design", true);
LoadParameters(json["Parameters"], ret, "Temporary Design");
LoadMaterials(json["Materials"], ret, "Temporary Design");
LoadBonus(items, ret, json["Bonus"]);
return ret;
}
protected static void LoadBonus(ItemManager items, DesignBase design, JToken? json)
{
if (json is not JObject)
{
design.Application.BonusItem = 0;
return;
}
foreach (var slot in BonusExtensions.AllFlags)
{
if (json[slot.ToString()] is not JObject itemJson)
{
design.Application.BonusItem &= ~slot;
design.GetDesignDataRef().SetBonusItem(slot, EquipItem.BonusItemNothing(slot));
continue;
}
design.SetApplyBonusItem(slot, itemJson["Apply"]?.ToObject<bool>() ?? false);
var id = itemJson["BonusId"]?.ToObject<ulong>() ?? 0;
var item = items.Resolve(slot, id);
design.GetDesignDataRef().SetBonusItem(slot, item);
}
}
protected static void LoadParameters(JToken? parameters, DesignBase design, string name)
{
if (parameters == null)
{
design.Application.Parameters = 0;
design.GetDesignDataRef().Parameters = default;
return;
}
foreach (var flag in CustomizeParameterExtensions.ValueFlags)
{
if (!TryGetToken(flag, out var token))
continue;
var value = token["Value"]?.ToObject<float>() ?? 0f;
design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(value);
}
foreach (var flag in CustomizeParameterExtensions.PercentageFlags)
{
if (!TryGetToken(flag, out var token))
continue;
var value = token["Percentage"]?.ToObject<float>() ?? 0f;
design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(value);
}
foreach (var flag in CustomizeParameterExtensions.RgbFlags)
{
if (!TryGetToken(flag, out var token))
continue;
var r = token["Red"]?.ToObject<float>() ?? 0f;
var g = token["Green"]?.ToObject<float>() ?? 0f;
var b = token["Blue"]?.ToObject<float>() ?? 0f;
design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(r, g, b);
}
foreach (var flag in CustomizeParameterExtensions.RgbaFlags)
{
if (!TryGetToken(flag, out var token))
continue;
var r = token["Red"]?.ToObject<float>() ?? 0f;
var g = token["Green"]?.ToObject<float>() ?? 0f;
var b = token["Blue"]?.ToObject<float>() ?? 0f;
var a = token["Alpha"]?.ToObject<float>() ?? 0f;
design.GetDesignDataRef().Parameters[flag] = new CustomizeParameterValue(r, g, b, a);
}
MigrateLipOpacity();
return;
// Load the token and set application.
bool TryGetToken(CustomizeParameterFlag flag, [NotNullWhen(true)] out JToken? token)
{
token = parameters[flag.ToString()];
if (token != null)
{
var apply = token["Apply"]?.ToObject<bool>() ?? false;
design.SetApplyParameter(flag, apply);
return true;
}
design.Application.Parameters &= ~flag;
design.GetDesignDataRef().Parameters[flag] = CustomizeParameterValue.Zero;
return false;
}
void MigrateLipOpacity()
{
var token = parameters["LipOpacity"]?["Percentage"]?.ToObject<float>();
var actualToken = parameters[CustomizeParameterFlag.LipDiffuse.ToString()]?["Alpha"];
if (token != null && actualToken == null)
design.GetDesignDataRef().Parameters.LipDiffuse.W = token.Value;
}
}
protected static void LoadEquip(ItemManager items, JToken? equip, DesignBase design, string name, bool allowUnknown)
{
if (equip == null)
{
design._designData.SetDefaultEquipment(items);
design.DesignData.SetDefaultEquipment(items);
Glamourer.Messager.NotificationMessage("The loaded design does not contain any equipment data, reset to default.",
NotificationType.Warning);
return;
}
if (!design._designData.IsHuman)
if (!design.DesignData.IsHuman)
{
var textArray = equip["Array"]?.ToObject<string>() ?? string.Empty;
design._designData.SetEquipmentBytesFromBase64(textArray);
design.DesignData.SetEquipmentBytesFromBase64(textArray);
return;
}
foreach (var slot in EquipSlotExtensions.EqdpSlots)
static (CustomItemId, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item)
{
var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]);
PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown));
PrintWarning(items.ValidateStain(stains, out stains, allowUnknown));
var crestSlot = slot.ToCrestFlag();
design._designData.SetItem(slot, item);
design._designData.SetStain(slot, stains);
design._designData.SetCrest(crestSlot, crest);
design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain);
design.SetApplyCrest(crestSlot, applyCrest);
var id = item?["ItemId"]?.ToObject<ulong>() ?? ItemManager.NothingId(slot).Id;
var stain = (StainId)(item?["Stain"]?.ToObject<byte>() ?? 0);
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
return (id, stain, apply, applyStain);
}
{
var (id, stains, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.MainHand))
id = items.DefaultSword.ItemId;
var (idOff, stainsOff, crestOff, applyOff, applyStainOff, applyCrestOff) =
ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield);
PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown));
PrintWarning(items.ValidateStain(stains, out stains, allowUnknown));
PrintWarning(items.ValidateStain(stainsOff, out stainsOff, allowUnknown));
design._designData.SetItem(EquipSlot.MainHand, main);
design._designData.SetItem(EquipSlot.OffHand, off);
design._designData.SetStain(EquipSlot.MainHand, stains);
design._designData.SetStain(EquipSlot.OffHand, stainsOff);
design._designData.SetCrest(CrestFlag.MainHand, crest);
design._designData.SetCrest(CrestFlag.OffHand, crestOff);
design.SetApplyEquip(EquipSlot.MainHand, apply);
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
design.SetApplyCrest(CrestFlag.MainHand, applyCrest);
design.SetApplyCrest(CrestFlag.OffHand, applyCrestOff);
}
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyMeta(MetaIndex.HatState, metaValue.Enabled);
design._designData.SetHatVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyMeta(MetaIndex.WeaponState, metaValue.Enabled);
design._designData.SetWeaponVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
design.SetApplyMeta(MetaIndex.VisorState, metaValue.Enabled);
design._designData.SetVisor(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["VieraEars"], "Show", "Apply", QuadBool.NullTrue);
design.SetApplyMeta(MetaIndex.EarState, metaValue.Enabled);
design._designData.SetEarsVisible(metaValue.ForcedValue);
return;
void PrintWarning(string msg)
{
if (msg.Length > 0 && name != "Temporary Design")
Glamourer.Messager.NotificationMessage($"{msg} ({name})", NotificationType.Warning);
}
static (CustomItemId, StainIds, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item)
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var id = item?["ItemId"]?.ToObject<ulong>() ?? ItemManager.NothingId(slot).Id;
var stains = StainIds.ParseFromObject(item as JObject);
var crest = item?["Crest"]?.ToObject<bool>() ?? false;
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
var applyCrest = item?["ApplyCrest"]?.ToObject<bool>() ?? false;
return (id, stains, crest, apply, applyStain, applyCrest);
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown));
PrintWarning(items.ValidateStain(stain, out stain, allowUnknown));
design.DesignData.SetItem(slot, item);
design.DesignData.SetStain(slot, stain);
design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain);
}
{
var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.MainHand))
id = items.DefaultSword.ItemId;
var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield);
PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off, allowUnknown));
PrintWarning(items.ValidateStain(stain, out stain, allowUnknown));
PrintWarning(items.ValidateStain(stainOff, out stainOff, allowUnknown));
design.DesignData.SetItem(EquipSlot.MainHand, main);
design.DesignData.SetItem(EquipSlot.OffHand, off);
design.DesignData.SetStain(EquipSlot.MainHand, stain);
design.DesignData.SetStain(EquipSlot.OffHand, stainOff);
design.SetApplyEquip(EquipSlot.MainHand, apply);
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
}
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyHatVisible(metaValue.Enabled);
design.DesignData.SetHatVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyWeaponVisible(metaValue.Enabled);
design.DesignData.SetWeaponVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
design.SetApplyVisorToggle(metaValue.Enabled);
design.DesignData.SetVisor(metaValue.ForcedValue);
}
protected static void LoadCustomize(CustomizeService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman,
protected static void LoadCustomize(CustomizationService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman,
bool allowUnknown)
{
if (json == null)
{
design._designData.ModelId = 0;
design._designData.IsHuman = true;
design.SetCustomize(customizations, CustomizeArray.Default);
design.DesignData.ModelId = 0;
design.DesignData.IsHuman = true;
design.DesignData.Customize = Customize.Default;
Glamourer.Messager.NotificationMessage("The loaded design does not contain any customization data, reset to default.",
NotificationType.Warning);
return;
@ -651,23 +380,21 @@ public class DesignBase
}
var wetness = QuadBool.FromJObject(json["Wetness"], "Value", "Apply", QuadBool.NullFalse);
design._designData.SetIsWet(wetness.ForcedValue);
design.SetApplyMeta(MetaIndex.Wetness, wetness.Enabled);
design.DesignData.SetIsWet(wetness.ForcedValue);
design.SetApplyWetness(wetness.Enabled);
design._designData.ModelId = json["ModelId"]?.ToObject<uint>() ?? 0;
PrintWarning(customizations.ValidateModelId(design._designData.ModelId, out design._designData.ModelId,
out design._designData.IsHuman));
if (design._designData.ModelId != 0 && forbidNonHuman)
design.DesignData.ModelId = json["ModelId"]?.ToObject<uint>() ?? 0;
PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId, out design.DesignData.IsHuman));
if (design.DesignData.ModelId != 0 && forbidNonHuman)
{
PrintWarning("Model IDs different from 0 are not currently allowed, reset model id to 0.");
design._designData.ModelId = 0;
design._designData.IsHuman = true;
design.DesignData.ModelId = 0;
design.DesignData.IsHuman = true;
}
else if (!design._designData.IsHuman)
else if (!design.DesignData.IsHuman)
{
var arrayText = json["Array"]?.ToObject<string>() ?? string.Empty;
design._designData.Customize.LoadBase64(arrayText);
design.CustomizeSet = design.SetCustomizationSet(customizations);
design.DesignData.Customize.LoadBase64(arrayText);
return;
}
@ -676,45 +403,51 @@ public class DesignBase
PrintWarning(customizations.ValidateClan(clan, race, out race, out clan));
var gender = (Gender)((json[CustomizeIndex.Gender.ToString()]?["Value"]?.ToObject<byte>() ?? 0) + 1);
PrintWarning(customizations.ValidateGender(race, gender, out gender));
var bodyType = (CustomizeValue)(json[CustomizeIndex.BodyType.ToString()]?["Value"]?.ToObject<byte>() ?? 1);
design._designData.Customize.Race = race;
design._designData.Customize.Clan = clan;
design._designData.Customize.Gender = gender;
design._designData.Customize.BodyType = bodyType;
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);
design.SetApplyCustomize(CustomizeIndex.BodyType, bodyType != 0);
var set = design.CustomizeSet;
design.DesignData.Customize.Race = race;
design.DesignData.Customize.Clan = clan;
design.DesignData.Customize.Gender = gender;
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 = customizations.AwaitedService.GetList(clan, gender);
foreach (var idx in CustomizationExtensions.AllBasic)
{
var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
if (set.IsAvailable(idx) && design._designData.Customize.BodyType == 1)
PrintWarning(CustomizeService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data,
if (set.IsAvailable(idx))
{
var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
PrintWarning(CustomizationService.ValidateCustomizeValue(set, design.DesignData.Customize.Face, idx, data, out data,
allowUnknown));
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
design._designData.Customize[idx] = data;
design.SetApplyCustomize(idx, apply);
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
design.DesignData.Customize[idx] = data;
design.SetApplyCustomize(idx, apply);
}
else
{
design.DesignData.Customize[idx] = CustomizeValue.Zero;
design.SetApplyCustomize(idx, false);
}
}
design.FixCustomizeApplication(set, design.ApplyCustomize);
}
public void MigrateBase64(CustomizeService customize, ItemManager items, HumanModelList humans, string base64)
public void MigrateBase64(ItemManager items, HumanModelList humans, string base64)
{
try
{
_designData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags,
out var writeProtected, out var applyMeta);
Application.Equip = equipFlags;
ApplyCustomize = customizeFlags;
Application.Parameters = 0;
Application.Crest = 0;
Application.Meta = applyMeta;
Application.BonusItem = 0;
DesignData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags,
out var writeProtected,
out var applyHat, out var applyVisor, out var applyWeapon);
ApplyEquip = equipFlags;
ApplyCustomize = customizeFlags;
SetWriteProtected(writeProtected);
CustomizeSet = SetCustomizationSet(customize);
SetApplyHatVisible(applyHat);
SetApplyVisorToggle(applyVisor);
SetApplyWeaponVisible(applyWeapon);
SetApplyWetness(true);
}
catch (Exception ex)
{
@ -722,5 +455,15 @@ public class DesignBase
}
}
public void RemoveInvalidCustomize(CustomizationService customizations)
{
var set = customizations.AwaitedService.GetList(DesignData.Customize.Clan, DesignData.Customize.Gender);
foreach (var idx in CustomizationExtensions.AllBasic.Where(i => !set.IsAvailable(i)))
{
DesignData.Customize[idx] = CustomizeValue.Zero;
SetApplyCustomize(idx, false);
}
}
#endregion
}

View file

@ -1,8 +1,9 @@
using Glamourer.Api.Enums;
using System;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using OtterGui;
using OtterGui.Extensions;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -15,7 +16,7 @@ public class DesignBase64Migration
public const int Base64SizeV4 = 95;
public static unsafe DesignData MigrateBase64(ItemManager items, HumanModelList humans, string base64, out EquipFlag equipFlags,
out CustomizeFlag customizeFlags, out bool writeProtected, out MetaFlag metaFlags)
out CustomizeFlag customizeFlags, out bool writeProtected, out bool applyHat, out bool applyVisor, out bool applyWeapon)
{
static void CheckSize(int length, int requiredLength)
{
@ -27,7 +28,9 @@ public class DesignBase64Migration
byte applicationFlags;
ushort equipFlagsS;
var bytes = Convert.FromBase64String(base64);
metaFlags = MetaFlag.Wetness;
applyHat = false;
applyVisor = false;
applyWeapon = false;
var data = new DesignData();
switch (bytes[0])
{
@ -59,7 +62,7 @@ public class DesignBase64Migration
data.SetHatVisible((bytes[90] & 0x01) == 0);
data.SetVisor((bytes[90] & 0x10) != 0);
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
break;
}
case 5:
@ -70,19 +73,16 @@ public class DesignBase64Migration
data.SetHatVisible((bytes[90] & 0x01) == 0);
data.SetVisor((bytes[90] & 0x10) != 0);
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
break;
default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}.");
}
customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0;
data.SetIsWet((applicationFlags & 0x02) != 0);
if ((applicationFlags & 0x04) != 0)
metaFlags |= MetaFlag.HatState;
if ((applicationFlags & 0x08) != 0)
metaFlags |= MetaFlag.WeaponState;
if ((applicationFlags & 0x10) != 0)
metaFlags |= MetaFlag.VisorState;
applyHat = (applicationFlags & 0x04) != 0;
applyWeapon = (applicationFlags & 0x08) != 0;
applyVisor = (applicationFlags & 0x10) != 0;
writeProtected = (applicationFlags & 0x20) != 0;
equipFlags = 0;
@ -97,16 +97,16 @@ public class DesignBase64Migration
fixed (byte* ptr = bytes)
{
var cur = (LegacyCharacterWeapon*)(ptr + 30);
var eq = (LegacyCharacterArmor*)(cur + 2);
var cur = (CharacterWeapon*)(ptr + 30);
var eq = (CharacterArmor*)(cur + 2);
if (!humans.IsHuman(data.ModelId))
{
data.LoadNonHuman(data.ModelId, *(CustomizeArray*)(ptr + 4), (nint)eq);
data.LoadNonHuman(data.ModelId, *(Customize*)(ptr + 4), (nint)eq);
return data;
}
data.Customize = *(CustomizeArray*)(ptr + 4);
data.Customize.Load(*(Customize*)(ptr + 4));
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{
var mdl = eq[idx];
@ -121,9 +121,9 @@ public class DesignBase64Migration
data.SetStain(slot, mdl.Stain);
}
var main = cur[0].Skeleton.Id == 0
var main = cur[0].Set.Id == 0
? items.DefaultSword
: items.Identify(EquipSlot.MainHand, cur[0].Skeleton, cur[0].Weapon, cur[0].Variant);
: items.Identify(EquipSlot.MainHand, cur[0].Set, cur[0].Type, cur[0].Variant);
if (!main.Valid)
{
Glamourer.Log.Warning("Base64 string invalid, weapon could not be identified.");
@ -135,10 +135,10 @@ public class DesignBase64Migration
EquipItem off;
// Fist weapon hack
if (main.PrimaryId.Id is > 1600 and < 1651 && cur[1].Variant == 0)
if (main.ModelId.Id is > 1600 and < 1651 && cur[1].Variant == 0)
{
off = items.Identify(EquipSlot.OffHand, (PrimaryId)(main.PrimaryId.Id + 50), main.SecondaryId, main.Variant, main.Type);
var gauntlet = items.Identify(EquipSlot.Hands, cur[1].Skeleton, (Variant)cur[1].Weapon.Id);
off = items.Identify(EquipSlot.OffHand, (SetId)(main.ModelId.Id + 50), main.WeaponType, main.Variant, main.Type);
var gauntlet = items.Identify(EquipSlot.Hands, cur[1].Set, (Variant)cur[1].Type.Id);
if (gauntlet.Valid)
{
data.SetItem(EquipSlot.Hands, gauntlet);
@ -147,9 +147,9 @@ public class DesignBase64Migration
}
else
{
off = cur[0].Skeleton.Id == 0
off = cur[0].Set.Id == 0
? ItemManager.NothingItem(FullEquipType.Shield)
: items.Identify(EquipSlot.OffHand, cur[1].Skeleton, cur[1].Weapon, cur[1].Variant, main.Type);
: items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, cur[1].Variant, main.Type);
}
if (main.Type.ValidOffhand() != FullEquipType.Unknown && !off.Valid)
@ -164,16 +164,16 @@ public class DesignBase64Migration
}
}
public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, MetaFlag meta,
bool writeProtected, float alpha = 1.0f)
public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags,
bool setHat, bool setVisor, bool setWeapon, bool writeProtected, float alpha = 1.0f)
{
var data = stackalloc byte[Base64SizeV4];
data[0] = 5;
data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0)
| (save.IsWet() ? 0x02 : 0)
| (meta.HasFlag(MetaFlag.HatState) ? 0x04 : 0)
| (meta.HasFlag(MetaFlag.WeaponState) ? 0x08 : 0)
| (meta.HasFlag(MetaFlag.VisorState) ? 0x10 : 0)
| (setHat ? 0x04 : 0)
| (setWeapon ? 0x08 : 0)
| (setVisor ? 0x10 : 0)
| (writeProtected ? 0x20 : 0));
data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0)
| (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0)
@ -187,13 +187,11 @@ public class DesignBase64Migration
| (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0)
| (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0)
| (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0));
save.Customize.Write(data + 4);
((LegacyCharacterWeapon*)(data + 30))[0] =
new LegacyCharacterWeapon(save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand)));
((LegacyCharacterWeapon*)(data + 30))[1] =
new LegacyCharacterWeapon(save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand)));
save.Customize.Write((nint)data + 4);
((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand));
((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand));
foreach (var slot in EquipSlotExtensions.EqdpSlots)
((LegacyCharacterArmor*)(data + 44))[slot.ToIndex()] = new LegacyCharacterArmor(save.Item(slot).Armor(save.Stain(slot)));
((CharacterArmor*)(data + 44))[slot.ToIndex()] = save.Item(slot).Armor(save.Stain(slot));
*(ushort*)(data + 84) = 1; // IsSet.
*(float*)(data + 86) = 1f;
data[90] = (byte)((save.IsHatVisible() ? 0x00 : 0x01)

View file

@ -1,292 +0,0 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility.Raii;
using Glamourer.Gui;
using Glamourer.Services;
using Dalamud.Bindings.ImGui;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
namespace Glamourer.Designs;
public class DesignColorUi(DesignColors colors, Configuration config)
{
private string _newName = string.Empty;
public void Draw()
{
using var table = ImRaii.Table("designColors", 3, ImGuiTableFlags.RowBg);
if (!table)
return;
var changeString = string.Empty;
uint? changeValue = null;
var buttonSize = new Vector2(ImGui.GetFrameHeight());
ImGui.TableSetupColumn("##Delete", ImGuiTableColumnFlags.WidthFixed, buttonSize.X);
ImGui.TableSetupColumn("##Select", ImGuiTableColumnFlags.WidthFixed, buttonSize.X);
ImGui.TableSetupColumn("Color Name", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableHeadersRow();
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), buttonSize,
"Revert the color used for missing design colors to its default.", colors.MissingColor == DesignColors.MissingColorDefault,
true))
{
changeString = DesignColors.MissingColorName;
changeValue = DesignColors.MissingColorDefault;
}
ImGui.TableNextColumn();
if (DrawColorButton(DesignColors.MissingColorName, colors.MissingColor, out var newColor))
{
changeString = DesignColors.MissingColorName;
changeValue = newColor;
}
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(DesignColors.MissingColorName);
ImGuiUtil.HoverTooltip("This color is used when the color specified in a design is not available.");
var disabled = !config.DeleteDesignModifier.IsActive();
var tt = "Delete this color. This does not remove it from designs using it.";
if (disabled)
tt += $"\nHold {config.DeleteDesignModifier} to delete.";
foreach (var ((name, color), idx) in colors.WithIndex())
{
using var id = ImRaii.PushId(idx);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, tt, disabled, true))
{
changeString = name;
changeValue = null;
}
ImGui.TableNextColumn();
if (DrawColorButton(name, color, out newColor))
{
changeString = name;
changeValue = newColor;
}
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(name);
}
ImGui.TableNextColumn();
(tt, disabled) = _newName.Length == 0
? ("Specify a name for a new color first.", true)
: _newName is DesignColors.MissingColorName or DesignColors.AutomaticName
? ($"You can not use the name {DesignColors.MissingColorName} or {DesignColors.AutomaticName}, choose a different one.", true)
: colors.ContainsKey(_newName)
? ($"The color {_newName} already exists, please choose a different name.", true)
: ($"Add a new color {_newName} to your list.", false);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), buttonSize, tt, disabled, true))
{
changeString = _newName;
changeValue = 0xFFFFFFFF;
}
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputTextWithHint("##newDesignColor", "New Color Name...", ref _newName, 64, ImGuiInputTextFlags.EnterReturnsTrue))
{
changeString = _newName;
changeValue = 0xFFFFFFFF;
}
if (changeString.Length > 0)
{
if (!changeValue.HasValue)
colors.DeleteColor(changeString);
else
colors.SetColor(changeString, changeValue.Value);
}
}
public static bool DrawColorButton(string tooltip, uint color, out uint newColor)
{
var vec = ImGui.ColorConvertU32ToFloat4(color);
if (!ImGui.ColorEdit4(tooltip, ref vec, ImGuiColorEditFlags.AlphaPreviewHalf | ImGuiColorEditFlags.NoInputs))
{
ImGuiUtil.HoverTooltip(tooltip);
newColor = color;
return false;
}
ImGuiUtil.HoverTooltip(tooltip);
newColor = ImGui.ColorConvertFloat4ToU32(vec);
return newColor != color;
}
}
public class DesignColors : ISavable, IReadOnlyDictionary<string, uint>
{
public const string AutomaticName = "Automatic";
public const string MissingColorName = "Missing Color";
public const uint MissingColorDefault = 0xFF0000D0;
private readonly SaveService _saveService;
private readonly Dictionary<string, uint> _colors = [];
public uint MissingColor { get; private set; } = MissingColorDefault;
public event Action? ColorChanged;
public DesignColors(SaveService saveService)
{
_saveService = saveService;
Load();
}
public uint GetColor(Design? design)
{
if (design == null)
return ColorId.NormalDesign.Value();
if (design.Color.Length == 0)
return AutoColor(design);
return TryGetValue(design.Color, out var color) ? color : MissingColor;
}
public void SetColor(string key, uint newColor)
{
if (key.Length == 0)
return;
if (key is MissingColorName && MissingColor != newColor)
{
MissingColor = newColor;
SaveAndInvoke();
return;
}
if (_colors.TryAdd(key, newColor))
{
SaveAndInvoke();
return;
}
_colors.TryGetValue(key, out var color);
_colors[key] = newColor;
if (color != newColor)
SaveAndInvoke();
}
private void SaveAndInvoke()
{
ColorChanged?.Invoke();
_saveService.DelaySave(this, TimeSpan.FromSeconds(2));
}
public void DeleteColor(string key)
{
if (_colors.Remove(key))
SaveAndInvoke();
}
public string ToFilename(FilenameService fileNames)
=> fileNames.DesignColorFile;
public void Save(StreamWriter writer)
{
var jObj = new JObject
{
["Version"] = 1,
["MissingColor"] = MissingColor,
["Definitions"] = JToken.FromObject(_colors),
};
writer.Write(jObj.ToString(Formatting.Indented));
}
private void Load()
{
_colors.Clear();
var file = _saveService.FileNames.DesignColorFile;
if (!File.Exists(file))
return;
try
{
var text = File.ReadAllText(file);
var jObj = JObject.Parse(text);
var version = jObj["Version"]?.ToObject<int>() ?? 0;
switch (version)
{
case 1:
{
var dict = jObj["Definitions"]?.ToObject<Dictionary<string, uint>>() ?? new Dictionary<string, uint>();
_colors.EnsureCapacity(dict.Count);
foreach (var kvp in dict)
_colors.Add(kvp.Key, kvp.Value);
MissingColor = jObj["MissingColor"]?.ToObject<uint>() ?? MissingColorDefault;
break;
}
default: throw new Exception($"Unknown Version {version}");
}
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, "Could not read design color file.", NotificationType.Error);
}
}
public IEnumerator<KeyValuePair<string, uint>> GetEnumerator()
=> _colors.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _colors.Count;
public bool ContainsKey(string key)
=> _colors.ContainsKey(key);
public bool TryGetValue(string key, out uint value)
{
if (_colors.TryGetValue(key, out value))
{
if (value == 0)
value = ImGui.GetColorU32(ImGuiCol.Text);
return true;
}
return false;
}
public static uint AutoColor(DesignBase design)
{
var customize = design.ApplyCustomizeExcludingBodyType == 0;
var equip = design.Application.Equip == 0;
return (customize, equip) switch
{
(true, true) => ColorId.StateDesign.Value(),
(true, false) => ColorId.EquipmentDesign.Value(),
(false, true) => ColorId.CustomizationDesign.Value(),
(false, false) => ColorId.NormalDesign.Value(),
};
}
public uint this[string key]
=> _colors[key];
public IEnumerable<string> Keys
=> _colors.Keys;
public IEnumerable<uint> Values
=> _colors.Values;
}

View file

@ -1,26 +1,34 @@
using Glamourer.Designs.Links;
using Glamourer.Interop.Material;
using System;
using System.Diagnostics;
using System.Text;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignConverter(
SaveService saveService,
ItemManager _items,
DesignManager _designs,
CustomizeService _customize,
HumanModelList _humans,
DesignLinkLoader _linkLoader)
public class DesignConverter
{
public const byte Version = 6;
public const byte Version = 5;
private readonly ItemManager _items;
private readonly DesignManager _designs;
private readonly CustomizationService _customize;
private readonly HumanModelList _humans;
public DesignConverter(ItemManager items, DesignManager designs, CustomizationService customize, HumanModelList humans)
{
_items = items;
_designs = designs;
_customize = customize;
_humans = humans;
}
public JObject ShareJObject(DesignBase design)
=> design.JsonSerialize();
@ -28,66 +36,40 @@ public class DesignConverter(
public JObject ShareJObject(Design design)
=> design.JsonSerialize();
public JObject ShareJObject(ActorState state, in ApplicationRules rules)
public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
var design = Convert(state, rules);
var design = Convert(state, equipFlags, customizeFlags);
return ShareJObject(design);
}
public string ShareBase64(Design design)
=> ToBase64(ShareJObject(design));
=> ShareBackwardCompatible(ShareJObject(design), design);
public string ShareBase64(DesignBase design)
=> ToBase64(ShareJObject(design));
=> ShareBackwardCompatible(ShareJObject(design), design);
public string ShareBase64(ActorState state, in ApplicationRules rules)
=> ShareBase64(state.ModelData, state.Materials, rules);
public string ShareBase64(ActorState state)
=> ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All);
public string ShareBase64(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
var design = Convert(data, materials, rules);
return ToBase64(ShareJObject(design));
var design = Convert(state, equipFlags, customizeFlags);
return ShareBackwardCompatible(ShareJObject(design), design);
}
public DesignBase Convert(ActorState state, in ApplicationRules rules)
=> Convert(state.ModelData, state.Materials, rules);
public DesignBase Convert(in DesignData data, in StateMaterialManager materials, in ApplicationRules rules)
public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
var design = _designs.CreateTemporary();
rules.Apply(design);
design.SetDesignData(_customize, data);
if (rules.Materials)
ComputeMaterials(design.GetMaterialDataRef(), materials, rules.Equip);
design.ApplyEquip = equipFlags & EquipFlagExtensions.All;
design.SetApplyHatVisible(design.DoApplyEquip(EquipSlot.Head));
design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head));
design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand));
design.SetApplyWetness(true);
design.DesignData = state.ModelData;
design.FixCustomizeApplication(_customize, customizeFlags);
return design;
}
public DesignBase? FromJObject(JObject? jObject, bool customize, bool equip)
{
if (jObject == null)
return null;
try
{
var ret = jObject["Identifier"] != null
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObject)
: DesignBase.LoadDesignBase(_customize, _items, jObject);
if (!customize)
ret.Application.RemoveCustomize();
if (!equip)
ret.Application.RemoveEquip();
return ret;
}
catch (Exception ex)
{
Glamourer.Log.Warning($"Failure to parse JObject to design:\n{ex}");
return null;
}
}
public DesignBase? FromBase64(string base64, bool customize, bool equip, out byte version)
{
DesignBase ret;
@ -101,14 +83,14 @@ public class DesignConverter(
case (byte)'{':
var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes));
ret = jObj1["Identifier"] != null
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj1)
? Design.LoadDesign(_customize, _items, jObj1)
: DesignBase.LoadDesignBase(_customize, _items, jObj1);
break;
case 1:
case 2:
case 4:
ret = _designs.CreateTemporary();
ret.MigrateBase64(_customize, _items, _humans, base64);
ret.MigrateBase64(_items, _humans, base64);
break;
case 3:
{
@ -116,32 +98,21 @@ public class DesignConverter(
var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 3);
ret = jObj2["Identifier"] != null
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
? Design.LoadDesign(_customize, _items, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
break;
}
case 5:
case Version:
{
bytes = bytes[DesignBase64Migration.Base64SizeV4..];
version = bytes.DecompressToString(out var decompressed);
var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 5);
Debug.Assert(version == Version);
ret = jObj2["Identifier"] != null
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
? Design.LoadDesign(_customize, _items, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
break;
}
case 6:
{
version = bytes.DecompressToString(out var decompressed);
var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == 6);
ret = jObj2["Identifier"] != null
? Design.LoadDesign(saveService, _customize, _items, _linkLoader, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
break;
}
default: throw new Exception($"Unknown Version {bytes[0]}.");
}
}
@ -152,103 +123,43 @@ public class DesignConverter(
}
if (!customize)
ret.Application.RemoveCustomize();
{
ret.ApplyCustomize = 0;
ret.SetApplyWetness(false);
}
else
{
ret.FixCustomizeApplication(_customize, ret.ApplyCustomize);
}
if (!equip)
ret.Application.RemoveEquip();
{
ret.ApplyEquip = 0;
ret.SetApplyHatVisible(false);
ret.SetApplyWeaponVisible(false);
ret.SetApplyVisorToggle(false);
}
return ret;
}
public static string ToBase64(JToken jObject)
private static string ShareBase64(JObject jObj)
{
var json = jObject.ToString(Formatting.None);
var json = jObj.ToString(Formatting.None);
var compressed = json.Compress(Version);
return System.Convert.ToBase64String(compressed);
}
public IEnumerable<(EquipSlot Slot, EquipItem Item, StainIds Stains)> FromDrawData(IReadOnlyList<CharacterArmor> armors,
CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings)
private static string ShareBackwardCompatible(JObject jObject, DesignBase design)
{
if (armors.Count != 10)
throw new ArgumentException("Invalid length of armor array.");
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var index = (int)slot.ToIndex();
var armor = armors[index];
var item = _items.Identify(slot, armor.Set, armor.Variant);
if (!item.Valid)
{
if (!skipWarnings)
Glamourer.Log.Warning($"Appearance data {armor} for slot {slot} invalid, item could not be identified.");
item = ItemManager.NothingItem(slot);
}
yield return (slot, item, armor.Stains);
}
var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant);
if (!skipWarnings && !mh.Valid)
{
Glamourer.Log.Warning($"Appearance data {mainhand} for mainhand weapon invalid, item could not be identified.");
mh = _items.DefaultSword;
}
yield return (EquipSlot.MainHand, mh, mainhand.Stains);
var oh = _items.Identify(EquipSlot.OffHand, offhand.Skeleton, offhand.Weapon, offhand.Variant, mh.Type);
if (!skipWarnings && !oh.Valid)
{
Glamourer.Log.Warning($"Appearance data {offhand} for offhand weapon invalid, item could not be identified.");
oh = _items.GetDefaultOffhand(mh);
if (!oh.Valid)
oh = ItemManager.NothingItem(FullEquipType.Shield);
}
yield return (EquipSlot.OffHand, oh, offhand.Stains);
}
private static void ComputeMaterials(DesignMaterialManager manager, in StateMaterialManager materials,
EquipFlag equipFlags = EquipFlagExtensions.All, BonusItemFlag bonusFlags = BonusExtensions.All)
{
foreach (var (key, value) in materials.Values)
{
var idx = MaterialValueIndex.FromKey(key);
if (idx.RowIndex >= ColorTable.NumRows)
continue;
if (idx.MaterialIndex >= MaterialService.MaterialsPerModel)
continue;
switch (idx.DrawObject)
{
case MaterialValueIndex.DrawObjectType.Mainhand when idx.SlotIndex == 0:
if ((equipFlags & (EquipFlag.Mainhand | EquipFlag.MainhandStain)) == 0)
continue;
break;
case MaterialValueIndex.DrawObjectType.Offhand when idx.SlotIndex == 0:
if ((equipFlags & (EquipFlag.Offhand | EquipFlag.OffhandStain)) == 0)
continue;
break;
case MaterialValueIndex.DrawObjectType.Human:
if (idx.SlotIndex < 10)
{
if ((((uint)idx.SlotIndex).ToEquipSlot().ToBothFlags() & equipFlags) == 0)
continue;
}
else if (idx.SlotIndex >= 16)
{
if (((idx.SlotIndex - 16u).ToBonusSlot() & bonusFlags) == 0)
continue;
}
break;
default: continue;
}
manager.AddOrUpdateValue(idx, value.Convert());
}
var oldBase64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomize,
design.DoApplyHatVisible(), design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected(), 1f);
var oldBytes = System.Convert.FromBase64String(oldBase64);
var json = jObject.ToString(Formatting.None);
var compressed = json.Compress(Version);
var bytes = new byte[oldBytes.Length + compressed.Length];
oldBytes.CopyTo(bytes, 0);
compressed.CopyTo(bytes, oldBytes.Length);
return System.Convert.ToBase64String(bytes);
}
}

View file

@ -1,159 +1,73 @@
using Glamourer.GameData;
using System;
using System.Buffers.Text;
using System.Runtime.CompilerServices;
using Glamourer.Customization;
using Glamourer.Services;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.String.Functions;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Designs;
public unsafe struct DesignData
{
public const int NumEquipment = 10;
public const int EquipmentByteSize = NumEquipment * CharacterArmor.Size;
public const int NumBonusItems = 1;
public const int NumWeapons = 2;
private string _nameHead = string.Empty;
private string _nameBody = string.Empty;
private string _nameHands = string.Empty;
private string _nameLegs = string.Empty;
private string _nameFeet = string.Empty;
private string _nameEars = string.Empty;
private string _nameNeck = string.Empty;
private string _nameWrists = string.Empty;
private string _nameRFinger = string.Empty;
private string _nameLFinger = string.Empty;
private string _nameMainhand = string.Empty;
private string _nameOffhand = string.Empty;
private string _nameGlasses = string.Empty;
private fixed uint _itemIds[NumEquipment + NumWeapons];
private fixed uint _iconIds[NumEquipment + NumWeapons + NumBonusItems];
private fixed byte _equipmentBytes[EquipmentByteSize + NumWeapons * CharacterWeapon.Size];
private fixed ushort _bonusIds[NumBonusItems];
private fixed ushort _bonusModelIds[NumBonusItems];
private fixed byte _bonusVariants[NumBonusItems];
public CustomizeParameterData Parameters;
public CustomizeArray Customize = CustomizeArray.Default;
public uint ModelId;
public CrestFlag CrestVisibility;
private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand;
private byte _states;
public bool IsHuman = true;
private string _nameHead = string.Empty;
private string _nameBody = string.Empty;
private string _nameHands = string.Empty;
private string _nameLegs = string.Empty;
private string _nameFeet = string.Empty;
private string _nameEars = string.Empty;
private string _nameNeck = string.Empty;
private string _nameWrists = string.Empty;
private string _nameRFinger = string.Empty;
private string _nameLFinger = string.Empty;
private string _nameMainhand = string.Empty;
private string _nameOffhand = string.Empty;
private fixed uint _itemIds[12];
private fixed ushort _iconIds[12];
private fixed byte _equipmentBytes[48];
public Customize Customize = Customize.Default;
public uint ModelId;
private WeaponType _secondaryMainhand;
private WeaponType _secondaryOffhand;
private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand;
private byte _states;
public bool IsHuman = true;
public DesignData()
{ }
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public readonly bool ContainsName(LowerString name)
=> ItemNames.Any(name.IsContained);
public readonly StainIds Stain(EquipSlot slot)
public readonly StainId Stain(EquipSlot slot)
{
var index = slot.ToIndex();
return index switch
{
< 10 => new StainIds(_equipmentBytes[CharacterArmor.Size * index + 3], _equipmentBytes[CharacterArmor.Size * index + 4]),
10 => new StainIds(_equipmentBytes[EquipmentByteSize + 6], _equipmentBytes[EquipmentByteSize + 7]),
11 => new StainIds(_equipmentBytes[EquipmentByteSize + 14], _equipmentBytes[EquipmentByteSize + 15]),
_ => StainIds.None,
};
return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3];
}
public readonly bool Crest(CrestFlag slot)
=> CrestVisibility.HasFlag(slot);
public readonly IEnumerable<string> ItemNames
{
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
get
{
yield return _nameHead;
yield return _nameBody;
yield return _nameHands;
yield return _nameLegs;
yield return _nameFeet;
yield return _nameEars;
yield return _nameNeck;
yield return _nameWrists;
yield return _nameRFinger;
yield return _nameLFinger;
yield return _nameMainhand;
yield return _nameOffhand;
yield return _nameGlasses;
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public readonly IEnumerable<string> FilteredItemNames(EquipFlag item, BonusItemFlag bonusItem)
{
if (item.HasFlag(EquipFlag.Head))
yield return _nameHead;
if (item.HasFlag(EquipFlag.Body))
yield return _nameBody;
if (item.HasFlag(EquipFlag.Hands))
yield return _nameHands;
if (item.HasFlag(EquipFlag.Legs))
yield return _nameLegs;
if (item.HasFlag(EquipFlag.Feet))
yield return _nameFeet;
if (item.HasFlag(EquipFlag.Ears))
yield return _nameEars;
if (item.HasFlag(EquipFlag.Neck))
yield return _nameNeck;
if (item.HasFlag(EquipFlag.Wrist))
yield return _nameWrists;
if (item.HasFlag(EquipFlag.RFinger))
yield return _nameRFinger;
if (item.HasFlag(EquipFlag.LFinger))
yield return _nameLFinger;
if (item.HasFlag(EquipFlag.Mainhand))
yield return _nameMainhand;
if (item.HasFlag(EquipFlag.Offhand))
yield return _nameOffhand;
if (bonusItem.HasFlag(BonusItemFlag.Glasses))
yield return _nameGlasses;
}
public readonly FullEquipType MainhandType
public FullEquipType MainhandType
=> _typeMainhand;
public readonly FullEquipType OffhandType
public FullEquipType OffhandType
=> _typeOffhand;
public readonly EquipItem Item(EquipSlot slot)
{
fixed (byte* ptr = _equipmentBytes)
=> slot.ToIndex() switch
{
return slot.ToIndex() switch
{
// @formatter:off
0 => EquipItem.FromIds(_itemIds[ 0], _iconIds[ 0], ((CharacterArmor*)ptr)[0].Set, 0, ((CharacterArmor*)ptr)[0].Variant, FullEquipType.Head, name: _nameHead ),
1 => EquipItem.FromIds(_itemIds[ 1], _iconIds[ 1], ((CharacterArmor*)ptr)[1].Set, 0, ((CharacterArmor*)ptr)[1].Variant, FullEquipType.Body, name: _nameBody ),
2 => EquipItem.FromIds(_itemIds[ 2], _iconIds[ 2], ((CharacterArmor*)ptr)[2].Set, 0, ((CharacterArmor*)ptr)[2].Variant, FullEquipType.Hands, name: _nameHands ),
3 => EquipItem.FromIds(_itemIds[ 3], _iconIds[ 3], ((CharacterArmor*)ptr)[3].Set, 0, ((CharacterArmor*)ptr)[3].Variant, FullEquipType.Legs, name: _nameLegs ),
4 => EquipItem.FromIds(_itemIds[ 4], _iconIds[ 4], ((CharacterArmor*)ptr)[4].Set, 0, ((CharacterArmor*)ptr)[4].Variant, FullEquipType.Feet, name: _nameFeet ),
5 => EquipItem.FromIds(_itemIds[ 5], _iconIds[ 5], ((CharacterArmor*)ptr)[5].Set, 0, ((CharacterArmor*)ptr)[5].Variant, FullEquipType.Ears, name: _nameEars ),
6 => EquipItem.FromIds(_itemIds[ 6], _iconIds[ 6], ((CharacterArmor*)ptr)[6].Set, 0, ((CharacterArmor*)ptr)[6].Variant, FullEquipType.Neck, name: _nameNeck ),
7 => EquipItem.FromIds(_itemIds[ 7], _iconIds[ 7], ((CharacterArmor*)ptr)[7].Set, 0, ((CharacterArmor*)ptr)[7].Variant, FullEquipType.Wrists, name: _nameWrists ),
8 => EquipItem.FromIds(_itemIds[ 8], _iconIds[ 8], ((CharacterArmor*)ptr)[8].Set, 0, ((CharacterArmor*)ptr)[8].Variant, FullEquipType.Finger, name: _nameRFinger ),
9 => EquipItem.FromIds(_itemIds[ 9], _iconIds[ 9], ((CharacterArmor*)ptr)[9].Set, 0, ((CharacterArmor*)ptr)[9].Variant, FullEquipType.Finger, name: _nameLFinger ),
10 => EquipItem.FromIds(_itemIds[10], _iconIds[10], *(PrimaryId*)(ptr + EquipmentByteSize + 0), *(SecondaryId*)(ptr + EquipmentByteSize + 2), *(Variant*)(ptr + EquipmentByteSize + 4), _typeMainhand, name: _nameMainhand),
11 => EquipItem.FromIds(_itemIds[11], _iconIds[11], *(PrimaryId*)(ptr + EquipmentByteSize + 8), *(SecondaryId*)(ptr + EquipmentByteSize + 10), *(Variant*)(ptr + EquipmentByteSize + 12), _typeOffhand, name: _nameOffhand ),
0 => EquipItem.FromIds(_itemIds[ 0], _iconIds[ 0], (SetId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (WeaponType)0, _equipmentBytes[ 2], FullEquipType.Head, name: _nameHead ),
1 => EquipItem.FromIds(_itemIds[ 1], _iconIds[ 1], (SetId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (WeaponType)0, _equipmentBytes[ 6], FullEquipType.Body, name: _nameBody ),
2 => EquipItem.FromIds(_itemIds[ 2], _iconIds[ 2], (SetId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (WeaponType)0, _equipmentBytes[10], FullEquipType.Hands, name: _nameHands ),
3 => EquipItem.FromIds(_itemIds[ 3], _iconIds[ 3], (SetId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (WeaponType)0, _equipmentBytes[14], FullEquipType.Legs, name: _nameLegs ),
4 => EquipItem.FromIds(_itemIds[ 4], _iconIds[ 4], (SetId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (WeaponType)0, _equipmentBytes[18], FullEquipType.Feet, name: _nameFeet ),
5 => EquipItem.FromIds(_itemIds[ 5], _iconIds[ 5], (SetId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (WeaponType)0, _equipmentBytes[22], FullEquipType.Ears, name: _nameEars ),
6 => EquipItem.FromIds(_itemIds[ 6], _iconIds[ 6], (SetId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (WeaponType)0, _equipmentBytes[26], FullEquipType.Neck, name: _nameNeck ),
7 => EquipItem.FromIds(_itemIds[ 7], _iconIds[ 7], (SetId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (WeaponType)0, _equipmentBytes[30], FullEquipType.Wrists, name: _nameWrists ),
8 => EquipItem.FromIds(_itemIds[ 8], _iconIds[ 8], (SetId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (WeaponType)0, _equipmentBytes[34], FullEquipType.Finger, name: _nameRFinger ),
9 => EquipItem.FromIds(_itemIds[ 9], _iconIds[ 9], (SetId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (WeaponType)0, _equipmentBytes[38], FullEquipType.Finger, name: _nameLFinger ),
10 => EquipItem.FromIds(_itemIds[10], _iconIds[10], (SetId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, _equipmentBytes[42], _typeMainhand, name: _nameMainhand),
11 => EquipItem.FromIds(_itemIds[11], _iconIds[11], (SetId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, _equipmentBytes[46], _typeOffhand, name: _nameOffhand ),
_ => new EquipItem(),
// @formatter:on
};
}
}
public readonly EquipItem BonusItem(BonusItemFlag slot)
=> slot switch
{
// @formatter:off
BonusItemFlag.Glasses => EquipItem.FromBonusIds(_bonusIds[0], _iconIds[12], _bonusModelIds[0], _bonusVariants[0], BonusItemFlag.Glasses, _nameGlasses),
_ => EquipItem.BonusItemNothing(slot),
// @formatter:on
};
@ -182,22 +96,22 @@ public unsafe struct DesignData
{
fixed (byte* ptr = _equipmentBytes)
{
var weaponPtr = (CharacterWeapon*)(ptr + EquipmentByteSize);
return weaponPtr[slot is EquipSlot.MainHand ? 0 : 1];
var armorPtr = (CharacterArmor*)ptr;
return slot is EquipSlot.MainHand ? armorPtr[10].ToWeapon(_secondaryMainhand) : armorPtr[11].ToWeapon(_secondaryOffhand);
}
}
public bool SetItem(EquipSlot slot, EquipItem item)
{
var index = slot.ToIndex();
if (index > NumEquipment + NumWeapons)
if (index > 11)
return false;
_itemIds[index] = item.ItemId.Id;
_iconIds[index] = item.IconId.Id;
_equipmentBytes[CharacterArmor.Size * index + 0] = (byte)item.PrimaryId.Id;
_equipmentBytes[CharacterArmor.Size * index + 1] = (byte)(item.PrimaryId.Id >> 8);
_equipmentBytes[CharacterArmor.Size * index + 2] = item.Variant.Id;
_itemIds[index] = item.ItemId.Id;
_iconIds[index] = item.IconId.Id;
_equipmentBytes[4 * index + 0] = (byte)item.ModelId.Id;
_equipmentBytes[4 * index + 1] = (byte)(item.ModelId.Id >> 8);
_equipmentBytes[4 * index + 2] = item.Variant.Id;
switch (index)
{
// @formatter:off
@ -213,93 +127,36 @@ public unsafe struct DesignData
case 9: _nameLFinger = item.Name; return true;
// @formatter:on
case 10:
_nameMainhand = item.Name;
_equipmentBytes[EquipmentByteSize + 2] = (byte)item.SecondaryId.Id;
_equipmentBytes[EquipmentByteSize + 3] = (byte)(item.SecondaryId.Id >> 8);
_equipmentBytes[EquipmentByteSize + 4] = item.Variant.Id;
_typeMainhand = item.Type;
_nameMainhand = item.Name;
_secondaryMainhand = item.WeaponType;
_typeMainhand = item.Type;
return true;
case 11:
_nameOffhand = item.Name;
_equipmentBytes[EquipmentByteSize + 10] = (byte)item.SecondaryId.Id;
_equipmentBytes[EquipmentByteSize + 11] = (byte)(item.SecondaryId.Id >> 8);
_equipmentBytes[EquipmentByteSize + 12] = item.Variant.Id;
_typeOffhand = item.Type;
_nameOffhand = item.Name;
_secondaryOffhand = item.WeaponType;
_typeOffhand = item.Type;
return true;
}
return true;
}
public bool SetBonusItem(BonusItemFlag slot, EquipItem item)
{
var index = slot.ToIndex();
if (index > NumBonusItems)
return false;
_iconIds[NumEquipment + NumWeapons + index] = item.IconId.Id;
_bonusIds[index] = item.Id.BonusItem.Id;
_bonusModelIds[index] = item.PrimaryId.Id;
_bonusVariants[index] = item.Variant.Id;
switch (index)
{
case 0:
_nameGlasses = item.Name;
return true;
default: return false;
}
}
public bool SetStain(EquipSlot slot, StainIds stains)
public bool SetStain(EquipSlot slot, StainId stain)
=> slot.ToIndex() switch
{
// @formatter:off
0 => SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[0 * CharacterArmor.Size + 4], stains.Stain2.Id),
1 => SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[1 * CharacterArmor.Size + 4], stains.Stain2.Id),
2 => SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[2 * CharacterArmor.Size + 4], stains.Stain2.Id),
3 => SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[3 * CharacterArmor.Size + 4], stains.Stain2.Id),
4 => SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[4 * CharacterArmor.Size + 4], stains.Stain2.Id),
5 => SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[5 * CharacterArmor.Size + 4], stains.Stain2.Id),
6 => SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[6 * CharacterArmor.Size + 4], stains.Stain2.Id),
7 => SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[7 * CharacterArmor.Size + 4], stains.Stain2.Id),
8 => SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[8 * CharacterArmor.Size + 4], stains.Stain2.Id),
9 => SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 3], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[9 * CharacterArmor.Size + 4], stains.Stain2.Id),
10 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 6], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 7], stains.Stain2.Id),
11 => SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 14], stains.Stain1.Id) | SetIfDifferent(ref _equipmentBytes[EquipmentByteSize + 15], stains.Stain2.Id),
_ => false,
// @formatter:on
};
public bool SetCrest(CrestFlag slot, bool visible)
{
var newValue = visible ? CrestVisibility | slot : CrestVisibility & ~slot;
if (newValue == CrestVisibility)
return false;
CrestVisibility = newValue;
return true;
}
public readonly bool GetMeta(MetaIndex index)
=> index switch
{
MetaIndex.Wetness => IsWet(),
MetaIndex.HatState => IsHatVisible(),
MetaIndex.VisorState => IsVisorToggled(),
MetaIndex.WeaponState => IsWeaponVisible(),
MetaIndex.EarState => AreEarsVisible(),
_ => false,
};
public bool SetMeta(MetaIndex index, bool value)
=> index switch
{
MetaIndex.Wetness => SetIsWet(value),
MetaIndex.HatState => SetHatVisible(value),
MetaIndex.VisorState => SetVisor(value),
MetaIndex.WeaponState => SetWeaponVisible(value),
MetaIndex.EarState => SetEarsVisible(value),
_ => false,
0 => SetIfDifferent(ref _equipmentBytes[3], stain.Id),
1 => SetIfDifferent(ref _equipmentBytes[7], stain.Id),
2 => SetIfDifferent(ref _equipmentBytes[11], stain.Id),
3 => SetIfDifferent(ref _equipmentBytes[15], stain.Id),
4 => SetIfDifferent(ref _equipmentBytes[19], stain.Id),
5 => SetIfDifferent(ref _equipmentBytes[23], stain.Id),
6 => SetIfDifferent(ref _equipmentBytes[27], stain.Id),
7 => SetIfDifferent(ref _equipmentBytes[31], stain.Id),
8 => SetIfDifferent(ref _equipmentBytes[35], stain.Id),
9 => SetIfDifferent(ref _equipmentBytes[39], stain.Id),
10 => SetIfDifferent(ref _equipmentBytes[43], stain.Id),
11 => SetIfDifferent(ref _equipmentBytes[47], stain.Id),
_ => false,
};
public readonly bool IsWet()
@ -342,9 +199,6 @@ public unsafe struct DesignData
public readonly bool IsWeaponVisible()
=> (_states & 0x08) == 0x08;
public readonly bool AreEarsVisible()
=> (_states & 0x10) == 0x00;
public bool SetWeaponVisible(bool value)
{
if (value == IsWeaponVisible())
@ -354,45 +208,26 @@ public unsafe struct DesignData
return true;
}
public bool SetEarsVisible(bool value)
{
if (value == AreEarsVisible())
return false;
_states = (byte)(value ? _states & ~0x10 : _states | 0x10);
return true;
}
public void SetDefaultEquipment(ItemManager items)
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
SetItem(slot, ItemManager.NothingItem(slot));
SetStain(slot, StainIds.None);
SetCrest(slot.ToCrestFlag(), false);
SetStain(slot, 0);
}
SetItem(EquipSlot.MainHand, items.DefaultSword);
SetStain(EquipSlot.MainHand, StainIds.None);
SetCrest(CrestFlag.MainHand, false);
SetStain(EquipSlot.MainHand, 0);
SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield));
SetStain(EquipSlot.OffHand, StainIds.None);
SetCrest(CrestFlag.OffHand, false);
SetDefaultBonusItems();
}
public void SetDefaultBonusItems()
{
foreach (var slot in BonusExtensions.AllFlags)
SetBonusItem(slot, EquipItem.BonusItemNothing(slot));
SetStain(EquipSlot.OffHand, 0);
}
public bool LoadNonHuman(uint modelId, CustomizeArray customize, nint equipData)
public bool LoadNonHuman(uint modelId, Customize customize, nint equipData)
{
ModelId = modelId;
IsHuman = false;
Customize.Read(customize.Data);
Customize.Load(customize);
fixed (byte* ptr = _equipmentBytes)
{
MemoryUtility.MemCpyUnchecked(ptr, (byte*)equipData, 40);
@ -400,14 +235,13 @@ public unsafe struct DesignData
SetHatVisible(true);
SetWeaponVisible(true);
SetEarsVisible(true);
SetVisor(false);
fixed (uint* ptr = _itemIds)
{
MemoryUtility.MemSet(ptr, 0, 10 * 4);
}
fixed (uint* ptr = _iconIds)
fixed (ushort* ptr = _iconIds)
{
MemoryUtility.MemSet(ptr, 0, 10 * 2);
}
@ -422,14 +256,13 @@ public unsafe struct DesignData
_nameWrists = string.Empty;
_nameRFinger = string.Empty;
_nameLFinger = string.Empty;
_nameGlasses = string.Empty;
return true;
}
public readonly byte[] GetCustomizeBytes()
{
var ret = new byte[CustomizeArray.Size];
fixed (byte* retPtr = ret, inPtr = Customize.Data)
var ret = new byte[CustomizeData.Size];
fixed (byte* retPtr = ret, inPtr = Customize.Data.Data)
{
MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length);
}
@ -439,7 +272,7 @@ public unsafe struct DesignData
public readonly byte[] GetEquipmentBytes()
{
var ret = new byte[80];
var ret = new byte[40];
fixed (byte* retPtr = ret, inPtr = _equipmentBytes)
{
MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length);
@ -460,8 +293,8 @@ public unsafe struct DesignData
{
fixed (byte* dataPtr = _equipmentBytes)
{
var data = new Span<byte>(dataPtr, 80);
return Convert.TryFromBase64String(base64, data, out var written) && written == 80;
var data = new Span<byte>(dataPtr, 40);
return Convert.TryFromBase64String(base64, data, out var written) && written == 40;
}
}

View file

@ -1,397 +0,0 @@
using Glamourer.Designs.History;
using Glamourer.Designs.Links;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignEditor(
SaveService saveService,
DesignChanged designChanged,
CustomizeService customizations,
ItemManager items,
Configuration config)
: IDesignEditor
{
protected readonly DesignChanged DesignChanged = designChanged;
protected readonly SaveService SaveService = saveService;
protected readonly ItemManager Items = items;
protected readonly CustomizeService Customizations = customizations;
protected readonly Configuration Config = config;
protected readonly Dictionary<Guid, DesignData> UndoStore = [];
private bool _forceFullItemOff;
/// <summary> Whether an Undo for the given design is possible. </summary>
public bool CanUndo(Design? design)
=> design != null && UndoStore.ContainsKey(design.Identifier);
/// <inheritdoc/>
public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings _ = default)
{
var design = (Design)data;
var oldValue = design.DesignData.Customize[idx];
switch (idx)
{
case CustomizeIndex.Race:
case CustomizeIndex.BodyType:
Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen.");
return;
case CustomizeIndex.Clan:
{
var customize = design.DesignData.Customize;
if (Customizations.ChangeClan(ref customize, (SubRace)value.Value) == 0)
return;
if (!design.SetCustomize(Customizations, customize))
return;
break;
}
case CustomizeIndex.Gender:
{
var customize = design.DesignData.Customize;
if (Customizations.ChangeGender(ref customize, (Gender)(value.Value + 1)) == 0)
return;
if (!design.SetCustomize(Customizations, customize))
return;
break;
}
default:
if (!Customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender,
design.DesignData.Customize.Face, idx, value)
|| !design.GetDesignDataRef().Customize.Set(idx, value))
return;
break;
}
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}.");
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Customize, design, new CustomizeTransaction(idx, oldValue, value));
}
/// <inheritdoc/>
public void ChangeEntireCustomize(object data, in CustomizeArray customize, CustomizeFlag apply, ApplySettings _ = default)
{
var design = (Design)data;
var (newCustomize, applied, changed) = Customizations.Combine(design.DesignData.Customize, customize, apply, true);
if (changed == 0)
return;
var oldCustomize = design.DesignData.Customize;
design.SetCustomize(Customizations, newCustomize);
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Changed entire customize with resulting flags {applied} and {changed}.");
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.EntireCustomize, design, new EntireCustomizeTransaction(changed, oldCustomize, newCustomize));
}
/// <inheritdoc/>
public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue value, ApplySettings _ = default)
{
var design = (Design)data;
var old = design.DesignData.Parameters[flag];
if (!design.GetDesignDataRef().Parameters.Set(flag, value))
return;
var @new = design.DesignData.Parameters[flag];
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Set customize parameter {flag} in design {design.Identifier} from {old} to {@new}.");
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Parameter, design, new ParameterTransaction(flag, old, @new));
}
/// <inheritdoc/>
public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings _ = default)
{
var design = (Design)data;
switch (slot)
{
case EquipSlot.MainHand:
{
var currentMain = design.DesignData.Item(EquipSlot.MainHand);
var currentOff = design.DesignData.Item(EquipSlot.OffHand);
if (!Items.IsItemValid(EquipSlot.MainHand, item.ItemId, out item))
return;
if (!ChangeMainhandPeriphery(design, currentMain, currentOff, item, out var newOff, out var newGauntlets))
return;
var currentGauntlets = design.DesignData.Item(EquipSlot.Hands);
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId}).");
DesignChanged.Invoke(DesignChanged.Type.Weapon, design,
new WeaponTransaction(currentMain, currentOff, currentGauntlets, item, newOff ?? currentOff,
newGauntlets ?? currentGauntlets));
return;
}
case EquipSlot.OffHand:
{
var currentMain = design.DesignData.Item(EquipSlot.MainHand);
var currentOff = design.DesignData.Item(EquipSlot.OffHand);
if (!Items.IsOffhandValid(currentOff.Type, item.ItemId, out item))
return;
if (!design.GetDesignDataRef().SetItem(EquipSlot.OffHand, item))
return;
var currentGauntlets = design.DesignData.Item(EquipSlot.Hands);
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId}).");
DesignChanged.Invoke(DesignChanged.Type.Weapon, design,
new WeaponTransaction(currentMain, currentOff, currentGauntlets, currentMain, item, currentGauntlets));
return;
}
default:
{
if (!Items.IsItemValid(slot, item.Id, out item))
return;
var old = design.DesignData.Item(slot);
if (!design.GetDesignDataRef().SetItem(slot, item))
return;
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug(
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}).");
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.Equip, design, new EquipTransaction(slot, old, item));
return;
}
}
}
/// <inheritdoc/>
public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default)
{
var design = (Design)data;
if (item.Type.ToBonus() != slot)
return;
var oldItem = design.DesignData.BonusItem(slot);
if (!design.GetDesignDataRef().SetBonusItem(slot, item))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set {slot} bonus item to {item}.");
DesignChanged.Invoke(DesignChanged.Type.BonusItem, design, new BonusItemTransaction(slot, oldItem, item));
}
/// <inheritdoc/>
public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings _ = default)
{
var design = (Design)data;
if (Items.ValidateStain(stains, out var _, false).Length > 0)
return;
var oldStain = design.DesignData.Stain(slot);
if (!design.GetDesignDataRef().SetStain(slot, stains))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stains}.");
DesignChanged.Invoke(DesignChanged.Type.Stains, design, new StainTransaction(slot, oldStain, stains));
}
/// <inheritdoc/>
public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings _ = default)
{
if (item.HasValue)
ChangeItem(data, slot, item.Value, _);
if (stains.HasValue)
ChangeStains(data, slot, stains.Value, _);
}
/// <inheritdoc/>
public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings _ = default)
{
var design = (Design)data;
var oldCrest = design.DesignData.Crest(slot);
if (!design.GetDesignDataRef().SetCrest(slot, crest))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}.");
DesignChanged.Invoke(DesignChanged.Type.Crest, design, new CrestTransaction(slot, oldCrest, crest));
}
/// <inheritdoc/>
public void ChangeMetaState(object data, MetaIndex metaIndex, bool value, ApplySettings _ = default)
{
var design = (Design)data;
if (!design.GetDesignDataRef().SetMeta(metaIndex, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set value of {metaIndex} to {value}.");
DesignChanged.Invoke(DesignChanged.Type.Other, design, new MetaTransaction(metaIndex, !value, value));
}
public void ChangeMaterialRevert(Design design, MaterialValueIndex index, bool revert)
{
var materials = design.GetMaterialDataRef();
if (!materials.TryGetValue(index, out var oldValue))
return;
materials.AddOrUpdateValue(index, oldValue with { Revert = revert });
Glamourer.Log.Debug($"Changed advanced dye value for {index} to {(revert ? "Revert." : "no longer Revert.")}");
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.MaterialRevert, design, new MaterialRevertTransaction(index, !revert, revert));
}
public void ChangeMaterialValue(Design design, MaterialValueIndex index, ColorRow? row)
{
var materials = design.GetMaterialDataRef();
if (materials.TryGetValue(index, out var oldValue))
{
if (!row.HasValue)
{
materials.RemoveValue(index);
Glamourer.Log.Debug($"Removed advanced dye value for {index}.");
}
else if (!row.Value.NearEqual(oldValue.Value))
{
materials.UpdateValue(index, new MaterialValueDesign(row.Value, oldValue.Enabled, oldValue.Revert), out _);
Glamourer.Log.Debug($"Updated advanced dye value for {index} to new value.");
}
else
{
return;
}
}
else
{
if (!row.HasValue)
return;
if (!materials.TryAddValue(index, new MaterialValueDesign(row.Value, true, false)))
return;
Glamourer.Log.Debug($"Added new advanced dye value for {index}.");
}
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.DelaySave(design);
DesignChanged.Invoke(DesignChanged.Type.Material, design, new MaterialTransaction(index, oldValue.Value, row));
}
public void ChangeApplyMaterialValue(Design design, MaterialValueIndex index, bool value)
{
var materials = design.GetMaterialDataRef();
if (!materials.TryGetValue(index, out var oldValue) || oldValue.Enabled == value)
return;
materials.AddOrUpdateValue(index, oldValue with { Enabled = value });
Glamourer.Log.Debug($"Changed application of advanced dye for {index} to {value}.");
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
DesignChanged.Invoke(DesignChanged.Type.ApplyMaterial, design, new ApplicationTransaction(index, !value, value));
}
/// <inheritdoc/>
public void ApplyDesign(object data, MergedDesign other, ApplySettings settings = default)
=> ApplyDesign(data, other.Design, settings);
/// <inheritdoc/>
public void ApplyDesign(object data, DesignBase other, ApplySettings _ = default)
{
var design = (Design)data;
UndoStore[design.Identifier] = design.DesignData;
foreach (var index in MetaExtensions.AllRelevant.Where(other.DoApplyMeta))
design.GetDesignDataRef().SetMeta(index, other.DesignData.GetMeta(index));
if (!design.DesignData.IsHuman)
return;
ChangeEntireCustomize(design, other.DesignData.Customize, other.ApplyCustomize);
_forceFullItemOff = true;
foreach (var slot in EquipSlotExtensions.FullSlots)
{
ChangeEquip(design, slot,
other.DoApplyEquip(slot) ? other.DesignData.Item(slot) : null,
other.DoApplyStain(slot) ? other.DesignData.Stain(slot) : null);
}
_forceFullItemOff = false;
foreach (var slot in BonusExtensions.AllFlags)
{
if (other.DoApplyBonusItem(slot))
ChangeBonusItem(design, slot, other.DesignData.BonusItem(slot));
}
foreach (var slot in Enum.GetValues<CrestFlag>().Where(other.DoApplyCrest))
ChangeCrest(design, slot, other.DesignData.Crest(slot));
foreach (var parameter in CustomizeParameterExtensions.AllFlags.Where(other.DoApplyParameter))
ChangeCustomizeParameter(design, parameter, other.DesignData.Parameters[parameter]);
foreach (var (key, value) in other.Materials)
{
if (!value.Enabled)
continue;
design.GetMaterialDataRef().AddOrUpdateValue(MaterialValueIndex.FromKey(key), value);
}
}
/// <summary> Change a mainhand weapon and either fix or apply appropriate offhand and potentially gauntlets. </summary>
private bool ChangeMainhandPeriphery(DesignBase design, EquipItem currentMain, EquipItem currentOff, EquipItem newMain,
out EquipItem? newOff,
out EquipItem? newGauntlets)
{
newOff = null;
newGauntlets = null;
if (newMain.Type != currentMain.Type)
{
var defaultOffhand = Items.GetDefaultOffhand(newMain);
if (!Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o))
return false;
newOff = o;
}
else if (!_forceFullItemOff && Config.ChangeEntireItem && newMain.Type is not FullEquipType.Sword) // Skip applying shields.
{
var defaultOffhand = Items.GetDefaultOffhand(newMain);
if (Items.IsOffhandValid(newMain, defaultOffhand.ItemId, out var o))
newOff = o;
if (newMain.Type is FullEquipType.Fists && Items.ItemData.Tertiary.TryGetValue(newMain.ItemId, out var g))
newGauntlets = g;
}
if (!design.GetDesignDataRef().SetItem(EquipSlot.MainHand, newMain))
return false;
if (newOff.HasValue && !design.GetDesignDataRef().SetItem(EquipSlot.OffHand, newOff.Value))
{
design.GetDesignDataRef().SetItem(EquipSlot.MainHand, currentMain);
return false;
}
if (newGauntlets.HasValue && !design.GetDesignDataRef().SetItem(EquipSlot.Hands, newGauntlets.Value))
{
design.GetDesignDataRef().SetItem(EquipSlot.MainHand, currentMain);
design.GetDesignDataRef().SetItem(EquipSlot.OffHand, currentOff);
return false;
}
return true;
}
}

View file

@ -1,6 +1,12 @@
using Dalamud.Interface.ImGuiNotification;
using Glamourer.Designs.History;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Events;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -41,11 +47,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct CreationDate : ISortMode<Design>
{
public ReadOnlySpan<byte> Name
=> "Creation Date (Older First)"u8;
public string Name
=> "Creation Date (Older First)";
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date."u8;
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date.";
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate));
@ -53,11 +59,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct UpdateDate : ISortMode<Design>
{
public ReadOnlySpan<byte> Name
=> "Update Date (Older First)"u8;
public string Name
=> "Update Date (Older First)";
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date."u8;
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their last update date.";
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.LastEdit));
@ -65,11 +71,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct InverseCreationDate : ISortMode<Design>
{
public ReadOnlySpan<byte> Name
=> "Creation Date (Newer First)"u8;
public string Name
=> "Creation Date (Newer First)";
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date."u8;
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date.";
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate));
@ -77,11 +83,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public struct InverseUpdateDate : ISortMode<Design>
{
public ReadOnlySpan<byte> Name
=> "Update Date (Newer First)"u8;
public string Name
=> "Update Date (Newer First)";
public ReadOnlySpan<byte> Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date."u8;
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse last update date.";
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.LastEdit));
@ -93,35 +99,34 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
_saveService.QueueSave(this);
}
private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? data)
private void OnDesignChange(DesignChanged.Type type, Design design, object? data)
{
switch (type)
{
case DesignChanged.Type.Created:
var parent = Root;
if ((data as CreationTransaction?)?.Path is { } path)
if (data is string path)
try
{
parent = FindOrCreateAllFolders(path);
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, $"Could not move design to {path} because the folder could not be created.",
NotificationType.Error);
Glamourer.Messager.NotificationMessage(ex, $"Could not move design to {path} because the folder could not be created.", NotificationType.Error);
}
CreateDuplicateLeaf(parent, design.Name.Text, design);
return;
case DesignChanged.Type.Deleted:
if (TryGetValue(design, out var leaf1))
if (FindLeaf(design, out var leaf1))
Delete(leaf1);
return;
case DesignChanged.Type.ReloadedAll:
Reload();
return;
case DesignChanged.Type.Renamed when (data as RenameTransaction?)?.Old is { } oldName:
if (!TryGetValue(design, out var leaf2))
case DesignChanged.Type.Renamed when data is string oldName:
if (!FindLeaf(design, out var leaf2))
return;
var old = oldName.FixName();
@ -150,6 +155,15 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
? (string.Empty, false)
: (DesignToIdentifier(design), true);
// Search the entire filesystem for the leaf corresponding to a design.
public bool FindLeaf(Design design, [NotNullWhen(true)] out Leaf? leaf)
{
leaf = Root.GetAllDescendants(ISortMode<Design>.Lexicographical)
.OfType<Leaf>()
.FirstOrDefault(l => l.Value == design);
return leaf != null;
}
internal static void MigrateOldPaths(SaveService saveService, Dictionary<string, string> oldPaths)
{
if (oldPaths.Count == 0)

View file

@ -1,84 +1,76 @@
using Dalamud.Utility;
using Glamourer.Designs.History;
using Glamourer.Designs.Links;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Utility;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using OtterGui.Extensions;
using Glamourer.State;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.DataContainers;
using OtterGui;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public sealed class DesignManager : DesignEditor
public class DesignManager
{
public readonly DesignStorage Designs;
private readonly HumanModelList _humans;
private readonly CustomizationService _customizations;
private readonly ItemManager _items;
private readonly HumanModelList _humans;
private readonly SaveService _saveService;
private readonly DesignChanged _event;
private readonly List<Design> _designs = new();
public DesignManager(SaveService saveService, ItemManager items, CustomizeService customizations,
DesignChanged @event, HumanModelList humans, DesignStorage storage, DesignLinkLoader designLinkLoader, Configuration config)
: base(saveService, @event, customizations, items, config)
public IReadOnlyList<Design> Designs
=> _designs;
public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations,
DesignChanged @event, HumanModelList humans)
{
Designs = storage;
_humans = humans;
LoadDesigns(designLinkLoader);
_saveService = saveService;
_items = items;
_customizations = customizations;
_event = @event;
_humans = humans;
CreateDesignFolder(saveService);
LoadDesigns();
MigrateOldDesigns();
designLinkLoader.SetAllObjects();
}
#region Design Management
/// <summary>
/// Clear currently loaded designs and load all designs anew from file.
/// Invalid data is fixed, but changes are not saved until manual changes.
/// </summary>
private void LoadDesigns(DesignLinkLoader linkLoader)
public void LoadDesigns()
{
_humans.Awaiter.Wait();
Customizations.Awaiter.Wait();
Items.ItemData.Awaiter.Wait();
var stopwatch = Stopwatch.StartNew();
Designs.Clear();
var skipped = 0;
ThreadLocal<List<(Design, string)>> designs = new(() => [], true);
Parallel.ForEach(SaveService.FileNames.Designs(), (f, _) =>
_designs.Clear();
List<(Design, string)> invalidNames = new();
var skipped = 0;
foreach (var file in _saveService.FileNames.Designs())
{
try
{
var text = File.ReadAllText(f.FullName);
var text = File.ReadAllText(file.FullName);
var data = JObject.Parse(text);
var design = Design.LoadDesign(SaveService, Customizations, Items, linkLoader, data);
designs.Value!.Add((design, f.FullName));
var design = Design.LoadDesign(_customizations, _items, data);
if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(file.Name))
invalidNames.Add((design, file.FullName));
if (_designs.Any(f => f.Identifier == design.Identifier))
throw new Exception($"Identifier {design.Identifier} was not unique.");
design.Index = _designs.Count;
_designs.Add(design);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not load design, skipped:\n{ex}");
Interlocked.Increment(ref skipped);
}
});
List<(Design, string)> invalidNames = [];
foreach (var (design, path) in designs.Values.SelectMany(v => v))
{
if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(path))
invalidNames.Add((design, path));
if (Designs.Contains(design.Identifier))
{
Glamourer.Log.Error($"Could not load design, skipped: Identifier {design.Identifier} was not unique.");
++skipped;
continue;
}
design.Index = Designs.Count;
Designs.Add(design);
}
var failed = MoveInvalidNames(invalidNames);
@ -87,35 +79,30 @@ public sealed class DesignManager : DesignEditor
$"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}");
Glamourer.Log.Information(
$"Loaded {Designs.Count} designs in {stopwatch.ElapsedMilliseconds} ms.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
DesignChanged.Invoke(DesignChanged.Type.ReloadedAll, null!, null);
$"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
_event.Invoke(DesignChanged.Type.ReloadedAll, null!);
}
/// <summary> Create a new temporary design without adding it to the manager. </summary>
public DesignBase CreateTemporary()
=> new(Customizations, Items);
=> new(_items);
/// <summary> Create a new design of a given name. </summary>
public Design CreateEmpty(string name, bool handlePath)
{
var (actualName, path) = ParseName(name, handlePath);
var design = new Design(Customizations, Items)
var design = new Design(_customizations, _items)
{
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = Designs.Count,
ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing,
ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes,
QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar,
ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings,
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = _designs.Count,
};
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design);
_designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
_saveService.ImmediateSave(design);
_event.Invoke(DesignChanged.Type.Created, design, path);
return design;
}
@ -125,22 +112,17 @@ public sealed class DesignManager : DesignEditor
var (actualName, path) = ParseName(name, handlePath);
var design = new Design(clone)
{
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = Designs.Count,
ForcedRedraw = Config.DefaultDesignSettings.AlwaysForceRedrawing,
ResetAdvancedDyes = Config.DefaultDesignSettings.ResetAdvancedDyes,
QuickDesign = Config.DefaultDesignSettings.ShowQuickDesignBar,
ResetTemporarySettings = Config.DefaultDesignSettings.ResetTemporarySettings,
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = _designs.Count,
};
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design);
_designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design.");
SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
_saveService.ImmediateSave(design);
_event.Invoke(DesignChanged.Type.Created, design, path);
return design;
}
@ -154,31 +136,26 @@ public sealed class DesignManager : DesignEditor
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
Index = Designs.Count,
Index = _designs.Count,
};
design.SetWriteProtected(Config.DefaultDesignSettings.Locked);
Designs.Add(design);
_designs.Add(design);
Glamourer.Log.Debug(
$"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}.");
SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, new CreationTransaction(actualName, path));
_saveService.ImmediateSave(design);
_event.Invoke(DesignChanged.Type.Created, design, path);
return design;
}
/// <summary> Delete a design. </summary>
public void Delete(Design design)
{
foreach (var d in Designs.Skip(design.Index + 1))
foreach (var d in _designs.Skip(design.Index + 1))
--d.Index;
Designs.RemoveAt(design.Index);
SaveService.ImmediateDelete(design);
DesignChanged.Invoke(DesignChanged.Type.Deleted, design, null);
_designs.RemoveAt(design.Index);
_saveService.ImmediateDelete(design);
_event.Invoke(DesignChanged.Type.Deleted, design);
}
#endregion
#region Edit Information
/// <summary> Rename a design. </summary>
public void Rename(Design design, string newName)
{
@ -188,9 +165,9 @@ public sealed class DesignManager : DesignEditor
design.Name = newName;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.Renamed, design, new RenameTransaction(oldName, newName));
_event.Invoke(DesignChanged.Type.Renamed, design, oldName);
}
/// <summary> Change the description of a design. </summary>
@ -202,23 +179,9 @@ public sealed class DesignManager : DesignEditor
design.Description = description;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Changed description of design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.ChangedDescription, design, new DescriptionTransaction(oldDescription, description));
}
/// <summary> Change the associated color of a design. </summary>
public void ChangeColor(Design design, string newColor)
{
var oldColor = design.Color;
if (oldColor == newColor)
return;
design.Color = newColor;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Changed color of design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.ChangedColor, design, new DesignColorTransaction(oldColor, newColor));
_event.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription);
}
/// <summary> Add a new tag to a design. The tags remain sorted. </summary>
@ -229,12 +192,16 @@ public sealed class DesignManager : DesignEditor
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
design.LastEdit = DateTimeOffset.UtcNow;
var idx = design.Tags.AsEnumerable().IndexOf(tag);
SaveService.QueueSave(design);
var idx = design.Tags.IndexOf(tag);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, new TagAddedTransaction(tag, idx));
_event.Invoke(DesignChanged.Type.AddedTag, design, (tag, idx));
}
/// <summary> Remove a tag from a design if it exists. </summary>
public void RemoveTag(Design design, string tag)
=> RemoveTag(design, design.Tags.IndexOf(tag));
/// <summary> Remove a tag from a design by its index. </summary>
public void RemoveTag(Design design, int tagIdx)
{
@ -244,9 +211,9 @@ public sealed class DesignManager : DesignEditor
var oldTag = design.Tags[tagIdx];
design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray();
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.RemovedTag, design, new TagRemovedTransaction(oldTag, tagIdx));
_event.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx));
}
/// <summary> Rename a tag from a design by its index. The tags stay sorted.</summary>
@ -259,10 +226,9 @@ public sealed class DesignManager : DesignEditor
design.Tags[tagIdx] = newTag;
Array.Sort(design.Tags);
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design,
new TagChangedTransaction(oldTag, newTag, tagIdx, design.Tags.AsEnumerable().IndexOf(newTag)));
_event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx));
}
/// <summary> Add an associated mod to a design. </summary>
@ -272,9 +238,9 @@ public sealed class DesignManager : DesignEditor
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings));
_event.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings));
}
/// <summary> Remove an associated mod from a design. </summary>
@ -284,28 +250,9 @@ public sealed class DesignManager : DesignEditor
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.RemovedMod, design, new ModRemovedTransaction(mod, settings));
}
/// <summary> Add or update an associated mod to a design. </summary>
public void UpdateMod(Design design, Mod mod, ModSettings settings)
{
var hasOldSettings = design.AssociatedMods.TryGetValue(mod, out var oldSettings);
design.AssociatedMods[mod] = settings;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
if (hasOldSettings)
{
Glamourer.Log.Debug($"Updated associated mod {mod.DirectoryName} from design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.UpdatedMod, design, new ModUpdatedTransaction(mod, oldSettings, settings));
}
else
{
Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} from design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.AddedMod, design, new ModAddedTransaction(mod, settings));
}
_event.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings));
}
/// <summary> Set the write protection status of a design. </summary>
@ -314,202 +261,260 @@ public sealed class DesignManager : DesignEditor
if (!design.SetWriteProtected(value))
return;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected.");
DesignChanged.Invoke(DesignChanged.Type.WriteProtection, design, null);
_event.Invoke(DesignChanged.Type.WriteProtection, design, value);
}
/// <summary> Set the quick design bar display status of a design. </summary>
public void SetQuickDesign(Design design, bool value)
/// <summary> Change a customization value. </summary>
public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value)
{
if (value == design.QuickDesign)
return;
var oldValue = design.DesignData.Customize[idx];
switch (idx)
{
case CustomizeIndex.Race:
case CustomizeIndex.BodyType:
Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen.");
return;
case CustomizeIndex.Clan:
if (_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value) == 0)
return;
design.QuickDesign = value;
SaveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set design {design.Identifier} to {(!value ? "no longer be " : string.Empty)} displayed in the quick design bar.");
DesignChanged.Invoke(DesignChanged.Type.QuickDesignBar, design, null);
}
design.RemoveInvalidCustomize(_customizations);
break;
case CustomizeIndex.Gender:
if (_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1)) == 0)
return;
#endregion
design.RemoveInvalidCustomize(_customizations);
break;
default:
if (!_customizations.IsCustomizationValid(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender,
design.DesignData.Customize.Face, idx, value)
|| !design.DesignData.Customize.Set(idx, value))
return;
#region Edit Application Rules
break;
}
public void ChangeForcedRedraw(Design design, bool forcedRedraw)
{
if (design.ForcedRedraw == forcedRedraw)
return;
design.ForcedRedraw = forcedRedraw;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set {design.Identifier} to {(forcedRedraw ? string.Empty : "not")} force redraws.");
DesignChanged.Invoke(DesignChanged.Type.ForceRedraw, design, null);
}
public void ChangeResetAdvancedDyes(Design design, bool resetAdvancedDyes)
{
if (design.ResetAdvancedDyes == resetAdvancedDyes)
return;
design.ResetAdvancedDyes = resetAdvancedDyes;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set {design.Identifier} to {(resetAdvancedDyes ? string.Empty : "not")} reset advanced dyes.");
DesignChanged.Invoke(DesignChanged.Type.ResetAdvancedDyes, design, null);
}
public void ChangeResetTemporarySettings(Design design, bool resetTemporarySettings)
{
if (design.ResetTemporarySettings == resetTemporarySettings)
return;
design.ResetTemporarySettings = resetTemporarySettings;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set {design.Identifier} to {(resetTemporarySettings ? string.Empty : "not")} reset temporary settings.");
DesignChanged.Invoke(DesignChanged.Type.ResetTemporarySettings, design, null);
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}.");
_saveService.QueueSave(design);
_event.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx));
}
/// <summary> Change whether to apply a specific customize value. </summary>
public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
{
var set = _customizations.AwaitedService.GetList(design.DesignData.Customize.Clan, design.DesignData.Customize.Gender);
value &= set.IsAvailable(idx) || idx is CustomizeIndex.Clan or CustomizeIndex.Gender;
if (!design.SetApplyCustomize(idx, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyCustomize, design, new ApplicationTransaction(idx, !value, value));
_event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx);
}
/// <summary> Change a non-weapon equipment piece. </summary>
public void ChangeEquip(Design design, EquipSlot slot, EquipItem item)
{
if (!_items.IsItemValid(slot, item.ItemId, out item))
return;
var old = design.DesignData.Item(slot);
if (!design.DesignData.SetItem(slot, item))
return;
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug(
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}).");
_saveService.QueueSave(design);
_event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot));
}
/// <summary> Change a weapon. </summary>
public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)
{
var currentMain = design.DesignData.Item(EquipSlot.MainHand);
var currentOff = design.DesignData.Item(EquipSlot.OffHand);
switch (slot)
{
case EquipSlot.MainHand:
var newOff = currentOff;
if (!_items.IsItemValid(EquipSlot.MainHand, item.ItemId, out item))
return;
if (item.Type != currentMain.Type)
{
var defaultOffhand = _items.GetDefaultOffhand(item);
if (!_items.IsOffhandValid(item, defaultOffhand.ItemId, out newOff))
return;
}
if (!(design.DesignData.SetItem(EquipSlot.MainHand, item) | design.DesignData.SetItem(EquipSlot.OffHand, newOff)))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.ItemId}) to {item.Name} ({item.ItemId}).");
_event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff));
return;
case EquipSlot.OffHand:
if (!_items.IsOffhandValid(currentOff.Type, item.ItemId, out item))
return;
if (!design.DesignData.SetItem(EquipSlot.OffHand, item))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.ItemId}) to {item.Name} ({item.ItemId}).");
_event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item));
return;
default: return;
}
}
/// <summary> Change whether to apply a specific equipment piece. </summary>
public void ChangeApplyItem(Design design, EquipSlot slot, bool value)
public void ChangeApplyEquip(Design design, EquipSlot slot, bool value)
{
if (!design.SetApplyEquip(slot, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, new ApplicationTransaction((slot, false), !value, value));
_event.Invoke(DesignChanged.Type.ApplyEquip, design, slot);
}
/// <summary> Change whether to apply a specific equipment piece. </summary>
public void ChangeApplyBonusItem(Design design, BonusItemFlag slot, bool value)
/// <summary> Change the stain for any equipment piece. </summary>
public void ChangeStain(Design design, EquipSlot slot, StainId stain)
{
if (!design.SetApplyBonusItem(slot, value))
if (_items.ValidateStain(stain, out _, false).Length > 0)
return;
var oldStain = design.DesignData.Stain(slot);
if (!design.DesignData.SetStain(slot, stain))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of {slot} bonus item to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyBonusItem, design, new ApplicationTransaction(slot, !value, value));
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Id}.");
_event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot));
}
/// <summary> Change whether to apply a specific stain. </summary>
public void ChangeApplyStains(Design design, EquipSlot slot, bool value)
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
{
if (!design.SetApplyStain(slot, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyStain, design, new ApplicationTransaction((slot, true), !value, value));
_event.Invoke(DesignChanged.Type.ApplyStain, design, slot);
}
/// <summary> Change whether to apply a specific crest visibility. </summary>
public void ChangeApplyCrest(Design design, CrestFlag slot, bool value)
/// <summary> Change the bool value of one of the meta flags. </summary>
public void ChangeMeta(Design design, ActorState.MetaIndex metaIndex, bool value)
{
if (!design.SetApplyCrest(slot, value))
var change = metaIndex switch
{
ActorState.MetaIndex.Wetness => design.DesignData.SetIsWet(value),
ActorState.MetaIndex.HatState => design.DesignData.SetHatVisible(value),
ActorState.MetaIndex.VisorState => design.DesignData.SetVisor(value),
ActorState.MetaIndex.WeaponState => design.DesignData.SetWeaponVisible(value),
_ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null),
};
if (!change)
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyCrest, design, new ApplicationTransaction(slot, !value, value));
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set value of {metaIndex} to {value}.");
_event.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value));
}
/// <summary> Change the application value of one of the meta flags. </summary>
public void ChangeApplyMeta(Design design, MetaIndex metaIndex, bool value)
public void ChangeApplyMeta(Design design, ActorState.MetaIndex metaIndex, bool value)
{
if (!design.SetApplyMeta(metaIndex, value))
var change = metaIndex switch
{
ActorState.MetaIndex.Wetness => design.SetApplyWetness(value),
ActorState.MetaIndex.HatState => design.SetApplyHatVisible(value),
ActorState.MetaIndex.VisorState => design.SetApplyVisorToggle(value),
ActorState.MetaIndex.WeaponState => design.SetApplyWeaponVisible(value),
_ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null),
};
if (!change)
return;
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}.");
DesignChanged.Invoke(DesignChanged.Type.Other, design, new ApplicationTransaction(metaIndex, !value, value));
_event.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value));
}
/// <summary> Change the application value of a customize parameter. </summary>
public void ChangeApplyParameter(Design design, CustomizeParameterFlag flag, bool value)
/// <summary> Apply an entire design based on its appliance rules piece by piece. </summary>
public void ApplyDesign(Design design, DesignBase other)
{
if (!design.SetApplyParameter(flag, value))
return;
if (other.DoApplyWetness())
design.DesignData.SetIsWet(other.DesignData.IsWet());
if (other.DoApplyHatVisible())
design.DesignData.SetHatVisible(other.DesignData.IsHatVisible());
if (other.DoApplyVisorToggle())
design.DesignData.SetVisor(other.DesignData.IsVisorToggled());
if (other.DoApplyWeaponVisible())
design.DesignData.SetWeaponVisible(other.DesignData.IsWeaponVisible());
design.LastEdit = DateTimeOffset.UtcNow;
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of parameter {flag} to {value}.");
DesignChanged.Invoke(DesignChanged.Type.ApplyParameter, design, new ApplicationTransaction(flag, !value, value));
}
if (design.DesignData.IsHuman)
{
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
if (other.DoApplyCustomize(index))
ChangeCustomize(design, index, other.DesignData.Customize[index]);
}
/// <summary> Change multiple application values at once. </summary>
public void ChangeApplyMulti(Design design, bool? equipment, bool? customization, bool? bonus, bool? parameters, bool? meta, bool? stains,
bool? materials, bool? crest)
{
if (equipment is { } e)
foreach (var f in EquipSlotExtensions.FullSlots)
ChangeApplyItem(design, f, e);
if (stains is { } s)
foreach (var f in EquipSlotExtensions.FullSlots)
ChangeApplyStains(design, f, s);
if (customization is { } c)
foreach (var f in CustomizationExtensions.All.Where(design.CustomizeSet.IsAvailable).Prepend(CustomizeIndex.Clan)
.Prepend(CustomizeIndex.Gender))
ChangeApplyCustomize(design, f, c);
if (bonus is { } b)
foreach (var f in BonusExtensions.AllFlags)
ChangeApplyBonusItem(design, f, b);
if (meta is { } m)
foreach (var f in MetaExtensions.AllRelevant)
ChangeApplyMeta(design, f, m);
if (crest is { } cr)
foreach (var f in CrestExtensions.AllRelevantSet)
ChangeApplyCrest(design, f, cr);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
if (other.DoApplyEquip(slot))
ChangeEquip(design, slot, other.DesignData.Item(slot));
if (parameters is { } p)
foreach (var f in CustomizeParameterExtensions.AllFlags)
ChangeApplyParameter(design, f, p);
if (other.DoApplyStain(slot))
ChangeStain(design, slot, other.DesignData.Stain(slot));
}
}
if (materials is { } ma)
foreach (var (key, _) in design.GetMaterialData().ToArray())
ChangeApplyMaterialValue(design, MaterialValueIndex.FromKey(key), ma);
}
if (other.DoApplyEquip(EquipSlot.MainHand))
ChangeWeapon(design, EquipSlot.MainHand, other.DesignData.Item(EquipSlot.MainHand));
#endregion
if (other.DoApplyEquip(EquipSlot.OffHand))
ChangeWeapon(design, EquipSlot.OffHand, other.DesignData.Item(EquipSlot.OffHand));
public void UndoDesignChange(Design design)
{
if (!UndoStore.Remove(design.Identifier, out var otherData))
return;
if (other.DoApplyStain(EquipSlot.MainHand))
ChangeStain(design, EquipSlot.MainHand, other.DesignData.Stain(EquipSlot.MainHand));
var other = CreateTemporary();
other.SetDesignData(Customizations, otherData);
ApplyDesign(design, other);
if (other.DoApplyStain(EquipSlot.OffHand))
ChangeStain(design, EquipSlot.OffHand, other.DesignData.Stain(EquipSlot.OffHand));
}
private void MigrateOldDesigns()
{
if (!File.Exists(SaveService.FileNames.MigrationDesignFile))
if (!File.Exists(_saveService.FileNames.MigrationDesignFile))
return;
var errors = 0;
var skips = 0;
var successes = 0;
var oldDesigns = Designs.ToList();
var oldDesigns = _designs.ToList();
try
{
var text = File.ReadAllText(SaveService.FileNames.MigrationDesignFile);
var text = File.ReadAllText(_saveService.FileNames.MigrationDesignFile);
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(text) ?? new Dictionary<string, string>();
var migratedFileSystemPaths = new Dictionary<string, string>(dict.Count);
foreach (var (name, base64) in dict)
@ -517,14 +522,14 @@ public sealed class DesignManager : DesignEditor
try
{
var actualName = Path.GetFileName(name);
var design = new Design(Customizations, Items)
var design = new Design(_customizations, _items)
{
CreationDate = File.GetCreationTimeUtc(SaveService.FileNames.MigrationDesignFile),
LastEdit = File.GetLastWriteTimeUtc(SaveService.FileNames.MigrationDesignFile),
CreationDate = File.GetCreationTimeUtc(_saveService.FileNames.MigrationDesignFile),
LastEdit = File.GetLastWriteTimeUtc(_saveService.FileNames.MigrationDesignFile),
Identifier = CreateNewGuid(),
Name = actualName,
};
design.MigrateBase64(Customizations, Items, _humans, base64);
design.MigrateBase64(_items, _humans, base64);
if (!oldDesigns.Any(d => d.Name == design.Name && d.CreationDate == design.CreationDate))
{
Add(design, $"Migrated old design to {design.Identifier}.");
@ -545,24 +550,24 @@ public sealed class DesignManager : DesignEditor
}
}
DesignFileSystem.MigrateOldPaths(SaveService, migratedFileSystemPaths);
DesignFileSystem.MigrateOldPaths(_saveService, migratedFileSystemPaths);
Glamourer.Log.Information(
$"Successfully migrated {successes} old designs. Skipped {skips} already migrated designs. Failed to migrate {errors} designs.");
}
catch (Exception e)
{
Glamourer.Log.Error($"Could not migrate old design file {SaveService.FileNames.MigrationDesignFile}:\n{e}");
Glamourer.Log.Error($"Could not migrate old design file {_saveService.FileNames.MigrationDesignFile}:\n{e}");
}
try
{
File.Move(SaveService.FileNames.MigrationDesignFile,
Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak"), true);
Glamourer.Log.Information($"Moved migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file.");
File.Move(_saveService.FileNames.MigrationDesignFile,
Path.ChangeExtension(_saveService.FileNames.MigrationDesignFile, ".json.bak"));
Glamourer.Log.Information($"Moved migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file.");
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not move migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file:\n{ex}");
Glamourer.Log.Error($"Could not move migrated design file {_saveService.FileNames.MigrationDesignFile} to backup file:\n{ex}");
}
}
@ -592,7 +597,7 @@ public sealed class DesignManager : DesignEditor
{
try
{
var correctName = SaveService.FileNames.DesignFile(design);
var correctName = _saveService.FileNames.DesignFile(design);
File.Move(name, correctName, false);
Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
}
@ -612,7 +617,7 @@ public sealed class DesignManager : DesignEditor
while (true)
{
var guid = Guid.NewGuid();
if (!Designs.Contains(guid))
if (_designs.All(d => d.Identifier != guid))
return guid;
}
}
@ -622,17 +627,18 @@ public sealed class DesignManager : DesignEditor
/// Returns false if the design is already contained or if the identifier is already in use.
/// The design is treated as newly created and invokes an event.
/// </summary>
private void Add(Design design, string? message)
private bool Add(Design design, string? message)
{
if (Designs.Any(d => d == design || d.Identifier == design.Identifier))
return;
if (_designs.Any(d => d == design || d.Identifier == design.Identifier))
return false;
design.Index = Designs.Count;
Designs.Add(design);
design.Index = _designs.Count;
_designs.Add(design);
if (!message.IsNullOrEmpty())
Glamourer.Log.Debug(message);
SaveService.ImmediateSave(design);
DesignChanged.Invoke(DesignChanged.Type.Created, design, null);
_saveService.ImmediateSave(design);
_event.Invoke(DesignChanged.Type.Created, design);
return true;
}
/// <summary> Split a given string into its folder path and its name, if <paramref name="handlePath"/> is true. </summary>

View file

@ -1,18 +0,0 @@
using OtterGui.Services;
namespace Glamourer.Designs;
public class DesignStorage : List<Design>, IService
{
public bool TryGetValue(Guid identifier, [NotNullWhen(true)] out Design? design)
{
design = ByIdentifier(identifier);
return design != null;
}
public Design? ByIdentifier(Guid identifier)
=> this.FirstOrDefault(d => d.Identifier == identifier);
public bool Contains(Guid identifier)
=> ByIdentifier(identifier) != null;
}

View file

@ -1,185 +0,0 @@
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs.History;
/// <remarks> Only Designs. Can not be reverted. </remarks>
public readonly record struct CreationTransaction(string Name, string? Path)
: ITransaction
{
public ITransaction? Merge(ITransaction other)
=> null;
public void Revert(IDesignEditor editor, object data)
{ }
}
/// <remarks> Only Designs. </remarks>
public readonly record struct RenameTransaction(string Old, string New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is RenameTransaction other ? new RenameTransaction(other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).Rename((Design)data, Old);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct DescriptionTransaction(string Old, string New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is DescriptionTransaction other ? new DescriptionTransaction(other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).ChangeDescription((Design)data, Old);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct DesignColorTransaction(string Old, string New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is DesignColorTransaction other ? new DesignColorTransaction(other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).ChangeColor((Design)data, Old);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct TagAddedTransaction(string New, int Index)
: ITransaction
{
public ITransaction? Merge(ITransaction other)
=> null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).RemoveTag((Design)data, Index);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct TagRemovedTransaction(string Old, int Index)
: ITransaction
{
public ITransaction? Merge(ITransaction other)
=> null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).AddTag((Design)data, Old);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct TagChangedTransaction(string Old, string New, int IndexOld, int IndexNew)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is TagChangedTransaction other && other.IndexNew == IndexOld
? new TagChangedTransaction(other.Old, New, other.IndexOld, IndexNew)
: null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).RenameTag((Design)data, IndexNew, Old);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct ModAddedTransaction(Mod Mod, ModSettings Settings)
: ITransaction
{
public ITransaction? Merge(ITransaction other)
=> null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).RemoveMod((Design)data, Mod);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct ModRemovedTransaction(Mod Mod, ModSettings Settings)
: ITransaction
{
public ITransaction? Merge(ITransaction other)
=> null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).AddMod((Design)data, Mod, Settings);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct ModUpdatedTransaction(Mod Mod, ModSettings Old, ModSettings New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is ModUpdatedTransaction other && Mod == other.Mod ? new ModUpdatedTransaction(Mod, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).UpdateMod((Design)data, Mod, Old);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct MaterialTransaction(MaterialValueIndex Index, ColorRow? Old, ColorRow? New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is MaterialTransaction other && Index == other.Index ? new MaterialTransaction(Index, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
{
if (editor is DesignManager e)
e.ChangeMaterialValue((Design)data, Index, Old);
}
}
/// <remarks> Only Designs. </remarks>
public readonly record struct MaterialRevertTransaction(MaterialValueIndex Index, bool Old, bool New)
: ITransaction
{
public ITransaction? Merge(ITransaction other)
=> null;
public void Revert(IDesignEditor editor, object data)
=> ((DesignManager)editor).ChangeMaterialRevert((Design)data, Index, Old);
}
/// <remarks> Only Designs. </remarks>
public readonly record struct ApplicationTransaction(object Index, bool Old, bool New)
: ITransaction
{
public ITransaction? Merge(ITransaction other)
=> null;
public void Revert(IDesignEditor editor, object data)
{
var manager = (DesignManager)editor;
var design = (Design)data;
switch (Index)
{
case CustomizeIndex idx:
manager.ChangeApplyCustomize(design, idx, Old);
break;
case (EquipSlot slot, true):
manager.ChangeApplyStains(design, slot, Old);
break;
case (EquipSlot slot, _):
manager.ChangeApplyItem(design, slot, Old);
break;
case BonusItemFlag slot:
manager.ChangeApplyBonusItem(design, slot, Old);
break;
case CrestFlag slot:
manager.ChangeApplyCrest(design, slot, Old);
break;
case MetaIndex slot:
manager.ChangeApplyMeta(design, slot, Old);
break;
case CustomizeParameterFlag slot:
manager.ChangeApplyParameter(design, slot, Old);
break;
case MaterialValueIndex slot:
manager.ChangeApplyMaterialValue(design, slot, Old);
break;
}
}
}

View file

@ -1,191 +0,0 @@
using Glamourer.Api.Enums;
using Glamourer.Events;
using Glamourer.State;
using OtterGui.Services;
using Penumbra.GameData.Interop;
namespace Glamourer.Designs.History;
public class EditorHistory : IDisposable, IService
{
public const int MaxUndo = 16;
private sealed class Queue : IReadOnlyList<ITransaction>
{
private DateTime _lastAdd = DateTime.UtcNow;
private readonly ITransaction[] _data = new ITransaction[MaxUndo];
public int Offset { get; private set; }
public int Count { get; private set; }
public void Add(ITransaction transaction)
{
if (!TryMerge(transaction))
{
if (Count == MaxUndo)
{
_data[Offset] = transaction;
Offset = (Offset + 1) % MaxUndo;
}
else
{
if (Offset > 0)
{
_data[(Count + Offset) % MaxUndo] = transaction;
++Count;
}
else
{
_data[Count] = transaction;
++Count;
}
}
}
_lastAdd = DateTime.UtcNow;
}
private bool TryMerge(ITransaction newTransaction)
{
if (Count == 0)
return false;
var time = DateTime.UtcNow;
if (time - _lastAdd > TimeSpan.FromMilliseconds(250))
return false;
var lastIdx = (Offset + Count - 1) % MaxUndo;
if (newTransaction.Merge(_data[lastIdx]) is not { } transaction)
return false;
_data[lastIdx] = transaction;
return true;
}
public ITransaction? RemoveLast()
{
if (Count == 0)
return null;
--Count;
var idx = (Offset + Count) % MaxUndo;
return _data[idx];
}
public IEnumerator<ITransaction> GetEnumerator()
{
var end = Offset + (Offset + Count) % MaxUndo;
for (var i = Offset; i < end; ++i)
yield return _data[i];
end = Count - end;
for (var i = 0; i < end; ++i)
yield return _data[i];
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public ITransaction this[int index]
=> index < 0 || index >= Count
? throw new IndexOutOfRangeException()
: _data[(Offset + index) % MaxUndo];
}
private readonly DesignEditor _designEditor;
private readonly StateEditor _stateEditor;
private readonly DesignChanged _designChanged;
private readonly StateChanged _stateChanged;
private readonly Dictionary<ActorState, Queue> _stateEntries = [];
private readonly Dictionary<Design, Queue> _designEntries = [];
private bool _undoMode;
public EditorHistory(DesignManager designEditor, StateManager stateEditor, DesignChanged designChanged, StateChanged stateChanged)
{
_designEditor = designEditor;
_stateEditor = stateEditor;
_designChanged = designChanged;
_stateChanged = stateChanged;
_designChanged.Subscribe(OnDesignChanged, DesignChanged.Priority.EditorHistory);
_stateChanged.Subscribe(OnStateChanged, StateChanged.Priority.EditorHistory);
}
public void Dispose()
{
_designChanged.Unsubscribe(OnDesignChanged);
_stateChanged.Unsubscribe(OnStateChanged);
}
public bool CanUndo(ActorState state)
=> _stateEntries.TryGetValue(state, out var list) && list.Count > 0;
public bool CanUndo(Design design)
=> _designEntries.TryGetValue(design, out var list) && list.Count > 0;
public bool Undo(ActorState state)
{
if (!_stateEntries.TryGetValue(state, out var list) || list.Count == 0)
return false;
_undoMode = true;
list.RemoveLast()!.Revert(_stateEditor, state);
_undoMode = false;
return true;
}
public bool Undo(Design design)
{
if (!_designEntries.TryGetValue(design, out var list) || list.Count == 0)
return false;
_undoMode = true;
list.RemoveLast()!.Revert(_designEditor, design);
_undoMode = false;
return true;
}
private void AddStateTransaction(ActorState state, ITransaction transaction)
{
if (!_stateEntries.TryGetValue(state, out var list))
{
list = [];
_stateEntries.Add(state, list);
}
list.Add(transaction);
}
private void AddDesignTransaction(Design design, ITransaction transaction)
{
if (!_designEntries.TryGetValue(design, out var list))
{
list = [];
_designEntries.Add(design, list);
}
list.Add(transaction);
}
private void OnStateChanged(StateChangeType type, StateSource source, ActorState state, ActorData actors, ITransaction? data)
{
if (_undoMode || source is not StateSource.Manual)
return;
if (data is not null)
AddStateTransaction(state, data);
}
private void OnDesignChanged(DesignChanged.Type type, Design design, ITransaction? data)
{
if (_undoMode)
return;
if (data is not null)
AddDesignTransaction(design, data);
}
}

View file

@ -1,113 +0,0 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Glamourer.GameData;
namespace Glamourer.Designs.History;
public interface ITransaction
{
public ITransaction? Merge(ITransaction other);
public void Revert(IDesignEditor editor, object data);
}
public readonly record struct CustomizeTransaction(CustomizeIndex Slot, CustomizeValue Old, CustomizeValue New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is CustomizeTransaction other && Slot == other.Slot ? new CustomizeTransaction(Slot, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> editor.ChangeCustomize(data, Slot, Old, ApplySettings.Manual);
}
public readonly record struct EntireCustomizeTransaction(CustomizeFlag Apply, CustomizeArray Old, CustomizeArray New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is EntireCustomizeTransaction other ? new EntireCustomizeTransaction(Apply | other.Apply, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> editor.ChangeEntireCustomize(data, Old, Apply, ApplySettings.Manual);
}
public readonly record struct EquipTransaction(EquipSlot Slot, EquipItem Old, EquipItem New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is EquipTransaction other && Slot == other.Slot ? new EquipTransaction(Slot, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> editor.ChangeItem(data, Slot, Old, ApplySettings.Manual);
}
public readonly record struct BonusItemTransaction(BonusItemFlag Slot, EquipItem Old, EquipItem New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is BonusItemTransaction other && Slot == other.Slot ? new BonusItemTransaction(Slot, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> editor.ChangeBonusItem(data, Slot, Old, ApplySettings.Manual);
}
public readonly record struct WeaponTransaction(
EquipItem OldMain,
EquipItem OldOff,
EquipItem OldGauntlets,
EquipItem NewMain,
EquipItem NewOff,
EquipItem NewGauntlets)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is WeaponTransaction other
? new WeaponTransaction(other.OldMain, other.OldOff, other.OldGauntlets, NewMain, NewOff, NewGauntlets)
: null;
public void Revert(IDesignEditor editor, object data)
{
editor.ChangeItem(data, EquipSlot.MainHand, OldMain, ApplySettings.Manual);
editor.ChangeItem(data, EquipSlot.OffHand, OldOff, ApplySettings.Manual);
editor.ChangeItem(data, EquipSlot.Hands, OldGauntlets, ApplySettings.Manual);
}
}
public readonly record struct StainTransaction(EquipSlot Slot, StainIds Old, StainIds New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is StainTransaction other && Slot == other.Slot ? new StainTransaction(Slot, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> editor.ChangeStains(data, Slot, Old, ApplySettings.Manual);
}
public readonly record struct CrestTransaction(CrestFlag Slot, bool Old, bool New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is CrestTransaction other && Slot == other.Slot ? new CrestTransaction(Slot, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> editor.ChangeCrest(data, Slot, Old, ApplySettings.Manual);
}
public readonly record struct ParameterTransaction(CustomizeParameterFlag Slot, CustomizeParameterValue Old, CustomizeParameterValue New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> older is ParameterTransaction other && Slot == other.Slot ? new ParameterTransaction(Slot, other.Old, New) : null;
public void Revert(IDesignEditor editor, object data)
=> editor.ChangeCustomizeParameter(data, Slot, Old, ApplySettings.Manual);
}
public readonly record struct MetaTransaction(MetaIndex Slot, bool Old, bool New)
: ITransaction
{
public ITransaction? Merge(ITransaction older)
=> null;
public void Revert(IDesignEditor editor, object data)
=> editor.ChangeMetaState(data, Slot, Old, ApplySettings.Manual);
}

View file

@ -1,92 +0,0 @@
using Glamourer.Designs.Links;
using Glamourer.GameData;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public readonly record struct ApplySettings(
uint Key = 0,
StateSource Source = StateSource.Manual,
bool RespectManual = false,
bool FromJobChange = false,
bool UseSingleSource = false,
bool MergeLinks = false,
bool ResetMaterials = false,
bool IsFinal = false)
{
public static readonly ApplySettings Manual = new()
{
Key = 0,
Source = StateSource.Manual,
FromJobChange = false,
RespectManual = false,
UseSingleSource = false,
MergeLinks = false,
ResetMaterials = false,
IsFinal = false,
};
public static readonly ApplySettings ManualWithLinks = new()
{
Key = 0,
Source = StateSource.Manual,
FromJobChange = false,
RespectManual = false,
UseSingleSource = false,
MergeLinks = true,
ResetMaterials = false,
IsFinal = false,
};
public static readonly ApplySettings Game = new()
{
Key = 0,
Source = StateSource.Game,
FromJobChange = false,
RespectManual = false,
UseSingleSource = false,
MergeLinks = false,
ResetMaterials = true,
IsFinal = false,
};
}
public interface IDesignEditor
{
/// <summary> Change a customization value. </summary>
public void ChangeCustomize(object data, CustomizeIndex idx, CustomizeValue value, ApplySettings settings = default);
/// <summary> Change an entire customize array according to the given flags. </summary>
public void ChangeEntireCustomize(object data, in CustomizeArray customizeInput, CustomizeFlag apply, ApplySettings settings = default);
/// <summary> Change a customize parameter. </summary>
public void ChangeCustomizeParameter(object data, CustomizeParameterFlag flag, CustomizeParameterValue v, ApplySettings settings = default);
/// <summary> Change an equipment piece. </summary>
public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default)
=> ChangeEquip(data, slot, item, null, settings);
/// <summary> Change a bonus item. </summary>
public void ChangeBonusItem(object data, BonusItemFlag slot, EquipItem item, ApplySettings settings = default);
/// <summary> Change the stain for any equipment piece. </summary>
public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings = default)
=> ChangeEquip(data, slot, null, stains, settings);
/// <summary> Change an equipment piece and its stain at the same time. </summary>
public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings settings = default);
/// <summary> Change the crest visibility for any equipment piece. </summary>
public void ChangeCrest(object data, CrestFlag slot, bool crest, ApplySettings settings = default);
/// <summary> Change the bool value of one of the meta flags. </summary>
public void ChangeMetaState(object data, MetaIndex slot, bool value, ApplySettings settings = default);
/// <summary> Change all values applies from the given design. </summary>
public void ApplyDesign(object data, MergedDesign design, ApplySettings settings = default);
/// <summary> Change all values applies from the given design. </summary>
public void ApplyDesign(object data, DesignBase design, ApplySettings settings = default);
}

View file

@ -1,31 +0,0 @@
using Glamourer.Automation;
using Glamourer.Interop.Material;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public interface IDesignStandIn : IEquatable<IDesignStandIn>
{
public string ResolveName(bool incognito);
public ref readonly DesignData GetDesignData(in DesignData baseRef);
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData();
public string SerializeName();
public StateSource AssociatedSource();
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication);
public void AddData(JObject jObj);
public void ParseData(JObject jObj);
public bool ChangeData(object data);
public bool ForcedRedraw { get; }
public bool ResetAdvancedDyes { get; }
public bool ResetTemporarySettings { get; }
}

View file

@ -1,19 +0,0 @@
using Glamourer.Automation;
namespace Glamourer.Designs.Links;
public record struct DesignLink(Design Link, ApplicationType Type);
public readonly record struct LinkData(Guid Identity, ApplicationType Type, LinkOrder Order)
{
public override string ToString()
=> Identity.ToString();
}
public enum LinkOrder : byte
{
Self,
After,
Before,
None,
};

View file

@ -1,28 +0,0 @@
using Dalamud.Interface.ImGuiNotification;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Services;
using Notification = OtterGui.Classes.Notification;
namespace Glamourer.Designs.Links;
public sealed class DesignLinkLoader(DesignStorage designStorage, MessageService messager)
: DelayedReferenceLoader<Design, LinkData>(messager), IService
{
protected override bool TryGetObject(LinkData data, [NotNullWhen(true)] out Design? obj)
=> designStorage.FindFirst(d => d.Identifier == data.Identity, out obj);
protected override bool SetObject(Design parent, Design child, LinkData data, out string error)
=> LinkContainer.AddLink(parent, child, data.Type, data.Order, out error);
protected override void HandleChildNotFound(Design parent, LinkData data)
{
Messager.AddMessage(new Notification(
$"Could not find the design {data.Identity}. If this design was deleted, please re-save {parent.Identifier}.",
NotificationType.Warning));
}
protected override void HandleChildNotSet(Design parent, Design child, string error)
=> Messager.AddMessage(new Notification($"Could not link {child.Identifier} to {parent.Identifier}: {error}",
NotificationType.Warning));
}

View file

@ -1,86 +0,0 @@
using Glamourer.Automation;
using Glamourer.Designs.History;
using Glamourer.Events;
using Glamourer.Services;
using OtterGui.Services;
namespace Glamourer.Designs.Links;
public sealed class DesignLinkManager : IService, IDisposable
{
private readonly DesignStorage _storage;
private readonly DesignChanged _event;
private readonly SaveService _saveService;
public DesignLinkManager(DesignStorage storage, DesignChanged @event, SaveService saveService)
{
_storage = storage;
_event = @event;
_saveService = saveService;
_event.Subscribe(OnDesignChanged, DesignChanged.Priority.DesignLinkManager);
}
public void Dispose()
=> _event.Unsubscribe(OnDesignChanged);
public void MoveDesignLink(Design parent, int idxFrom, LinkOrder orderFrom, int idxTo, LinkOrder orderTo)
{
if (!parent.Links.Reorder(idxFrom, orderFrom, idxTo, orderTo))
return;
parent.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(parent);
Glamourer.Log.Debug($"Moved link from {orderFrom} {idxFrom} to {idxTo} {orderTo}.");
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
}
public void AddDesignLink(Design parent, Design child, LinkOrder order)
{
if (!LinkContainer.AddLink(parent, child, ApplicationType.All, order, out _))
return;
parent.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(parent);
Glamourer.Log.Debug($"Added new {order} link to {child.Identifier} for {parent.Identifier}.");
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
}
public void RemoveDesignLink(Design parent, int idx, LinkOrder order)
{
if (!parent.Links.Remove(idx, order))
return;
parent.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(parent);
Glamourer.Log.Debug($"Removed the {order} link at {idx} for {parent.Identifier}.");
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
}
public void ChangeApplicationType(Design parent, int idx, LinkOrder order, ApplicationType applicationType)
{
applicationType &= ApplicationType.All;
if (!parent.Links.ChangeApplicationRules(idx, order, applicationType, out var old))
return;
_saveService.QueueSave(parent);
Glamourer.Log.Debug($"Changed link application type from {old} to {applicationType} for design link {order} {idx + 1} in design {parent.Identifier}.");
_event.Invoke(DesignChanged.Type.ChangedLink, parent, null);
}
private void OnDesignChanged(DesignChanged.Type type, Design deletedDesign, ITransaction? _)
{
if (type is not DesignChanged.Type.Deleted)
return;
foreach (var design in _storage)
{
if (!design.Links.Remove(deletedDesign))
continue;
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Removed {deletedDesign.Identifier} from {design.Identifier} links due to deletion.");
_saveService.QueueSave(design);
}
}
}

View file

@ -1,328 +0,0 @@
using Glamourer.Api.Enums;
using Glamourer.Automation;
using Glamourer.Designs.Special;
using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Unlocks;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs.Links;
public class DesignMerger(
DesignManager designManager,
CustomizeService _customize,
Configuration _config,
ItemUnlockManager _itemUnlocks,
CustomizeUnlockManager _customizeUnlocks) : IService
{
public MergedDesign Merge(LinkContainer designs, in CustomizeArray currentCustomize, in DesignData baseRef, bool respectOwnership,
bool modAssociations)
=> Merge(designs.Select(d => ((IDesignStandIn)d.Link, d.Type, JobFlag.All)), currentCustomize, baseRef, respectOwnership,
modAssociations);
public MergedDesign Merge(IEnumerable<(IDesignStandIn, ApplicationType, JobFlag)> designs, in CustomizeArray currentCustomize,
in DesignData baseRef, bool respectOwnership, bool modAssociations)
{
var ret = new MergedDesign(designManager);
ret.Design.SetCustomize(_customize, currentCustomize);
var startBodyType = currentCustomize.BodyType;
CustomizeFlag fixFlags = 0;
respectOwnership &= _config.UnlockedItemMode;
foreach (var (design, type, jobs) in designs)
{
if (type is 0)
continue;
ref readonly var data = ref design.GetDesignData(baseRef);
var source = design.AssociatedSource();
if (!data.IsHuman)
continue;
var collection = type.ApplyWhat(design);
ReduceMeta(data, collection.Meta, ret, source);
ReduceCustomize(data, collection.Customize, ref fixFlags, ret, source, respectOwnership, startBodyType);
ReduceEquip(data, collection.Equip, ret, source, respectOwnership);
ReduceBonusItems(data, collection.BonusItem, ret, source, respectOwnership);
ReduceMainhands(data, jobs, collection.Equip, ret, source, respectOwnership);
ReduceOffhands(data, jobs, collection.Equip, ret, source, respectOwnership);
ReduceCrests(data, collection.Crest, ret, source);
ReduceParameters(data, collection.Parameters, ret, source);
ReduceMods(design as Design, ret, modAssociations);
if (type.HasFlag(ApplicationType.GearCustomization))
ReduceMaterials(design, ret);
if (design.ForcedRedraw)
ret.ForcedRedraw = true;
if (design.ResetAdvancedDyes)
ret.ResetAdvancedDyes = true;
if (design.ResetTemporarySettings)
ret.ResetTemporarySettings = true;
}
ApplyFixFlags(ret, fixFlags);
return ret;
}
private static void ReduceMaterials(IDesignStandIn designStandIn, MergedDesign ret)
{
if (designStandIn is not DesignBase design)
return;
var materials = ret.Design.GetMaterialDataRef();
foreach (var (key, value) in design.Materials.Where(p => p.Item2.Enabled))
materials.TryAddValue(MaterialValueIndex.FromKey(key), value);
}
private static void ReduceMods(Design? design, MergedDesign ret, bool modAssociations)
{
if (design == null || !modAssociations)
return;
foreach (var (mod, settings) in design.AssociatedMods)
ret.AssociatedMods.TryAdd(mod, settings);
}
private static void ReduceMeta(in DesignData design, MetaFlag applyMeta, MergedDesign ret, StateSource source)
{
applyMeta &= ~ret.Design.Application.Meta;
if (applyMeta == 0)
return;
foreach (var index in MetaExtensions.AllRelevant)
{
if (!applyMeta.HasFlag(index.ToFlag()))
continue;
ret.Design.SetApplyMeta(index, true);
ret.Design.GetDesignDataRef().SetMeta(index, design.GetMeta(index));
ret.Sources[index] = source;
}
}
private static void ReduceCrests(in DesignData design, CrestFlag crestFlags, MergedDesign ret, StateSource source)
{
crestFlags &= ~ret.Design.Application.Crest;
if (crestFlags == 0)
return;
foreach (var slot in CrestExtensions.AllRelevantSet)
{
if (!crestFlags.HasFlag(slot))
continue;
ret.Design.GetDesignDataRef().SetCrest(slot, design.Crest(slot));
ret.Design.SetApplyCrest(slot, true);
ret.Sources[slot] = source;
}
}
private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret,
StateSource source)
{
parameterFlags &= ~ret.Design.Application.Parameters;
if (parameterFlags == 0)
return;
foreach (var flag in CustomizeParameterExtensions.AllFlags)
{
if (!parameterFlags.HasFlag(flag))
continue;
ret.Design.GetDesignDataRef().Parameters.Set(flag, design.Parameters[flag]);
ret.Design.SetApplyParameter(flag, true);
ret.Sources[flag] = source;
}
}
private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source,
bool respectOwnership)
{
equipFlags &= ~ret.Design.Application.Equip;
if (equipFlags == 0)
return;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var flag = slot.ToFlag();
if (equipFlags.HasFlag(flag))
{
var item = design.Item(slot);
if (!respectOwnership || _itemUnlocks.IsUnlocked(item.Id, out _))
ret.Design.GetDesignDataRef().SetItem(slot, item);
ret.Design.SetApplyEquip(slot, true);
ret.Sources[slot, false] = source;
}
var stainFlag = slot.ToStainFlag();
if (equipFlags.HasFlag(stainFlag))
{
ret.Design.GetDesignDataRef().SetStain(slot, design.Stain(slot));
ret.Design.SetApplyStain(slot, true);
ret.Sources[slot, true] = source;
}
}
foreach (var slot in EquipSlotExtensions.WeaponSlots)
{
var stainFlag = slot.ToStainFlag();
if (equipFlags.HasFlag(stainFlag))
{
ret.Design.GetDesignDataRef().SetStain(slot, design.Stain(slot));
ret.Design.SetApplyStain(slot, true);
ret.Sources[slot, true] = source;
}
}
}
private void ReduceBonusItems(in DesignData design, BonusItemFlag bonusItems, MergedDesign ret, StateSource source, bool respectOwnership)
{
bonusItems &= ~ret.Design.Application.BonusItem;
if (bonusItems == 0)
return;
foreach (var slot in BonusExtensions.AllFlags.Where(b => bonusItems.HasFlag(b)))
{
var item = design.BonusItem(slot);
if (!respectOwnership || true) // TODO: maybe check unlocks
ret.Design.GetDesignDataRef().SetBonusItem(slot, item);
ret.Design.SetApplyBonusItem(slot, true);
ret.Sources[slot] = source;
}
}
private void ReduceMainhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source,
bool respectOwnership)
{
if (!equipFlags.HasFlag(EquipFlag.Mainhand))
return;
var weapon = design.Item(EquipSlot.MainHand);
if (respectOwnership && !_itemUnlocks.IsUnlocked(weapon.Id, out _))
return;
if (!ret.Design.DoApplyEquip(EquipSlot.MainHand))
{
ret.Design.SetApplyEquip(EquipSlot.MainHand, true);
ret.Design.GetDesignDataRef().SetItem(EquipSlot.MainHand, weapon);
}
ret.Weapons.TryAdd(weapon.Type, weapon, source, allowedJobs);
}
private void ReduceOffhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source,
bool respectOwnership)
{
if (!equipFlags.HasFlag(EquipFlag.Offhand))
return;
var weapon = design.Item(EquipSlot.OffHand);
if (respectOwnership && !_itemUnlocks.IsUnlocked(weapon.Id, out _))
return;
if (!ret.Design.DoApplyEquip(EquipSlot.OffHand))
{
ret.Design.SetApplyEquip(EquipSlot.OffHand, true);
ret.Design.GetDesignDataRef().SetItem(EquipSlot.OffHand, weapon);
}
if (weapon.Valid)
ret.Weapons.TryAdd(weapon.Type, weapon, source, allowedJobs);
}
private void ReduceCustomize(in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag fixFlags, MergedDesign ret,
StateSource source, bool respectOwnership, CustomizeValue startBodyType)
{
customizeFlags &= ~ret.Design.ApplyCustomizeExcludingBodyType;
if (ret.Design.DesignData.Customize.BodyType != startBodyType)
customizeFlags &= ~CustomizeFlag.BodyType;
if (customizeFlags == 0)
return;
// Skip anything not human.
if (!ret.Design.DesignData.IsHuman || !design.IsHuman)
return;
var customize = ret.Design.DesignData.Customize;
if (customizeFlags.HasFlag(CustomizeFlag.Clan))
{
fixFlags |= _customize.ChangeClan(ref customize, design.Customize.Clan);
ret.Design.SetApplyCustomize(CustomizeIndex.Clan, true);
ret.Design.SetApplyCustomize(CustomizeIndex.Race, true);
customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race);
ret.Sources[CustomizeIndex.Clan] = source;
ret.Sources[CustomizeIndex.Race] = source;
}
if (customizeFlags.HasFlag(CustomizeFlag.Gender))
{
fixFlags |= _customize.ChangeGender(ref customize, design.Customize.Gender);
ret.Design.SetApplyCustomize(CustomizeIndex.Gender, true);
customizeFlags &= ~CustomizeFlag.Gender;
ret.Sources[CustomizeIndex.Gender] = source;
}
if (customizeFlags.HasFlag(CustomizeFlag.Face))
{
customize[CustomizeIndex.Face] = design.Customize.Face;
ret.Design.SetApplyCustomize(CustomizeIndex.Face, true);
customizeFlags &= ~CustomizeFlag.Face;
ret.Sources[CustomizeIndex.Face] = source;
}
if (customizeFlags.HasFlag(CustomizeFlag.BodyType))
{
customize[CustomizeIndex.BodyType] = design.Customize.BodyType;
customizeFlags &= ~CustomizeFlag.BodyType;
ret.Sources[CustomizeIndex.BodyType] = source;
}
var set = _customize.Manager.GetSet(customize.Clan, customize.Gender);
var face = customize.Face;
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
var flag = index.ToFlag();
if (!customizeFlags.HasFlag(flag))
continue;
var value = design.Customize[index];
if (!CustomizeService.IsCustomizationValid(set, face, index, value, out var data))
continue;
if (data.HasValue && respectOwnership && !_customizeUnlocks.IsUnlocked(data.Value, out _))
continue;
customize[index] = data?.Value ?? value;
ret.Design.SetApplyCustomize(index, true);
ret.Sources[index] = source;
fixFlags &= ~flag;
}
ret.Design.SetCustomize(_customize, customize);
}
private static void ApplyFixFlags(MergedDesign ret, CustomizeFlag fixFlags)
{
if (fixFlags == 0)
return;
var source = ret.Design.DoApplyCustomize(CustomizeIndex.Clan)
? ret.Sources[CustomizeIndex.Clan]
: ret.Sources[CustomizeIndex.Gender];
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
var flag = index.ToFlag();
if (!fixFlags.HasFlag(flag))
continue;
ret.Sources[index] = source;
ret.Design.SetApplyCustomize(index, true);
}
}
}

View file

@ -1,205 +0,0 @@
using Glamourer.Automation;
using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
namespace Glamourer.Designs.Links;
public sealed class LinkContainer : List<DesignLink>
{
public List<DesignLink> Before
=> this;
public readonly List<DesignLink> After = [];
public new int Count
=> base.Count + After.Count;
public LinkContainer Clone()
{
var ret = new LinkContainer();
ret.EnsureCapacity(base.Count);
ret.After.EnsureCapacity(After.Count);
ret.AddRange(this);
ret.After.AddRange(After);
return ret;
}
public bool Reorder(int fromIndex, LinkOrder fromOrder, int toIndex, LinkOrder toOrder)
{
var fromList = fromOrder switch
{
LinkOrder.Before => Before,
LinkOrder.After => After,
_ => throw new ArgumentException("Invalid link order."),
};
var toList = toOrder switch
{
LinkOrder.Before => Before,
LinkOrder.After => After,
_ => throw new ArgumentException("Invalid link order."),
};
if (fromList == toList)
return fromList.Move(fromIndex, toIndex);
if (fromIndex < 0 || fromIndex >= fromList.Count)
return false;
toIndex = Math.Clamp(toIndex, 0, toList.Count);
toList.Insert(toIndex, fromList[fromIndex]);
fromList.RemoveAt(fromIndex);
return true;
}
public bool Remove(int idx, LinkOrder order)
{
var list = order switch
{
LinkOrder.Before => Before,
LinkOrder.After => After,
_ => throw new ArgumentException("Invalid link order."),
};
if (idx < 0 || idx >= list.Count)
return false;
list.RemoveAt(idx);
return true;
}
public bool ChangeApplicationRules(int idx, LinkOrder order, ApplicationType type, out ApplicationType old)
{
var list = order switch
{
LinkOrder.Before => Before,
LinkOrder.After => After,
_ => throw new ArgumentException("Invalid link order."),
};
old = list[idx].Type;
if (idx < 0 || idx >= list.Count || old == type)
return false;
list[idx] = list[idx] with { Type = type };
return true;
}
public static bool CanAddLink(Design parent, Design child, LinkOrder order, out string error)
{
if (parent == child)
{
error = $"Can not link {parent.Incognito} with itself.";
return false;
}
if (parent.Links.Contains(child))
{
error = $"Design {parent.Incognito} already contains a direct link to {child.Incognito}.";
return false;
}
if (GetAllLinks(parent).Any(l => l.Link.Link == child && l.Order != order))
{
error =
$"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the parent already links to the child in the opposite direction.";
return false;
}
if (GetAllLinks(child).Any(l => l.Link.Link == parent && l.Order == order))
{
error =
$"Adding {child.Incognito} to {parent.Incognito}s links would create a circle, the child already links to the parent in the opposite direction.";
return false;
}
error = string.Empty;
return true;
}
public static bool AddLink(Design parent, Design child, ApplicationType type, LinkOrder order, out string error)
{
if (!CanAddLink(parent, child, order, out error))
return false;
var list = order switch
{
LinkOrder.Before => parent.Links.Before,
LinkOrder.After => parent.Links.After,
_ => null,
};
if (list == null)
{
error = $"Order {order} is invalid.";
return false;
}
type &= ApplicationType.All;
list.Add(new DesignLink(child, type));
error = string.Empty;
return true;
}
public bool Contains(Design child)
=> Before.Any(l => l.Link == child) || After.Any(l => l.Link == child);
public bool Remove(Design child)
=> Before.RemoveAll(l => l.Link == child) + After.RemoveAll(l => l.Link == child) > 0;
public static IEnumerable<(DesignLink Link, LinkOrder Order)> GetAllLinks(Design design)
{
var set = new HashSet<Design>(design.Links.Count * 4);
return GetAllLinks(new DesignLink(design, ApplicationType.All), LinkOrder.Self, set);
}
private static IEnumerable<(DesignLink Link, LinkOrder Order)> GetAllLinks(DesignLink design, LinkOrder currentOrder, ISet<Design> visited)
{
if (design.Link.Links.Count == 0)
{
if (visited.Add(design.Link))
yield return (design, currentOrder);
yield break;
}
foreach (var link in design.Link.Links.Before
.Where(l => !visited.Contains(l.Link))
.SelectMany(l => GetAllLinks(l, currentOrder == LinkOrder.After ? LinkOrder.After : LinkOrder.Before, visited)))
yield return link;
if (visited.Add(design.Link))
yield return (design, currentOrder);
foreach (var link in design.Link.Links.After.Where(l => !visited.Contains(l.Link))
.SelectMany(l => GetAllLinks(l, currentOrder == LinkOrder.Before ? LinkOrder.Before : LinkOrder.After, visited)))
yield return link;
}
public JObject Serialize()
{
var before = new JArray();
foreach (var link in Before)
{
before.Add(new JObject
{
["Design"] = link.Link.Identifier,
["Type"] = (uint)link.Type,
});
}
var after = new JArray();
foreach (var link in After)
{
after.Add(new JObject
{
["Design"] = link.Link.Identifier,
["Type"] = (uint)link.Type,
});
}
return new JObject
{
[nameof(Before)] = before,
[nameof(After)] = after,
};
}
}

View file

@ -1,105 +0,0 @@
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs.Links;
public readonly struct WeaponList
{
private readonly Dictionary<FullEquipType, List<(EquipItem, StateSource, JobFlag)>> _list = new(4);
public IEnumerable<(EquipItem, StateSource, JobFlag)> Values
=> _list.Values.SelectMany(t => t);
public void Clear()
=> _list.Clear();
public bool TryAdd(FullEquipType type, EquipItem item, StateSource source, JobFlag flags)
{
if (!_list.TryGetValue(type, out var list))
{
list = new List<(EquipItem, StateSource, JobFlag)>(2);
_list.Add(type, list);
}
var existingFlags = list.Count == 0 ? 0 : list.Select(t => t.Item3).Aggregate((t, existing) => t | existing);
var remainingFlags = flags & ~existingFlags;
if (remainingFlags == 0)
return false;
list.Add((item, source, remainingFlags));
return true;
}
public bool TryGet(FullEquipType type, JobId id, bool gameStateAllowed, out (EquipItem, StateSource) ret)
{
if (!_list.TryGetValue(type, out var list))
{
ret = default;
return false;
}
var flag = (JobFlag)(1ul << id.Id);
foreach (var (item, source, flags) in list)
{
if (flags.HasFlag(flag) && (gameStateAllowed || source is not StateSource.Game))
{
ret = (item, source);
return true;
}
}
ret = default;
return false;
}
public WeaponList()
{ }
}
public sealed class MergedDesign
{
public MergedDesign(DesignManager designManager)
{
Design = designManager.CreateTemporary();
Design.Application = ApplicationCollection.None;
}
public MergedDesign(DesignBase design)
{
Design = design;
if (design.DoApplyEquip(EquipSlot.MainHand))
{
var weapon = design.DesignData.Item(EquipSlot.MainHand);
if (weapon.Valid)
Weapons.TryAdd(weapon.Type, weapon, StateSource.Manual, JobFlag.All);
}
if (design.DoApplyEquip(EquipSlot.OffHand))
{
var weapon = design.DesignData.Item(EquipSlot.OffHand);
if (weapon.Valid)
Weapons.TryAdd(weapon.Type, weapon, StateSource.Manual, JobFlag.All);
}
ForcedRedraw = design is IDesignStandIn { ForcedRedraw: true };
}
public MergedDesign(Design design)
: this((DesignBase)design)
{
foreach (var (mod, settings) in design.AssociatedMods)
AssociatedMods[mod] = settings;
}
public readonly DesignBase Design;
public readonly WeaponList Weapons = new();
public readonly SortedList<Mod, ModSettings> AssociatedMods = [];
public StateSources Sources = new();
public bool ForcedRedraw;
public bool ResetAdvancedDyes;
public bool ResetTemporarySettings;
}

View file

@ -1,80 +0,0 @@
using Glamourer.Api.Enums;
using Glamourer.State;
namespace Glamourer.Designs;
public enum MetaIndex
{
Wetness = StateIndex.MetaWetness,
HatState = StateIndex.MetaHatState,
VisorState = StateIndex.MetaVisorState,
WeaponState = StateIndex.MetaWeaponState,
ModelId = StateIndex.MetaModelId,
EarState = StateIndex.MetaEarState,
}
public static class MetaExtensions
{
public static readonly IReadOnlyList<MetaIndex> AllRelevant =
[MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState, MetaIndex.EarState];
public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState;
public static MetaFlag ToFlag(this MetaIndex index)
=> index switch
{
MetaIndex.Wetness => MetaFlag.Wetness,
MetaIndex.HatState => MetaFlag.HatState,
MetaIndex.VisorState => MetaFlag.VisorState,
MetaIndex.WeaponState => MetaFlag.WeaponState,
MetaIndex.EarState => MetaFlag.EarState,
_ => (MetaFlag)byte.MaxValue,
};
public static MetaIndex ToIndex(this MetaFlag index)
=> index switch
{
MetaFlag.Wetness => MetaIndex.Wetness,
MetaFlag.HatState => MetaIndex.HatState,
MetaFlag.VisorState => MetaIndex.VisorState,
MetaFlag.WeaponState => MetaIndex.WeaponState,
MetaFlag.EarState => MetaIndex.EarState,
_ => (MetaIndex)byte.MaxValue,
};
public static IEnumerable<MetaIndex> ToIndices(this MetaFlag index)
{
if (index.HasFlag(MetaFlag.Wetness))
yield return MetaIndex.Wetness;
if (index.HasFlag(MetaFlag.HatState))
yield return MetaIndex.HatState;
if (index.HasFlag(MetaFlag.VisorState))
yield return MetaIndex.VisorState;
if (index.HasFlag(MetaFlag.WeaponState))
yield return MetaIndex.WeaponState;
if (index.HasFlag(MetaFlag.EarState))
yield return MetaIndex.EarState;
}
public static string ToName(this MetaIndex index)
=> index switch
{
MetaIndex.HatState => "Hat Visible",
MetaIndex.VisorState => "Visor Toggled",
MetaIndex.WeaponState => "Weapon Visible",
MetaIndex.Wetness => "Force Wetness",
MetaIndex.EarState => "Ears Visible",
_ => "Unknown Meta",
};
public static string ToTooltip(this MetaIndex index)
=> index switch
{
MetaIndex.HatState => "Hide or show the characters head gear.",
MetaIndex.VisorState => "Toggle the visor state of the characters head gear.",
MetaIndex.WeaponState => "Hide or show the characters weapons when not drawn.",
MetaIndex.Wetness => "Force the character to be wet or not.",
MetaIndex.EarState => "Hide or show the characters ears through the head gear. (Viera only)",
_ => string.Empty,
};
}

View file

@ -1,62 +0,0 @@
using Glamourer.Automation;
using Glamourer.Gui;
using Glamourer.Interop.Material;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using OtterGui.Services;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs.Special;
public class QuickSelectedDesign(QuickDesignCombo combo) : IDesignStandIn, IService
{
public const string SerializedName = "//QuickSelection";
public const string ResolvedName = "Quick Design Bar Selection";
public bool Equals(IDesignStandIn? other)
=> other is QuickSelectedDesign;
public string ResolveName(bool incognito)
=> ResolvedName;
public Design? CurrentDesign
=> combo.Design as Design;
public ref readonly DesignData GetDesignData(in DesignData baseRef)
{
if (combo.Design != null)
return ref combo.Design.GetDesignData(baseRef);
return ref baseRef;
}
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData()
=> combo.Design?.GetMaterialData() ?? [];
public string SerializeName()
=> SerializedName;
public StateSource AssociatedSource()
=> StateSource.Manual;
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
=> combo.Design?.AllLinks(newApplication) ?? [];
public void AddData(JObject jObj)
{ }
public void ParseData(JObject jObj)
{ }
public bool ChangeData(object data)
=> false;
public bool ForcedRedraw
=> combo.Design?.ForcedRedraw ?? false;
public bool ResetAdvancedDyes
=> combo.Design?.ResetAdvancedDyes ?? false;
public bool ResetTemporarySettings
=> combo.Design?.ResetTemporarySettings ?? false;
}

View file

@ -1,102 +0,0 @@
using Glamourer.Automation;
using Glamourer.Interop.Material;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs.Special;
public class RandomDesign(RandomDesignGenerator rng) : IDesignStandIn
{
public const string SerializedName = "//Random";
public const string ResolvedName = "Random";
private Design? _currentDesign;
public IReadOnlyList<IDesignPredicate> Predicates { get; private set; } = [];
public bool ResetOnRedraw { get; set; } = false;
public string ResolveName(bool _)
=> ResolvedName;
public ref readonly DesignData GetDesignData(in DesignData baseRef)
{
_currentDesign ??= rng.Design(Predicates);
if (_currentDesign == null)
return ref baseRef;
return ref _currentDesign.GetDesignDataRef();
}
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData()
{
_currentDesign ??= rng.Design(Predicates);
if (_currentDesign == null)
return [];
return _currentDesign.Materials;
}
public string SerializeName()
=> SerializedName;
public bool Equals(IDesignStandIn? other)
=> other is RandomDesign r
&& r.ResetOnRedraw == ResetOnRedraw
&& string.Equals(RandomPredicate.GeneratePredicateString(r.Predicates), RandomPredicate.GeneratePredicateString(Predicates),
StringComparison.OrdinalIgnoreCase);
public StateSource AssociatedSource()
=> StateSource.Manual;
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool newApplication)
{
if (newApplication || ResetOnRedraw)
_currentDesign = rng.Design(Predicates);
else
_currentDesign ??= rng.Design(Predicates);
if (_currentDesign == null)
yield break;
foreach (var (link, type, jobs) in _currentDesign.AllLinks(newApplication))
yield return (link, type, jobs);
}
public void AddData(JObject jObj)
{
jObj["Restrictions"] = RandomPredicate.GeneratePredicateString(Predicates);
jObj["ResetOnRedraw"] = ResetOnRedraw;
}
public void ParseData(JObject jObj)
{
var restrictions = jObj["Restrictions"]?.ToObject<string>() ?? string.Empty;
Predicates = RandomPredicate.GeneratePredicates(restrictions);
ResetOnRedraw = jObj["ResetOnRedraw"]?.ToObject<bool>() ?? false;
}
public bool ChangeData(object data)
{
if (data is List<IDesignPredicate> predicates)
{
Predicates = predicates;
return true;
}
if (data is bool resetOnRedraw)
{
ResetOnRedraw = resetOnRedraw;
return true;
}
return false;
}
public bool ForcedRedraw
=> _currentDesign?.ForcedRedraw ?? false;
public bool ResetAdvancedDyes
=> _currentDesign?.ResetAdvancedDyes ?? false;
public bool ResetTemporarySettings
=> _currentDesign?.ResetTemporarySettings ?? false;
}

View file

@ -1,51 +0,0 @@
using OtterGui;
using OtterGui.Services;
namespace Glamourer.Designs.Special;
public class RandomDesignGenerator(DesignStorage designs, DesignFileSystem fileSystem, Configuration config) : IService
{
private readonly Random _rng = new();
private readonly WeakReference<Design> _lastDesign = new(null!, false);
public Design? Design(IReadOnlyList<Design> localDesigns)
{
if (localDesigns.Count is 0)
return null;
var idx = _rng.Next(0, localDesigns.Count);
if (localDesigns.Count is 1)
{
_lastDesign.SetTarget(localDesigns[idx]);
return localDesigns[idx];
}
if (config.PreventRandomRepeats && _lastDesign.TryGetTarget(out var lastDesign))
while (lastDesign == localDesigns[idx])
idx = _rng.Next(0, localDesigns.Count);
var design = localDesigns[idx];
Glamourer.Log.Verbose($"[Random Design] Chose design {idx + 1} out of {localDesigns.Count}: {design.Incognito}.");
_lastDesign.SetTarget(design);
return design;
}
public Design? Design()
=> Design(designs);
public Design? Design(IDesignPredicate predicate)
=> Design(predicate.Get(designs, fileSystem).ToList());
public Design? Design(IReadOnlyList<IDesignPredicate> predicates)
{
return predicates.Count switch
{
0 => Design(),
1 => Design(predicates[0]),
_ => Design(IDesignPredicate.Get(predicates, designs, fileSystem).ToList()),
};
}
public Design? Design(string restrictions)
=> Design(RandomPredicate.GeneratePredicates(restrictions));
}

View file

@ -1,163 +0,0 @@
using OtterGui.Classes;
namespace Glamourer.Designs.Special;
public interface IDesignPredicate
{
public bool Invoke(Design design, string lowerName, string identifier, string lowerPath);
public bool Invoke((Design Design, string LowerName, string Identifier, string LowerPath) args)
=> Invoke(args.Design, args.LowerName, args.Identifier, args.LowerPath);
public IEnumerable<Design> Get(IEnumerable<Design> designs, DesignFileSystem fileSystem)
=> designs.Select(d => Transform(d, fileSystem))
.Where(Invoke)
.Select(t => t.Design);
public static IEnumerable<Design> Get(IReadOnlyList<IDesignPredicate> predicates, IEnumerable<Design> designs, DesignFileSystem fileSystem)
=> predicates.Count > 0
? designs.Select(d => Transform(d, fileSystem))
.Where(t => predicates.Any(p => p.Invoke(t)))
.Select(t => t.Design)
: designs;
private static (Design Design, string LowerName, string Identifier, string LowerPath) Transform(Design d, DesignFileSystem fs)
=> (d, d.Name.Lower, d.Identifier.ToString(), fs.TryGetValue(d, out var l) ? l.FullName().ToLowerInvariant() : string.Empty);
}
public static class RandomPredicate
{
public readonly struct StartsWith(string value) : IDesignPredicate
{
public LowerString Value { get; } = value;
public bool Invoke(Design design, string lowerName, string identifier, string lowerPath)
=> lowerPath.StartsWith(Value.Lower);
public override string ToString()
=> $"/{Value.Text}";
}
public readonly struct Contains(string value) : IDesignPredicate
{
public LowerString Value { get; } = value;
public bool Invoke(Design design, string lowerName, string identifier, string lowerPath)
{
if (lowerName.Contains(Value.Lower))
return true;
if (identifier.Contains(Value.Lower))
return true;
if (lowerPath.Contains(Value.Lower))
return true;
return false;
}
public override string ToString()
=> Value.Text;
}
public readonly struct Exact(Exact.Type type, string value) : IDesignPredicate
{
public enum Type : byte
{
Name,
Path,
Identifier,
Tag,
Color,
}
public Type Which { get; } = type;
public LowerString Value { get; } = value;
public bool Invoke(Design design, string lowerName, string identifier, string lowerPath)
=> Which switch
{
Type.Name => lowerName == Value.Lower,
Type.Path => lowerPath == Value.Lower,
Type.Identifier => identifier == Value.Lower,
Type.Tag => IsContained(Value, design.Tags),
Type.Color => design.Color == Value,
_ => false,
};
private static bool IsContained(LowerString value, IEnumerable<string> data)
=> data.Any(t => t == value);
public override string ToString()
=> $"\"{Which switch { Type.Name => 'n', Type.Identifier => 'i', Type.Path => 'p', Type.Tag => 't', Type.Color => 'c', _ => '?' }}?{Value.Text}\"";
}
public static IDesignPredicate CreateSinglePredicate(string restriction)
{
switch (restriction[0])
{
case '/': return new StartsWith(restriction[1..]);
case '"':
var end = restriction.IndexOf('"', 1);
if (end < 3)
return new Contains(restriction);
switch (restriction[1], restriction[2])
{
case ('n', '?'):
case ('N', '?'):
return new Exact(Exact.Type.Name, restriction[3..end]);
case ('p', '?'):
case ('P', '?'):
return new Exact(Exact.Type.Path, restriction[3..end]);
case ('i', '?'):
case ('I', '?'):
return new Exact(Exact.Type.Identifier, restriction[3..end]);
case ('t', '?'):
case ('T', '?'):
return new Exact(Exact.Type.Tag, restriction[3..end]);
case ('c', '?'):
case ('C', '?'):
return new Exact(Exact.Type.Color, restriction[3..end]);
default: return new Contains(restriction);
}
default: return new Contains(restriction);
}
}
public static List<IDesignPredicate> GeneratePredicates(string restrictions)
{
if (restrictions.Length == 0)
return [];
List<IDesignPredicate> predicates = new(1);
if (restrictions[0] is '{')
{
var end = restrictions.IndexOf('}');
if (end == -1)
{
predicates.Add(CreateSinglePredicate(restrictions));
}
else
{
restrictions = restrictions[1..end];
var split = restrictions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
predicates.AddRange(split.Distinct().Select(CreateSinglePredicate));
}
}
else
{
predicates.Add(CreateSinglePredicate(restrictions));
}
return predicates;
}
public static string GeneratePredicateString(IReadOnlyCollection<IDesignPredicate> predicates)
{
if (predicates.Count == 0)
return string.Empty;
if (predicates.Count == 1)
return predicates.First()!.ToString()!;
return $"{{{string.Join("; ", predicates)}}}";
}
}

View file

@ -1,54 +0,0 @@
using Glamourer.Automation;
using Glamourer.Interop.Material;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs.Special;
public class RevertDesign : IDesignStandIn
{
public const string SerializedName = "//Revert";
public const string ResolvedName = "Revert";
public string ResolveName(bool _)
=> ResolvedName;
public ref readonly DesignData GetDesignData(in DesignData baseRef)
=> ref baseRef;
public IReadOnlyList<(uint, MaterialValueDesign)> GetMaterialData()
=> [];
public string SerializeName()
=> SerializedName;
public bool Equals(IDesignStandIn? other)
=> other is RevertDesign;
public StateSource AssociatedSource()
=> StateSource.Game;
public IEnumerable<(IDesignStandIn Design, ApplicationType Flags, JobFlag Jobs)> AllLinks(bool _)
{
yield return (this, ApplicationType.All, JobFlag.All);
}
public void AddData(JObject jObj)
{ }
public void ParseData(JObject jObj)
{ }
public bool ChangeData(object data)
=> false;
public bool ForcedRedraw
=> false;
public bool ResetAdvancedDyes
=> true;
public bool ResetTemporarySettings
=> true;
}

View file

@ -1,78 +0,0 @@
using Dalamud.Interface.ImGuiNotification;
using Glamourer.Gui;
using Glamourer.Services;
using Newtonsoft.Json;
using OtterGui.Classes;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Glamourer;
public class EphemeralConfig : ISavable
{
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
public bool IncognitoMode { get; set; } = false;
public bool UnlockDetailMode { get; set; } = true;
public bool ShowDesignQuickBar { get; set; } = false;
public bool LockDesignQuickBar { get; set; } = false;
public bool LockMainWindow { get; set; } = false;
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
public Guid SelectedDesign { get; set; } = Guid.Empty;
public Guid SelectedQuickDesign { get; set; } = Guid.Empty;
public int LastSeenVersion { get; set; } = GlamourerChangelog.LastChangelogVersion;
public float CurrentDesignSelectorWidth { get; set; } = 200f;
public float DesignSelectorMinimumScale { get; set; } = 0.1f;
public float DesignSelectorMaximumScale { get; set; } = 0.5f;
[JsonIgnore]
private readonly SaveService _saveService;
public EphemeralConfig(SaveService saveService)
{
_saveService = saveService;
Load();
}
public void Save()
=> _saveService.DelaySave(this, TimeSpan.FromSeconds(5));
public void Load()
{
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
{
Glamourer.Log.Error(
$"Error parsing ephemeral Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
errorArgs.ErrorContext.Handled = true;
}
if (!File.Exists(_saveService.FileNames.EphemeralConfigFile))
return;
try
{
var text = File.ReadAllText(_saveService.FileNames.EphemeralConfigFile);
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
{
Error = HandleDeserializationError,
});
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex,
"Error reading ephemeral Configuration, reverting to default.",
"Error reading ephemeral Configuration", NotificationType.Error);
}
}
public string ToFilename(FilenameService fileNames)
=> fileNames.EphemeralConfigFile;
public void Save(StreamWriter writer)
{
using var jWriter = new JsonTextWriter(writer);
jWriter.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}
}

View file

@ -1,16 +0,0 @@
using OtterGui.Classes;
namespace Glamourer.Events;
/// <summary>
/// Triggered when the auto-reload gear setting is changed in glamourer configuration.
/// </summary>
public sealed class AutoRedrawChanged()
: EventWrapper<bool, AutoRedrawChanged.Priority>(nameof(AutoRedrawChanged))
{
public enum Priority
{
/// <seealso cref="Api.StateApi.OnGPoseChange"/>
StateApi = int.MinValue,
}
}

View file

@ -1,4 +1,5 @@
using Glamourer.Automation;
using System;
using Glamourer.Automation;
using OtterGui.Classes;
namespace Glamourer.Events;
@ -11,8 +12,8 @@ namespace Glamourer.Events;
/// <item>Parameter is additional data depending on the type of change. </item>
/// </list>
/// </summary>
public sealed class AutomationChanged()
: EventWrapper<AutomationChanged.Type, AutoDesignSet?, object?, AutomationChanged.Priority>(nameof(AutomationChanged))
public sealed class AutomationChanged : EventWrapper<Action<AutomationChanged.Type, AutoDesignSet?, object?>,
AutomationChanged.Priority>
{
public enum Type
{
@ -37,9 +38,6 @@ public sealed class AutomationChanged()
/// <summary> Change the used base state of a given set. Additional data is prior and new base. [(AutoDesignSet.Base, AutoDesignSet.Base)]. </summary>
ChangedBase,
/// <summary> Change the resetting of temporary settings for a given set. Additional data is the new value. </summary>
ChangedTemporarySettingsReset,
/// <summary> Add a new associated design to a given set. Additional data is the index it got added at [int]. </summary>
AddedDesign,
@ -49,7 +47,7 @@ public sealed class AutomationChanged()
/// <summary> Move a given associated design in the list of a given set. Additional data is the index that got moved and the index it got moved to [(int, int)]. </summary>
MovedDesign,
/// <summary> Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, IDesignStandIn, IDesignStandIn)]. </summary>
/// <summary> Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, Design, Design)]. </summary>
ChangedDesign,
/// <summary> Change the job condition in an associated design for a given set. Additional data is the index of the changed associated design, the old job group and the new job group [(int, JobGroup, JobGroup)]. </summary>
@ -57,9 +55,6 @@ public sealed class AutomationChanged()
/// <summary> Change the application type in an associated design for a given set. Additional data is the index of the changed associated design, the old type and the new type. [(int, AutoDesign.Type, AutoDesign.Type)]. </summary>
ChangedType,
/// <summary> Change the additional data for a specific design type. Additional data is the index of the changed associated design and the new data. [(int, object)] </summary>
ChangedData,
}
public enum Priority
@ -68,9 +63,13 @@ public sealed class AutomationChanged()
SetSelector = 0,
/// <seealso cref="AutoDesignApplier.OnAutomationChange"/>
AutoDesignApplier = 0,
/// <seealso cref="Gui.Tabs.AutomationTab.RandomRestrictionDrawer.OnAutomationChange"/>
RandomRestrictionDrawer = -1,
AutoDesignApplier,
}
public AutomationChanged()
: base(nameof(AutomationChanged))
{ }
public void Invoke(Type type, AutoDesignSet? set, object? data)
=> Invoke(this, type, set, data);
}

View file

@ -1,25 +0,0 @@
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a model flags a bonus slot for an update.
/// <list type="number">
/// <item>Parameter is the model with a flagged slot. </item>
/// <item>Parameter is the bonus slot changed. </item>
/// <item>Parameter is the model values to change the bonus piece to. </item>
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item>
/// </list>
/// </summary>
public sealed class BonusSlotUpdating()
: EventWrapperRef34<Model, BonusItemFlag, CharacterArmor, ulong, BonusSlotUpdating.Priority>(nameof(BonusSlotUpdating))
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnBonusSlotUpdating"/>
StateListener = 0,
}
}

View file

@ -1,6 +1,5 @@
using System;
using Glamourer.Designs;
using Glamourer.Designs.History;
using Glamourer.Gui;
using OtterGui.Classes;
namespace Glamourer.Events;
@ -13,138 +12,84 @@ namespace Glamourer.Events;
/// <item>Parameter is any additional data depending on the type of change. </item>
/// </list>
/// </summary>
public sealed class DesignChanged()
: EventWrapper<DesignChanged.Type, Design, ITransaction?, DesignChanged.Priority>(nameof(DesignChanged))
public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Design, object?>, DesignChanged.Priority>
{
public enum Type
{
/// <summary> A new design was created. </summary>
/// <summary> A new design was created. Data is a potential path to move it to [string?]. </summary>
Created,
/// <summary> An existing design was deleted. </summary>
/// <summary> An existing design was deleted. Data is null. </summary>
Deleted,
/// <summary> Invoked on full reload. </summary>
/// <summary> Invoked on full reload. Design and Data are null. </summary>
ReloadedAll,
/// <summary> An existing design was renamed. </summary>
/// <summary> An existing design was renamed. Data is the prior name [string]. </summary>
Renamed,
/// <summary> An existing design had its description changed. </summary>
/// <summary> An existing design had its description changed. Data is the prior description [string]. </summary>
ChangedDescription,
/// <summary> An existing design had its associated color changed. </summary>
ChangedColor,
/// <summary> An existing design had a new tag added. </summary>
/// <summary> An existing design had a new tag added. Data is the new tag and the index it was added at [(string, int)]. </summary>
AddedTag,
/// <summary> An existing design had an existing tag removed. </summary>
/// <summary> An existing design had an existing tag removed. Data is the removed tag and the index it had before removal [(string, int)]. </summary>
RemovedTag,
/// <summary> An existing design had an existing tag renamed. </summary>
/// <summary> An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. </summary>
ChangedTag,
/// <summary> An existing design had a new associated mod added. </summary>
/// <summary> An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
AddedMod,
/// <summary> An existing design had an existing associated mod removed. </summary>
/// <summary> An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
RemovedMod,
/// <summary> An existing design had an existing associated mod updated. </summary>
UpdatedMod,
/// <summary> An existing design had a link to a different design added, removed or moved. </summary>
ChangedLink,
/// <summary> An existing design had a customization changed. </summary>
/// <summary> An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. </summary>
Customize,
/// <summary> An existing design had its entire customize array changed. </summary>
EntireCustomize,
/// <summary> An existing design had an equipment piece changed. </summary>
/// <summary> An existing design had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. </summary>
Equip,
/// <summary> An existing design had a bonus item changed. </summary>
BonusItem,
/// <summary> An existing design had its weapons changed. </summary>
/// <summary> An existing design had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. </summary>
Weapon,
/// <summary> An existing design had a stain changed. </summary>
Stains,
/// <summary> An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Stain,
/// <summary> An existing design had a crest visibility changed. </summary>
Crest,
/// <summary> An existing design had a customize parameter changed. </summary>
Parameter,
/// <summary> An existing design had an advanced dye row added, changed, or deleted. </summary>
Material,
/// <summary> An existing design had an advanced dye rows Revert state changed. </summary>
MaterialRevert,
/// <summary> An existing design had changed whether it always forces a redraw or not. </summary>
ForceRedraw,
/// <summary> An existing design had changed whether it always resets advanced dyes or not. </summary>
ResetAdvancedDyes,
/// <summary> An existing design had changed whether it always resets all prior temporary settings or not. </summary>
ResetTemporarySettings,
/// <summary> An existing design changed whether a specific customization is applied. </summary>
/// <summary> An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. </summary>
ApplyCustomize,
/// <summary> An existing design changed whether a specific equipment piece is applied. </summary>
/// <summary> An existing design changed whether a specific equipment is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyEquip,
/// <summary> An existing design changed whether a specific bonus item is applied. </summary>
ApplyBonusItem,
/// <summary> An existing design changed whether a specific stain is applied. </summary>
/// <summary> An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyStain,
/// <summary> An existing design changed whether a specific crest visibility is applied. </summary>
ApplyCrest,
/// <summary> An existing design changed whether a specific customize parameter is applied. </summary>
ApplyParameter,
/// <summary> An existing design changed whether an advanced dye row is applied. </summary>
ApplyMaterial,
/// <summary> An existing design changed its write protection status. </summary>
/// <summary> An existing design changed its write protection status. Data is the new value [bool]. </summary>
WriteProtection,
/// <summary> An existing design changed its display status for the quick design bar. </summary>
QuickDesignBar,
/// <summary> An existing design changed one of the meta flags. </summary>
/// <summary> An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. </summary>
Other,
}
public enum Priority
{
/// <seealso cref="Designs.Links.DesignLinkManager.OnDesignChanged"/>
DesignLinkManager = 1,
/// <seealso cref="Automation.AutoDesignManager.OnDesignChange"/>
AutoDesignManager = 1,
/// <seealso cref="DesignFileSystem.OnDesignChange"/>
DesignFileSystem = 0,
/// <seealso cref="Gui.Tabs.DesignTab.DesignFileSystemSelector.OnDesignChange"/>
DesignFileSystemSelector = -1,
/// <seealso cref="DesignComboBase.OnDesignChanged"/>
DesignCombo = -2,
/// <seealso cref="EditorHistory.OnDesignChanged" />
EditorHistory = -1000,
/// <seealso cref="Automation.AutoDesignManager.OnDesignChange"/>
AutoDesignManager = 1,
}
public DesignChanged()
: base(nameof(DesignChanged))
{ }
public void Invoke(Type type, Design design, object? data = null)
=> Invoke(this, type, design, data);
}

View file

@ -1,25 +0,0 @@
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a model flags an equipment slot for an update.
/// <list type="number">
/// <item>Parameter is the model with a flagged slot. </item>
/// <item>Parameter is the equipment slot changed. </item>
/// <item>Parameter is the model values to change the equipment piece to. </item>
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item>
/// </list>
/// </summary>
public sealed class EquipSlotUpdating()
: EventWrapperRef34<Model, EquipSlot, CharacterArmor, ulong, EquipSlotUpdating.Priority>(nameof(EquipSlotUpdating))
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnEquipSlotUpdating"/>
StateListener = 0,
}
}

View file

@ -1,23 +0,0 @@
using OtterGui.Classes;
namespace Glamourer.Events;
/// <summary>
/// Triggered when the player equips a gear set.
/// <list type="number">
/// <item>Parameter is the name of the gear set. </item>
/// <item>Parameter is the id of the gear set. </item>
/// <item>Parameter is the id of the prior gear set. </item>
/// <item>Parameter is the id of the associated glamour. </item>
/// <item>Parameter is the job id of the associated job. </item>
/// </list>
/// </summary>
public sealed class EquippedGearset()
: EventWrapper<string, int, int, byte, byte, EquippedGearset.Priority>(nameof(EquippedGearset))
{
public enum Priority
{
/// <seealso cref="Automation.AutoDesignApplier.OnEquippedGearset"/>
AutoDesignApplier = 0,
}
}

View file

@ -1,9 +1,11 @@
using Dalamud.Plugin.Services;
using System;
using System.Collections.Concurrent;
using Dalamud.Plugin.Services;
using OtterGui.Classes;
namespace Glamourer.Events;
public sealed class GPoseService : EventWrapper<bool, GPoseService.Priority>
public sealed class GPoseService : EventWrapper<Action<bool>, GPoseService.Priority>
{
private readonly IFramework _framework;
private readonly IClientState _state;
@ -13,8 +15,8 @@ public sealed class GPoseService : EventWrapper<bool, GPoseService.Priority>
public enum Priority
{
/// <seealso cref="Api.StateApi.OnGPoseChange"/>
StateApi = int.MinValue,
/// <seealso cref="Api.GlamourerIpc.OnGPoseChanged"/>
GlamourerIpc = int.MinValue,
}
public bool InGPose { get; private set; }
@ -54,9 +56,9 @@ public sealed class GPoseService : EventWrapper<bool, GPoseService.Priority>
return;
InGPose = inGPose;
Invoke(InGPose);
Invoke(this, InGPose);
var actions = InGPose ? _onEnter : _onLeave;
while (actions.TryDequeue(out var action))
foreach (var action in actions)
{
try
{

View file

@ -1,21 +0,0 @@
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
/// <summary>
/// Triggers when the equipped gearset finished all LoadEquipment, LoadWeapon, and LoadCrest calls. (All Non-MetaData)
/// This defines an endpoint for when the gameState is updated.
/// <list type="number">
/// <item>The model draw object associated with the finished load (Also fired by other players on render) </item>
/// </list>
/// </summary>
public sealed class GearsetDataLoaded()
: EventWrapper<Actor, Model, GearsetDataLoaded.Priority>(nameof(GearsetDataLoaded))
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnGearsetDataLoaded"/>
StateListener = 0,
}
}

View file

@ -1,5 +1,6 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
@ -10,12 +11,22 @@ namespace Glamourer.Events;
/// <item>Parameter is the new state. </item>
/// </list>
/// </summary>
public sealed class HeadGearVisibilityChanged()
: EventWrapperRef2<Actor, bool, HeadGearVisibilityChanged.Priority>(nameof(HeadGearVisibilityChanged))
public sealed class HeadGearVisibilityChanged : EventWrapper<Action<Actor, Ref<bool>>, HeadGearVisibilityChanged.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnHeadGearVisibilityChange"/>
StateListener = 0,
}
public HeadGearVisibilityChanged()
: base(nameof(HeadGearVisibilityChanged))
{ }
public void Invoke(Actor actor, ref bool state)
{
var value = new Ref<bool>(state);
Invoke(this, actor, value);
state = value;
}
}

View file

@ -1,3 +1,4 @@
using System;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -10,12 +11,18 @@ namespace Glamourer.Events;
/// <item>Parameter is an array of slots updated and corresponding item ids and stains. </item>
/// </list>
/// </summary>
public sealed class MovedEquipment()
: EventWrapper<(EquipSlot, uint, StainIds)[], MovedEquipment.Priority>(nameof(MovedEquipment))
public sealed class MovedEquipment : EventWrapper<Action<(EquipSlot, uint, StainId)[]>, MovedEquipment.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnMovedEquipment"/>
StateListener = 0,
}
public MovedEquipment()
: base(nameof(MovedEquipment))
{ }
public void Invoke((EquipSlot, uint, StainId)[] items)
=> Invoke(this, items);
}

View file

@ -1,3 +1,4 @@
using System;
using OtterGui.Classes;
namespace Glamourer.Events;
@ -10,8 +11,7 @@ namespace Glamourer.Events;
/// <item>Parameter is the timestamp of the unlock. </item>
/// </list>
/// </summary>
public sealed class ObjectUnlocked()
: EventWrapper<ObjectUnlocked.Type, uint, DateTimeOffset, ObjectUnlocked.Priority>(nameof(ObjectUnlocked))
public sealed class ObjectUnlocked : EventWrapper<Action<ObjectUnlocked.Type, uint, DateTimeOffset>, ObjectUnlocked.Priority>
{
public enum Type
{
@ -25,4 +25,11 @@ public sealed class ObjectUnlocked()
/// <remarks> Currently used as a hack to make the unlock table dirty in it. If anything else starts using this, rework. </remarks>
UnlockTable = 0,
}
public ObjectUnlocked()
: base(nameof(ObjectUnlocked))
{ }
public void Invoke(Type type, uint id, DateTimeOffset timestamp)
=> Invoke(this, type, id, timestamp);
}

View file

@ -1,3 +1,4 @@
using System;
using OtterGui.Classes;
namespace Glamourer.Events;
@ -5,18 +6,18 @@ namespace Glamourer.Events;
/// <summary>
/// Triggered when Penumbra is reloaded.
/// </summary>
public sealed class PenumbraReloaded()
: EventWrapper<PenumbraReloaded.Priority>(nameof(PenumbraReloaded))
public sealed class PenumbraReloaded : EventWrapper<Action, PenumbraReloaded.Priority>
{
public enum Priority
{
/// <seealso cref="Interop.ChangeCustomizeService.Restore"/>
ChangeCustomizeService = 0,
/// <seealso cref="Interop.VisorService.Restore"/>
VisorService = 0,
/// <seealso cref="Interop.VieraEarService.Restore"/>
VieraEarService = 0,
}
public PenumbraReloaded()
: base(nameof(PenumbraReloaded))
{ }
public void Invoke()
=> Invoke(this);
}

View file

@ -0,0 +1,38 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a model flags an equipment slot for an update.
/// <list type="number">
/// <item>Parameter is the model with a flagged slot. </item>
/// <item>Parameter is the equipment slot changed. </item>
/// <item>Parameter is the model values to change the equipment piece to. </item>
/// <item>Parameter is the return value the function should return, if it is ulong.MaxValue, the original will be called and returned. </item>
/// </list>
/// </summary>
public sealed class SlotUpdating : EventWrapper<Action<Model, EquipSlot, Ref<CharacterArmor>, Ref<ulong>>, SlotUpdating.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnSlotUpdating"/>
StateListener = 0,
}
public SlotUpdating()
: base(nameof(SlotUpdating))
{ }
public void Invoke(Model model, EquipSlot slot, ref CharacterArmor armor, ref ulong returnValue)
{
var value = new Ref<CharacterArmor>(armor);
var @return = new Ref<ulong>(returnValue);
Invoke(this, model, slot, value, @return);
armor = value;
returnValue = @return;
}
}

View file

@ -1,9 +1,8 @@
using Glamourer.Api.Enums;
using Glamourer.Designs.History;
using System;
using Glamourer.Interop.Structs;
using Glamourer.State;
using OtterGui.Classes;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Actors;
namespace Glamourer.Events;
@ -16,18 +15,55 @@ namespace Glamourer.Events;
/// <item>Parameter is any additional data depending on the type of change. </item>
/// </list>
/// </summary>
public sealed class StateChanged()
: EventWrapper<StateChangeType, StateSource, ActorState, ActorData, ITransaction?, StateChanged.Priority>(nameof(StateChanged))
public sealed class StateChanged : EventWrapper<Action<StateChanged.Type, StateChanged.Source, ActorState, ActorData, object?>, StateChanged.Priority>
{
public enum Type
{
/// <summary> A characters saved state had the model id changed. This means everything may have changed. Data is the old model id and the new model id. [(uint, uint)] </summary>
Model,
/// <summary> A characters saved state had multiple customization values changed. TData is the old customize array and the applied changes. [(Customize, CustomizeFlag)] </summary>
EntireCustomize,
/// <summary> A characters saved state had a customization value changed. Data is the old value, the new value and the type. [(CustomizeValue, CustomizeValue, CustomizeIndex)]. </summary>
Customize,
/// <summary> A characters saved state had an equipment piece changed. Data is the old value, the new value and the slot [(EquipItem, EquipItem, EquipSlot)]. </summary>
Equip,
/// <summary> A characters saved state had its weapons changed. Data is the old mainhand, the old offhand, the new mainhand and the new offhand [(EquipItem, EquipItem, EquipItem, EquipItem)]. </summary>
Weapon,
/// <summary> A characters saved state had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Stain,
/// <summary> A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] </summary>
Design,
/// <summary> A characters saved state had its state reset to its game values. This means everything may have changed. Data is null. </summary>
Reset,
/// <summary> A characters saved state had a meta toggle changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Other,
}
public enum Source : byte
{
Game,
Manual,
Fixed,
Ipc,
}
public enum Priority
{
/// <seealso cref="Api.StateApi.OnStateChanged" />
GlamourerIpc = int.MinValue,
/// <seealso cref="Interop.Penumbra.PenumbraAutoRedraw.OnStateChanged" />
PenumbraAutoRedraw = 0,
/// <seealso cref="EditorHistory.OnStateChanged" />
EditorHistory = -1000,
}
public StateChanged()
: base(nameof(StateChanged))
{ }
public void Invoke(Type type, Source source, ActorState state, ActorData actors, object? data = null)
=> Invoke(this, type, source, state, actors, data);
}

View file

@ -1,24 +0,0 @@
using Glamourer.Api;
using Glamourer.Api.Enums;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a set of grouped changes finishes being applied to a Glamourer state.
/// <list type="number">
/// <item>Parameter is the operation that finished updating the saved state. </item>
/// <item>Parameter is the existing actors using this saved state. </item>
/// </list>
/// </summary>
public sealed class StateFinalized()
: EventWrapper<StateFinalizationType, ActorData, StateFinalized.Priority>(nameof(StateFinalized))
{
public enum Priority
{
/// <seealso cref="StateApi.OnStateFinalized"/>
StateApi = int.MinValue,
}
}

View file

@ -1,4 +1,5 @@
using Glamourer.Designs;
using System;
using Glamourer.Designs;
using Glamourer.Gui;
using OtterGui.Classes;
@ -11,8 +12,8 @@ namespace Glamourer.Events;
/// <item>Parameter is the design to select if the tab is the designs tab. </item>
/// </list>
/// </summary>
public sealed class TabSelected()
: EventWrapper<MainWindow.TabType, Design?, TabSelected.Priority>(nameof(TabSelected))
public sealed class TabSelected : EventWrapper<Action<MainWindow.TabType, Design?>,
TabSelected.Priority>
{
public enum Priority
{
@ -22,4 +23,11 @@ public sealed class TabSelected()
/// <seealso cref="Gui.MainWindow.OnTabSelected"/>
MainWindow = 1,
}
public TabSelected()
: base(nameof(TabSelected))
{ }
public void Invoke(MainWindow.TabType type, Design? design)
=> Invoke(this, type, design);
}

View file

@ -1,22 +0,0 @@
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
/// <summary>
/// Triggered when the state of viera ear visibility for any draw object is changed.
/// <list type="number">
/// <item>Parameter is the model with a changed viera ear visibility state. </item>
/// <item>Parameter is the new state. </item>
/// <item>Parameter is whether to call the original function. </item>
/// </list>
/// </summary>
public sealed class VieraEarStateChanged()
: EventWrapperRef2<Actor, bool, VieraEarStateChanged.Priority>(nameof(VieraEarStateChanged))
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnVieraEarChange"/>
StateListener = 0,
}
}

View file

@ -1,5 +1,6 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
@ -11,12 +12,22 @@ namespace Glamourer.Events;
/// <item>Parameter is whether to call the original function. </item>
/// </list>
/// </summary>
public sealed class VisorStateChanged()
: EventWrapperRef3<Model, bool, bool, VisorStateChanged.Priority>(nameof(VisorStateChanged))
public sealed class VisorStateChanged : EventWrapper<Action<Model, Ref<bool>>, VisorStateChanged.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnVisorChange"/>
StateListener = 0,
}
}
public VisorStateChanged()
: base(nameof(VisorStateChanged))
{ }
public void Invoke(Model model, ref bool state)
{
var value = new Ref<bool>(state);
Invoke(this, model, value);
state = value;
}
}

View file

@ -1,6 +1,7 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
@ -13,8 +14,7 @@ namespace Glamourer.Events;
/// <item>Parameter is the model values to change the weapon to. </item>
/// </list>
/// </summary>
public sealed class WeaponLoading()
: EventWrapperRef3<Actor, EquipSlot, CharacterWeapon, WeaponLoading.Priority>(nameof(WeaponLoading))
public sealed class WeaponLoading : EventWrapper<Action<Actor, EquipSlot, Ref<CharacterWeapon>>, WeaponLoading.Priority>
{
public enum Priority
{
@ -24,4 +24,15 @@ public sealed class WeaponLoading()
/// <seealso cref="Automation.AutoDesignApplier.OnWeaponLoading"/>
AutoDesignApplier = -1,
}
public WeaponLoading()
: base(nameof(WeaponLoading))
{ }
public void Invoke(Actor actor, EquipSlot slot, ref CharacterWeapon weapon)
{
var value = new Ref<CharacterWeapon>(weapon);
Invoke(this, actor, slot, value);
weapon = value;
}
}

View file

@ -1,5 +1,6 @@
using System;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
@ -10,11 +11,22 @@ namespace Glamourer.Events;
/// <item>Parameter is the new state. </item>
/// </list>
/// </summary>
public sealed class WeaponVisibilityChanged() : EventWrapperRef2<Actor, bool, WeaponVisibilityChanged.Priority>(nameof(WeaponVisibilityChanged))
public sealed class WeaponVisibilityChanged : EventWrapper<Action<Actor, Ref<bool>>, WeaponVisibilityChanged.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnWeaponVisibilityChange"/>
StateListener = 0,
}
public WeaponVisibilityChanged()
: base(nameof(WeaponVisibilityChanged))
{ }
public void Invoke(Actor actor, ref bool state)
{
var value = new Ref<bool>(state);
Invoke(this, actor, value);
state = value;
}
}

View file

@ -1,54 +0,0 @@
using Dalamud.Plugin.Services;
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);
public unsafe ColorParameters(IDataManager gameData, IPluginLog log)
{
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)
{
fixed (uint* ptr2 = _rgbaColors)
{
MemoryUtility.MemCpyUnchecked(ptr2, ptr1, file.Data.Length);
}
}
}
catch (Exception e)
{
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 = [];
}
}
/// <inheritdoc/>
public IEnumerator<uint> GetEnumerator()
=> (IEnumerator<uint>)_rgbaColors.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public int Count
=> _rgbaColors.Length;
/// <inheritdoc/>
public uint this[int index]
=> _rgbaColors[index];
}

View file

@ -1,96 +0,0 @@
using Dalamud.Interface.Textures;
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 : IAsyncDataContainer
{
/// <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 (!Finished)
Awaiter.Wait();
return _customizationSets[ToIndex(race, gender)];
}
/// <summary> Get specific icons. </summary>
public ISharedImmediateTexture GetIcon(uint id)
=> _icons.TextureProvider.GetFromGameIcon(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 TextureCache(gameData, textures);
var stopwatch = new Stopwatch();
var tmpTask = Task.Run(() =>
{
stopwatch.Start();
return 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).ContinueWith(_ =>
{
// This is far too hard to estimate sensibly.
TotalCount = 0;
Memory = 0;
Time = stopwatch.ElapsedMilliseconds;
});
}
/// <inheritdoc/>
public Task Awaiter { get; }
/// <inheritdoc/>
public bool Finished
=> Awaiter.IsCompletedSuccessfully;
private readonly TextureCache _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;
}
public long Time { get; private set; }
public long Memory { get; private set; }
public string Name
=> nameof(CustomizeManager);
public int TotalCount { get; private set; }
}

View file

@ -1,303 +0,0 @@
using FFXIVClientStructs.FFXIV.Shader;
namespace Glamourer.GameData;
public struct CustomizeParameterData
{
public Vector4 DecalColor;
public Vector4 LipDiffuse;
public Vector3 SkinDiffuse;
public Vector3 SkinSpecular;
public Vector3 HairDiffuse;
public Vector3 HairSpecular;
public Vector3 HairHighlight;
public Vector3 LeftEye;
public float LeftLimbalIntensity;
public Vector3 RightEye;
public float RightLimbalIntensity;
public Vector3 FeatureColor;
public float FacePaintUvMultiplier;
public float FacePaintUvOffset;
public float MuscleTone;
public CustomizeParameterValue this[CustomizeParameterFlag flag]
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
readonly get
{
return flag switch
{
CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(SkinDiffuse),
CustomizeParameterFlag.MuscleTone => new CustomizeParameterValue(MuscleTone),
CustomizeParameterFlag.SkinSpecular => new CustomizeParameterValue(SkinSpecular),
CustomizeParameterFlag.LipDiffuse => new CustomizeParameterValue(LipDiffuse),
CustomizeParameterFlag.HairDiffuse => new CustomizeParameterValue(HairDiffuse),
CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(HairSpecular),
CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(HairHighlight),
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye),
CustomizeParameterFlag.LeftLimbalIntensity => new CustomizeParameterValue(LeftLimbalIntensity),
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye),
CustomizeParameterFlag.RightLimbalIntensity => new CustomizeParameterValue(RightLimbalIntensity),
CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(FeatureColor),
CustomizeParameterFlag.DecalColor => new CustomizeParameterValue(DecalColor),
CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(FacePaintUvMultiplier),
CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(FacePaintUvOffset),
_ => CustomizeParameterValue.Zero,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
set => Set(flag, value);
}
public bool Set(CustomizeParameterFlag flag, CustomizeParameterValue value)
{
return flag switch
{
CustomizeParameterFlag.SkinDiffuse => SetIfDifferent(ref SkinDiffuse, value.InternalTriple),
CustomizeParameterFlag.MuscleTone => SetIfDifferent(ref MuscleTone, value.Single),
CustomizeParameterFlag.SkinSpecular => SetIfDifferent(ref SkinSpecular, value.InternalTriple),
CustomizeParameterFlag.LipDiffuse => SetIfDifferent(ref LipDiffuse, value.InternalQuadruple),
CustomizeParameterFlag.HairDiffuse => SetIfDifferent(ref HairDiffuse, value.InternalTriple),
CustomizeParameterFlag.HairSpecular => SetIfDifferent(ref HairSpecular, value.InternalTriple),
CustomizeParameterFlag.HairHighlight => SetIfDifferent(ref HairHighlight, value.InternalTriple),
CustomizeParameterFlag.LeftEye => SetIfDifferent(ref LeftEye, value.InternalTriple),
CustomizeParameterFlag.LeftLimbalIntensity => SetIfDifferent(ref LeftLimbalIntensity, value.Single),
CustomizeParameterFlag.RightEye => SetIfDifferent(ref RightEye, value.InternalTriple),
CustomizeParameterFlag.RightLimbalIntensity => SetIfDifferent(ref RightLimbalIntensity, value.Single),
CustomizeParameterFlag.FeatureColor => SetIfDifferent(ref FeatureColor, value.InternalTriple),
CustomizeParameterFlag.DecalColor => SetIfDifferent(ref DecalColor, value.InternalQuadruple),
CustomizeParameterFlag.FacePaintUvMultiplier => SetIfDifferent(ref FacePaintUvMultiplier, value.Single),
CustomizeParameterFlag.FacePaintUvOffset => SetIfDifferent(ref FacePaintUvOffset, value.Single),
_ => false,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public readonly void Apply(ref CustomizeParameter parameters, CustomizeParameterFlag flags = CustomizeParameterExtensions.All)
{
parameters.SkinColor = (flags & (CustomizeParameterFlag.SkinDiffuse | CustomizeParameterFlag.MuscleTone)) switch
{
0 => parameters.SkinColor,
CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(SkinDiffuse, parameters.SkinColor.W).XivQuadruple,
CustomizeParameterFlag.MuscleTone => parameters.SkinColor with { W = MuscleTone },
_ => new CustomizeParameterValue(SkinDiffuse, MuscleTone).XivQuadruple,
};
parameters.LeftColor = (flags & (CustomizeParameterFlag.LeftEye | CustomizeParameterFlag.LeftLimbalIntensity)) switch
{
0 => parameters.LeftColor,
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple,
CustomizeParameterFlag.LeftLimbalIntensity => parameters.LeftColor with { W = LeftLimbalIntensity },
_ => new CustomizeParameterValue(LeftEye, LeftLimbalIntensity).XivQuadruple,
};
parameters.RightColor = (flags & (CustomizeParameterFlag.RightEye | CustomizeParameterFlag.RightLimbalIntensity)) switch
{
0 => parameters.RightColor,
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple,
CustomizeParameterFlag.RightLimbalIntensity => parameters.RightColor with { W = RightLimbalIntensity },
_ => new CustomizeParameterValue(RightEye, RightLimbalIntensity).XivQuadruple,
};
if (flags.HasFlag(CustomizeParameterFlag.SkinSpecular))
parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple;
if (flags.HasFlag(CustomizeParameterFlag.HairDiffuse))
{
// Vector3 is 0x10 byte for some reason.
var triple = new CustomizeParameterValue(HairDiffuse).XivTriple;
parameters.MainColor.X = triple.X;
parameters.MainColor.Y = triple.Y;
parameters.MainColor.Z = triple.Z;
}
if (flags.HasFlag(CustomizeParameterFlag.HairSpecular))
parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple;
if (flags.HasFlag(CustomizeParameterFlag.HairHighlight))
{
// Vector3 is 0x10 byte for some reason.
var triple = new CustomizeParameterValue(HairHighlight).XivTriple;
parameters.MeshColor.X = triple.X;
parameters.MeshColor.Y = triple.Y;
parameters.MeshColor.Z = triple.Z;
}
if (flags.HasFlag(CustomizeParameterFlag.FacePaintUvMultiplier))
GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier;
if (flags.HasFlag(CustomizeParameterFlag.FacePaintUvOffset))
GetUvOffsetWrite(ref parameters) = FacePaintUvOffset;
if (flags.HasFlag(CustomizeParameterFlag.LipDiffuse))
parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple;
if (flags.HasFlag(CustomizeParameterFlag.FeatureColor))
parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public readonly void Apply(ref DecalParameters parameters, CustomizeParameterFlag flags = CustomizeParameterExtensions.All)
{
if (flags.HasFlag(CustomizeParameterFlag.DecalColor))
parameters.Color = new CustomizeParameterValue(DecalColor).XivQuadruple;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public readonly void ApplySingle(ref CustomizeParameter parameters, CustomizeParameterFlag flag)
{
switch (flag)
{
case CustomizeParameterFlag.SkinDiffuse:
parameters.SkinColor = new CustomizeParameterValue(SkinDiffuse, parameters.SkinColor.W).XivQuadruple;
break;
case CustomizeParameterFlag.MuscleTone:
parameters.SkinColor.W = MuscleTone;
break;
case CustomizeParameterFlag.SkinSpecular:
parameters.SkinFresnelValue0 = new CustomizeParameterValue(SkinSpecular).XivQuadruple;
break;
case CustomizeParameterFlag.LipDiffuse:
parameters.LipColor = new CustomizeParameterValue(LipDiffuse).XivQuadruple;
break;
case CustomizeParameterFlag.HairDiffuse:
// Vector3 is 0x10 byte for some reason.
var triple1 = new CustomizeParameterValue(HairDiffuse).XivTriple;
parameters.MainColor.X = triple1.X;
parameters.MainColor.Y = triple1.Y;
parameters.MainColor.Z = triple1.Z;
break;
case CustomizeParameterFlag.HairSpecular:
parameters.HairFresnelValue0 = new CustomizeParameterValue(HairSpecular).XivTriple;
break;
case CustomizeParameterFlag.HairHighlight:
// Vector3 is 0x10 byte for some reason.
var triple2 = new CustomizeParameterValue(HairHighlight).XivTriple;
parameters.MeshColor.X = triple2.X;
parameters.MeshColor.Y = triple2.Y;
parameters.MeshColor.Z = triple2.Z;
break;
case CustomizeParameterFlag.LeftEye:
parameters.LeftColor = new CustomizeParameterValue(LeftEye, parameters.LeftColor.W).XivQuadruple;
break;
case CustomizeParameterFlag.RightEye:
parameters.RightColor = new CustomizeParameterValue(RightEye, parameters.RightColor.W).XivQuadruple;
break;
case CustomizeParameterFlag.FeatureColor:
parameters.OptionColor = new CustomizeParameterValue(FeatureColor).XivTriple;
break;
case CustomizeParameterFlag.FacePaintUvMultiplier:
GetUvMultiplierWrite(ref parameters) = FacePaintUvMultiplier;
break;
case CustomizeParameterFlag.FacePaintUvOffset:
GetUvOffsetWrite(ref parameters) = FacePaintUvOffset;
break;
case CustomizeParameterFlag.LeftLimbalIntensity:
parameters.LeftColor.W = LeftLimbalIntensity;
break;
case CustomizeParameterFlag.RightLimbalIntensity:
parameters.RightColor.W = RightLimbalIntensity;
break;
}
}
public static CustomizeParameterData FromParameters(in CustomizeParameter parameter, in DecalParameters decal)
=> new()
{
FacePaintUvOffset = GetUvOffset(parameter),
FacePaintUvMultiplier = GetUvMultiplier(parameter),
MuscleTone = parameter.SkinColor.W,
SkinDiffuse = new CustomizeParameterValue(parameter.SkinColor).InternalTriple,
SkinSpecular = new CustomizeParameterValue(parameter.SkinFresnelValue0).InternalTriple,
LipDiffuse = new CustomizeParameterValue(parameter.LipColor).InternalQuadruple,
HairDiffuse = new CustomizeParameterValue(parameter.MainColor).InternalTriple,
HairSpecular = new CustomizeParameterValue(parameter.HairFresnelValue0).InternalTriple,
HairHighlight = new CustomizeParameterValue(parameter.MeshColor).InternalTriple,
LeftEye = new CustomizeParameterValue(parameter.LeftColor).InternalTriple,
LeftLimbalIntensity = new CustomizeParameterValue(parameter.LeftColor.W).Single,
RightEye = new CustomizeParameterValue(parameter.RightColor).InternalTriple,
RightLimbalIntensity = new CustomizeParameterValue(parameter.RightColor.W).Single,
FeatureColor = new CustomizeParameterValue(parameter.OptionColor).InternalTriple,
DecalColor = FromParameter(decal),
};
public static CustomizeParameterValue FromParameter(in CustomizeParameter parameter, CustomizeParameterFlag flag)
=> flag switch
{
CustomizeParameterFlag.SkinDiffuse => new CustomizeParameterValue(parameter.SkinColor),
CustomizeParameterFlag.MuscleTone => new CustomizeParameterValue(parameter.SkinColor.W),
CustomizeParameterFlag.SkinSpecular => new CustomizeParameterValue(parameter.SkinFresnelValue0),
CustomizeParameterFlag.LipDiffuse => new CustomizeParameterValue(parameter.LipColor),
CustomizeParameterFlag.HairDiffuse => new CustomizeParameterValue(parameter.MainColor),
CustomizeParameterFlag.HairSpecular => new CustomizeParameterValue(parameter.HairFresnelValue0),
CustomizeParameterFlag.HairHighlight => new CustomizeParameterValue(parameter.MeshColor),
CustomizeParameterFlag.LeftEye => new CustomizeParameterValue(parameter.LeftColor),
CustomizeParameterFlag.RightEye => new CustomizeParameterValue(parameter.RightColor),
CustomizeParameterFlag.FeatureColor => new CustomizeParameterValue(parameter.OptionColor),
CustomizeParameterFlag.FacePaintUvMultiplier => new CustomizeParameterValue(GetUvMultiplier(parameter)),
CustomizeParameterFlag.FacePaintUvOffset => new CustomizeParameterValue(GetUvOffset(parameter)),
_ => CustomizeParameterValue.Zero,
};
public static Vector4 FromParameter(in DecalParameters parameter)
=> new CustomizeParameterValue(parameter.Color).InternalQuadruple;
private static bool SetIfDifferent(ref Vector3 val, Vector3 @new)
{
if (@new == val)
return false;
val = @new;
return true;
}
private static bool SetIfDifferent(ref float val, float @new)
{
if (@new == val)
return false;
val = @new;
return true;
}
private static bool SetIfDifferent(ref Vector4 val, Vector4 @new)
{
if (@new == val)
return false;
val = @new;
return true;
}
private static unsafe float GetUvOffset(in CustomizeParameter parameter)
{
// TODO CS Update
fixed (CustomizeParameter* ptr = &parameter)
{
return ((float*)ptr)[23];
}
}
private static unsafe ref float GetUvOffsetWrite(ref CustomizeParameter parameter)
{
// TODO CS Update
fixed (CustomizeParameter* ptr = &parameter)
{
return ref ((float*)ptr)[23];
}
}
private static unsafe float GetUvMultiplier(in CustomizeParameter parameter)
{
// TODO CS Update
fixed (CustomizeParameter* ptr = &parameter)
{
return ((float*)ptr)[15];
}
}
private static unsafe ref float GetUvMultiplierWrite(ref CustomizeParameter parameter)
{
// TODO CS Update
fixed (CustomizeParameter* ptr = &parameter)
{
return ref ((float*)ptr)[15];
}
}
}

View file

@ -1,74 +0,0 @@
namespace Glamourer.GameData;
[Flags]
public enum CustomizeParameterFlag : ushort
{
SkinDiffuse = 0x0001,
MuscleTone = 0x0002,
SkinSpecular = 0x0004,
LipDiffuse = 0x0008,
HairDiffuse = 0x0010,
HairSpecular = 0x0020,
HairHighlight = 0x0040,
LeftEye = 0x0080,
RightEye = 0x0100,
FeatureColor = 0x0200,
FacePaintUvMultiplier = 0x0400,
FacePaintUvOffset = 0x0800,
DecalColor = 0x1000,
LeftLimbalIntensity = 0x2000,
RightLimbalIntensity = 0x4000,
}
public static class CustomizeParameterExtensions
{
// Speculars are not available anymore.
public const CustomizeParameterFlag All = (CustomizeParameterFlag)0x7FDB;
public const CustomizeParameterFlag RgbTriples = All
& ~(RgbaQuadruples | Percentages | Values);
public const CustomizeParameterFlag RgbaQuadruples = CustomizeParameterFlag.DecalColor | CustomizeParameterFlag.LipDiffuse;
public const CustomizeParameterFlag Percentages = CustomizeParameterFlag.MuscleTone
| CustomizeParameterFlag.LeftLimbalIntensity
| CustomizeParameterFlag.RightLimbalIntensity;
public const CustomizeParameterFlag Values = CustomizeParameterFlag.FacePaintUvOffset | CustomizeParameterFlag.FacePaintUvMultiplier;
public static readonly IReadOnlyList<CustomizeParameterFlag> AllFlags = [.. Enum.GetValues<CustomizeParameterFlag>().Where(f => All.HasFlag(f))];
public static readonly IReadOnlyList<CustomizeParameterFlag> RgbaFlags = AllFlags.Where(f => RgbaQuadruples.HasFlag(f)).ToArray();
public static readonly IReadOnlyList<CustomizeParameterFlag> RgbFlags = AllFlags.Where(f => RgbTriples.HasFlag(f)).ToArray();
public static readonly IReadOnlyList<CustomizeParameterFlag> PercentageFlags = AllFlags.Where(f => Percentages.HasFlag(f)).ToArray();
public static readonly IReadOnlyList<CustomizeParameterFlag> ValueFlags = AllFlags.Where(f => Values.HasFlag(f)).ToArray();
public static int Count(this CustomizeParameterFlag flag)
=> RgbaQuadruples.HasFlag(flag) ? 4 : RgbTriples.HasFlag(flag) ? 3 : 1;
public static IEnumerable<CustomizeParameterFlag> Iterate(this CustomizeParameterFlag flags)
=> AllFlags.Where(f => flags.HasFlag(f));
public static int ToInternalIndex(this CustomizeParameterFlag flag)
=> BitOperations.TrailingZeroCount((uint)flag);
public static string ToName(this CustomizeParameterFlag flag)
=> flag switch
{
CustomizeParameterFlag.SkinDiffuse => "Skin Color",
CustomizeParameterFlag.MuscleTone => "Muscle Tone",
CustomizeParameterFlag.SkinSpecular => "Skin Shine",
CustomizeParameterFlag.LipDiffuse => "Lip Color",
CustomizeParameterFlag.HairDiffuse => "Hair Color",
CustomizeParameterFlag.HairSpecular => "Hair Shine",
CustomizeParameterFlag.HairHighlight => "Hair Highlights",
CustomizeParameterFlag.LeftEye => "Left Eye Color",
CustomizeParameterFlag.RightEye => "Right Eye Color",
CustomizeParameterFlag.FeatureColor => "Feature Color",
CustomizeParameterFlag.FacePaintUvMultiplier => "Multiplier for Face Paint",
CustomizeParameterFlag.FacePaintUvOffset => "Offset of Face Paint",
CustomizeParameterFlag.DecalColor => "Face Paint Color",
CustomizeParameterFlag.LeftLimbalIntensity => "Left Limbal Ring Intensity",
CustomizeParameterFlag.RightLimbalIntensity => "Right Limbal Ring Intensity",
_ => string.Empty,
};
}

Some files were not shown because too many files have changed in this diff Show more