This commit is contained in:
Ottermandias 2023-06-18 00:49:26 +02:00
parent 27f151c55a
commit d10cb3137f
14 changed files with 1366 additions and 373 deletions

View file

@ -55,7 +55,7 @@ public static class CustomizationExtensions
CustomizeIndex.Clan => (4, 0xFF),
CustomizeIndex.Face => (5, 0xFF),
CustomizeIndex.Hairstyle => (6, 0xFF),
CustomizeIndex.Highlights => (7, 0xFF),
CustomizeIndex.Highlights => (7, 0x80),
CustomizeIndex.SkinColor => (8, 0xFF),
CustomizeIndex.EyeColorRight => (9, 0xFF),
CustomizeIndex.HairColor => (10, 0xFF),

View file

@ -15,8 +15,12 @@ namespace Glamourer.Designs;
public class Design : ISavable
{
#region Data
internal Design(ItemManager items)
{ }
{
SetDefaultEquipment(items);
}
// Metadata
public const int FileVersion = 1;
@ -31,6 +35,22 @@ public class Design : ISavable
internal DesignData DesignData;
public void SetDefaultEquipment(ItemManager items)
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
DesignData.SetItem(slot, ItemManager.NothingItem(slot));
DesignData.SetStain(slot, 0);
}
DesignData.SetItem(EquipSlot.MainHand, items.DefaultSword);
DesignData.SetStain(EquipSlot.MainHand, 0);
DesignData.SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield));
DesignData.SetStain(EquipSlot.OffHand, 0);
}
#endregion
#region Application Data
[Flags]
@ -42,9 +62,9 @@ public class Design : ISavable
WriteProtected = 0x08,
}
private CustomizeFlag _applyCustomize;
private EquipFlag _applyEquip;
private DesignFlags _designFlags;
internal CustomizeFlag ApplyCustomize = CustomizeFlagExtensions.All;
internal EquipFlag ApplyEquip = EquipFlagExtensions.All;
private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible;
public bool DoApplyHatVisible()
=> _designFlags.HasFlag(DesignFlags.ApplyHatVisible);
@ -100,47 +120,47 @@ public class Design : ISavable
public bool DoApplyEquip(EquipSlot slot)
=> _applyEquip.HasFlag(slot.ToFlag());
=> ApplyEquip.HasFlag(slot.ToFlag());
public bool DoApplyStain(EquipSlot slot)
=> _applyEquip.HasFlag(slot.ToStainFlag());
=> ApplyEquip.HasFlag(slot.ToStainFlag());
public bool DoApplyCustomize(CustomizeIndex idx)
=> _applyCustomize.HasFlag(idx.ToFlag());
=> ApplyCustomize.HasFlag(idx.ToFlag());
internal bool SetApplyEquip(EquipSlot slot, bool value)
{
var newValue = value ? _applyEquip | slot.ToFlag() : _applyEquip & ~slot.ToFlag();
if (newValue == _applyEquip)
var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag();
if (newValue == ApplyEquip)
return false;
_applyEquip = newValue;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyStain(EquipSlot slot, bool value)
{
var newValue = value ? _applyEquip | slot.ToStainFlag() : _applyEquip & ~slot.ToStainFlag();
if (newValue == _applyEquip)
var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag();
if (newValue == ApplyEquip)
return false;
_applyEquip = newValue;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{
var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag();
if (newValue == _applyCustomize)
var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag();
if (newValue == ApplyCustomize)
return false;
_applyCustomize = newValue;
ApplyCustomize = newValue;
return true;
}
#endregion
#region ISavable
#region Serialization
public JObject JsonSerialize()
{
@ -207,17 +227,21 @@ public class Design : ISavable
return ret;
}
public static Design LoadDesign(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes)
#endregion
#region Deserialization
public static Design LoadDesign(CustomizationService customizations, ItemManager items, JObject json)
{
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch
{
1 => LoadDesignV1(customizeManager, items, json, out changes),
1 => LoadDesignV1(customizations, items, json),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
}
private static Design LoadDesignV1(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes)
private static Design LoadDesignV1(CustomizationService customizations, ItemManager items, JObject json)
{
static string[] ParseTags(JObject json)
{
@ -236,85 +260,23 @@ public class Design : ISavable
Tags = ParseTags(json),
LastEdit = json["LastEdit"]?.ToObject<DateTimeOffset>() ?? creationDate,
};
if (design.LastEdit < creationDate)
design.LastEdit = creationDate;
changes = LoadEquip(items, json["Equipment"], design);
changes |= LoadCustomize(customizeManager, json["Customize"], design);
LoadEquip(items, json["Equipment"], design);
LoadCustomize(customizations, json["Customize"], design);
return design;
}
private static bool ValidateItem(ItemManager items, EquipSlot slot, uint itemId, out EquipItem item)
{
item = items.Resolve(slot, itemId);
if (item.Valid)
return true;
Glamourer.Chat.NotificationMessage($"The {slot.ToName()} item {itemId} does not exist, reset to Nothing.", "Warning",
NotificationType.Warning);
item = ItemManager.NothingItem(slot);
return false;
}
private static bool ValidateStain(ItemManager items, StainId stain, out StainId ret)
{
if (stain.Value != 0 && !items.Stains.ContainsKey(stain))
{
ret = 0;
Glamourer.Chat.NotificationMessage($"The Stain {stain} does not exist, reset to unstained.");
return false;
}
ret = stain;
return true;
}
private static bool ValidateWeapons(ItemManager items, uint mainId, uint offId, out EquipItem main, out EquipItem off)
{
var ret = true;
main = items.Resolve(EquipSlot.MainHand, mainId);
if (!main.Valid)
{
Glamourer.Chat.NotificationMessage($"The mainhand weapon {mainId} does not exist, reset to default sword.", "Warning",
NotificationType.Warning);
main = items.DefaultSword;
ret = false;
}
off = items.Resolve(main.Type.Offhand(), offId);
if (off.Valid)
return ret;
ret = false;
off = items.Resolve(main.Type.Offhand(), mainId);
if (off.Valid)
{
Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to implied offhand.", "Warning",
NotificationType.Warning);
}
else
{
off = ItemManager.NothingItem(FullEquipType.Shield);
if (main.Type.Offhand() == FullEquipType.Shield)
{
Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to no offhand.", "Warning",
NotificationType.Warning);
}
else
{
main = items.DefaultSword;
Glamourer.Chat.NotificationMessage(
$"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing.",
"Warning",
NotificationType.Warning);
}
}
return ret;
}
private static bool LoadEquip(ItemManager items, JToken? equip, Design design)
private static void LoadEquip(ItemManager items, JToken? equip, Design design)
{
if (equip == null)
return true;
{
design.SetDefaultEquipment(items);
Glamourer.Chat.NotificationMessage("The loaded design does not contain any equipment data, reset to default.", "Warning",
NotificationType.Warning);
return;
}
static (uint, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item)
{
@ -325,13 +287,19 @@ public class Design : ISavable
return (id, stain, apply, applyStain);
}
var changes = false;
void PrintWarning(string msg)
{
if (msg.Length > 0)
Glamourer.Chat.NotificationMessage($"{msg} ({design.Name})", "Warning", NotificationType.Warning);
}
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
changes |= !ValidateItem(items, slot, id, out var item);
changes |= !ValidateStain(items, stain, out stain);
design.DesignData.SetItem(item);
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
PrintWarning(items.ValidateItem(slot, id, out var item));
PrintWarning(items.ValidateStain(stain, out stain));
design.DesignData.SetItem(slot, item);
design.DesignData.SetStain(slot, stain);
design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain);
@ -344,11 +312,12 @@ public class Design : ISavable
var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield);
changes |= ValidateWeapons(items, id, idOff, out var main, out var off);
changes |= ValidateStain(items, stain, out stain);
changes |= ValidateStain(items, stainOff, out stainOff);
design.DesignData.SetItem(main);
design.DesignData.SetItem(off);
PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off));
PrintWarning(items.ValidateStain(stain, out stain));
PrintWarning(items.ValidateStain(stainOff, out stainOff));
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);
@ -367,146 +336,89 @@ public class Design : ISavable
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
design.SetApplyVisorToggle(metaValue.Enabled);
design.DesignData.SetVisor(metaValue.ForcedValue);
return changes;
}
private static bool ValidateCustomize(CustomizationManager manager, ref Customize customize)
{
var ret = true;
if (!manager.Races.Contains(customize.Race))
{
ret = false;
if (manager.Clans.Contains(customize.Clan))
{
Glamourer.Chat.NotificationMessage(
$"The race {customize.Race.ToName()} is unknown, reset to {customize.Clan.ToRace().ToName()} from Clan.", "Warning",
NotificationType.Warning);
customize.Race = customize.Clan.ToRace();
}
else
{
Glamourer.Chat.NotificationMessage(
$"The race {customize.Race.ToName()} is unknown, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}.", "Warning",
NotificationType.Warning);
customize.Race = Race.Hyur;
customize.Clan = SubRace.Midlander;
}
}
if (!manager.Clans.Contains(customize.Clan))
{
ret = false;
var oldClan = customize.Clan;
customize.Clan = (SubRace)((byte)customize.Race * 2 - 1);
if (manager.Clans.Contains(customize.Clan))
{
Glamourer.Chat.NotificationMessage($"The clan {oldClan.ToName()} is unknown, reset to {customize.Clan.ToName()} from race.",
"Warning", NotificationType.Warning);
}
else
{
customize.Race = Race.Hyur;
customize.Clan = SubRace.Midlander;
Glamourer.Chat.NotificationMessage(
$"The clan {oldClan.ToName()} is unknown, reset to {customize.Race.ToName()} {customize.Clan.ToName()}.", "Warning",
NotificationType.Warning);
}
}
if (!manager.Genders.Contains(customize.Gender))
{
ret = false;
Glamourer.Chat.NotificationMessage($"The gender {customize.Gender} is unknown, reset to {Gender.Male.ToName()}.", "Warning",
NotificationType.Warning);
customize.Gender = Gender.Male;
}
// TODO: Female Hrothgar
if (customize.Gender == Gender.Female && customize.Race == Race.Hrothgar)
{
ret = false;
Glamourer.Chat.NotificationMessage($"Hrothgar do not currently support female characters, reset to male.", "Warning",
NotificationType.Warning);
customize.Gender = Gender.Male;
}
var list = manager.GetList(customize.Clan, customize.Gender);
// Face is handled first automatically so it should not conflict with other customizations when corrupt.
foreach (var index in Enum.GetValues<CustomizeIndex>().Where(list.IsAvailable))
{
var value = customize.Get(index);
var count = list.Count(index, customize.Face);
var idx = list.DataByValue(index, value, out var data, customize.Face);
if (idx >= 0 && idx < count)
continue;
ret = false;
var name = list.Option(index);
var newValue = list.Data(index, 0, customize.Face);
Glamourer.Chat.NotificationMessage(
$"Customization {name} for {customize.Race.ToName()} {customize.Gender.ToName()}s does not support value {value.Value}, reset to {newValue.Value.Value}");
customize.Set(index, newValue.Value);
}
return ret;
}
private static bool ValidateModelId(ref uint modelId)
{
if (modelId != 0)
{
Glamourer.Chat.NotificationMessage($"Model IDs different from 0 are not currently allowed, reset {modelId} to 0.", "Warning",
NotificationType.Warning);
modelId = 0;
return false;
}
return true;
}
private static bool LoadCustomize(CustomizationManager manager, JToken? json, Design design)
private static void LoadCustomize(CustomizationService customizations, JToken? json, Design design)
{
if (json == null)
return true;
{
design.DesignData.ModelId = 0;
design.DesignData.Customize = Customize.Default;
Glamourer.Chat.NotificationMessage("The loaded design does not contain any customization data, reset to default.", "Warning",
NotificationType.Warning);
return;
}
void PrintWarning(string msg)
{
if (msg.Length > 0)
Glamourer.Chat.NotificationMessage($"{msg} ({design.Name})", "Warning", NotificationType.Warning);
}
design.DesignData.ModelId = json["ModelId"]?.ToObject<uint>() ?? 0;
var ret = !ValidateModelId(ref design.DesignData.ModelId);
PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId));
foreach (var idx in Enum.GetValues<CustomizeIndex>())
var race = (Race)(json[CustomizeIndex.Race.ToString()]?["Value"]?.ToObject<byte>() ?? 0);
var clan = (SubRace)(json[CustomizeIndex.Clan.ToString()]?["Value"]?.ToObject<byte>() ?? 0);
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));
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 Enum.GetValues<CustomizeIndex>().Where(set.IsAvailable))
{
var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
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));
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
design.DesignData.Customize[idx] = data;
design.SetApplyCustomize(idx, apply);
}
design.DesignData.SetIsWet(json["IsWet"]?.ToObject<bool>() ?? false);
ret |= !ValidateCustomize(manager, ref design.DesignData.Customize);
return ret;
}
//public void MigrateBase64(ItemManager items, string base64)
//{
// var data = DesignBase64Migration.MigrateBase64(items, base64, out var applyEquip, out var applyCustomize, out var writeProtected, out var wet,
// out var hat,
// out var visor, out var weapon);
// UpdateMainhand(items, data.MainHand);
// UpdateOffhand(items, data.OffHand);
// foreach (var slot in EquipSlotExtensions.EqdpSlots)
// UpdateArmor(items, slot, data.Armor(slot), true);
// ModelData.Customize = data.Customize;
// _applyEquip = applyEquip;
// _applyCustomize = applyCustomize;
// WriteProtected = writeProtected;
// Wetness = wet;
// Hat = hat;
// Visor = visor;
// Weapon = weapon;
//}
#endregion
#region ISavable
public string ToFilename(FilenameService fileNames)
=> fileNames.DesignFile(this);
public void Save(StreamWriter writer)
{
using var j = new JsonTextWriter(writer)
{
Formatting = Formatting.Indented,
};
var obj = JsonSerialize();
obj.WriteTo(j);
}
public string LogName(string fileName)
=> Path.GetFileNameWithoutExtension(fileName);
#endregion
public void MigrateBase64(ItemManager items, string base64)
{
DesignData = DesignBase64Migration.MigrateBase64(items, 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);
SetApplyHatVisible(applyHat);
SetApplyVisorToggle(applyVisor);
SetApplyWeaponVisible(applyWeapon);
}
//
//public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip)
//{
@ -528,22 +440,4 @@ public class Design : ISavable
// => DesignBase64Migration.CreateOldBase64(in ModelData, _applyEquip, _applyCustomize, Wetness == QuadBool.True, Hat.ForcedValue,
// Hat.Enabled,
// Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f);
public string ToFilename(FilenameService fileNames)
=> fileNames.DesignFile(this);
public void Save(StreamWriter writer)
{
using var j = new JsonTextWriter(writer)
{
Formatting = Formatting.Indented,
};
var obj = JsonSerialize();
obj.WriteTo(j);
}
public string LogName(string fileName)
=> Path.GetFileNameWithoutExtension(fileName);
#endregion
}

View file

@ -78,16 +78,16 @@ public static class DesignBase64Migration
if (!main.Valid)
throw new Exception($"Base64 string invalid, weapon could not be identified.");
data.SetItem(main);
data.SetItem(EquipSlot.MainHand, main);
data.SetStain(EquipSlot.MainHand, cur[0].Stain);
var off = items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, (byte)cur[1].Variant, main.Type);
if (!off.Valid)
throw new Exception($"Base64 string invalid, weapon could not be identified.");
data.SetItem(off);
data.SetItem(EquipSlot.OffHand, off);
data.SetStain(EquipSlot.OffHand, cur[1].Stain);
var eq = (CharacterArmor*)(ptr + 46);
var eq = (CharacterArmor*)(cur + 2);
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{
var mdl = eq[idx];
@ -95,7 +95,7 @@ public static class DesignBase64Migration
if (!item.Valid)
throw new Exception($"Base64 string invalid, item could not be identified.");
data.SetItem(item);
data.SetItem(slot, item);
data.SetStain(slot, mdl.Stain);
}
}
@ -130,20 +130,13 @@ public static class DesignBase64Migration
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));
((CharacterArmor*)(data + 46))[0] = save.Item(EquipSlot.Head).Armor(save.Stain(EquipSlot.Head));
((CharacterArmor*)(data + 46))[1] = save.Item(EquipSlot.Body).Armor(save.Stain(EquipSlot.Body));
((CharacterArmor*)(data + 46))[2] = save.Item(EquipSlot.Hands).Armor(save.Stain(EquipSlot.Hands));
((CharacterArmor*)(data + 46))[3] = save.Item(EquipSlot.Legs).Armor(save.Stain(EquipSlot.Legs));
((CharacterArmor*)(data + 46))[4] = save.Item(EquipSlot.Feet).Armor(save.Stain(EquipSlot.Feet));
((CharacterArmor*)(data + 46))[5] = save.Item(EquipSlot.Ears).Armor(save.Stain(EquipSlot.Ears));
((CharacterArmor*)(data + 46))[6] = save.Item(EquipSlot.Neck).Armor(save.Stain(EquipSlot.Neck));
((CharacterArmor*)(data + 46))[7] = save.Item(EquipSlot.Wrists).Armor(save.Stain(EquipSlot.Wrists));
((CharacterArmor*)(data + 46))[8] = save.Item(EquipSlot.RFinger).Armor(save.Stain(EquipSlot.RFinger));
((CharacterArmor*)(data + 46))[9] = save.Item(EquipSlot.LFinger).Armor(save.Stain(EquipSlot.LFinger));
*(float*)(data + 86) = 1f;
data[90] = (byte)((save.IsHatVisible() ? 0x01 : 0)
foreach (var slot in EquipSlotExtensions.EqdpSlots)
((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)
| (save.IsVisorToggled() ? 0x10 : 0)
| (save.IsWeaponVisible() ? 0x02 : 0));
| (save.IsWeaponVisible() ? 0x00 : 0x02));
return Convert.ToBase64String(new Span<byte>(data, Base64Size));
}

View file

@ -60,9 +60,9 @@ public unsafe struct DesignData
// @formatter:on
};
public bool SetItem(EquipItem item)
public bool SetItem(EquipSlot slot, EquipItem item)
{
var index = item.Type.ToSlot().ToIndex();
var index = slot.ToIndex();
if (index > 11 || _itemIds[index] == item.Id)
return false;

View file

@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Glamourer.Events;
using Glamourer.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
namespace Glamourer.Designs;
public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
{
private readonly DesignChanged _designChanged;
private readonly SaveService _saveService;
private readonly DesignManager _designManager;
public DesignFileSystem(DesignManager designManager, SaveService saveService, DesignChanged designChanged)
{
_designManager = designManager;
_saveService = saveService;
_designChanged = designChanged;
_designChanged.Subscribe(OnDataChange, DesignChanged.Priority.DesignFileSystem);
Changed += OnChange;
Reload();
}
private void Reload()
{
if (Load(new FileInfo(_saveService.FileNames.DesignFileSystem), _designManager.Designs, DesignToIdentifier, DesignToName))
_saveService.ImmediateSave(this);
Glamourer.Log.Debug("Reloaded design filesystem.");
}
public void Dispose()
{
_designChanged.Unsubscribe(OnDataChange);
}
public struct CreationDate : ISortMode<Design>
{
public string Name
=> "Creation Date (Older First)";
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));
}
public struct UpdateDate : ISortMode<Design>
{
public string Name
=> "Update Date (Older First)";
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));
}
public struct InverseCreationDate : ISortMode<Design>
{
public string Name
=> "Creation Date (Newer First)";
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));
}
public struct InverseUpdateDate : ISortMode<Design>
{
public string Name
=> "Update Date (Newer First)";
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));
}
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
{
if (type != FileSystemChangeType.Reload)
_saveService.QueueSave(this);
}
private void OnDataChange(DesignChanged.Type type, Design design, object? data)
{
switch (type)
{
case DesignChanged.Type.Created:
var originalName = design.Name.Text.FixName();
var name = originalName;
var counter = 1;
while (Find(name, out _))
name = $"{originalName} ({++counter})";
CreateLeaf(Root, name, design);
break;
case DesignChanged.Type.Deleted:
if (FindLeaf(design, out var leaf))
Delete(leaf);
break;
case DesignChanged.Type.ReloadedAll:
Reload();
break;
case DesignChanged.Type.Renamed when data is string oldName:
var old = oldName.FixName();
if (Find(old, out var child) && child is not Folder)
Rename(child, design.Name);
break;
}
}
// Used for saving and loading.
private static string DesignToIdentifier(Design design)
=> design.Identifier.ToString();
private static string DesignToName(Design design)
=> design.Name.Text.FixName();
private static bool DesignHasDefaultPath(Design design, string fullPath)
{
var regex = new Regex($@"^{Regex.Escape(DesignToName(design))}( \(\d+\))?$");
return regex.IsMatch(fullPath);
}
private static (string, bool) SaveDesign(Design design, string fullPath)
// Only save pairs with non-default paths.
=> DesignHasDefaultPath(design, fullPath)
? (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)
return;
var file = saveService.FileNames.DesignFileSystem;
try
{
JObject jObject;
if (File.Exists(file))
{
var text = File.ReadAllText(file);
jObject = JObject.Parse(text);
var dict = jObject["Data"]?.ToObject<Dictionary<string, string>>();
if (dict != null)
foreach (var (key, value) in dict)
oldPaths.TryAdd(key, value);
jObject["Data"] = JToken.FromObject(oldPaths);
}
else
{
jObject = new JObject
{
["Data"] = JToken.FromObject(oldPaths),
["EmptyFolders"] = JToken.FromObject(Array.Empty<string>()),
};
}
var data = jObject.ToString(Formatting.Indented);
File.WriteAllText(file, data);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not migrate old folder paths to new version:\n{ex}");
}
}
public string ToFilename(FilenameService fileNames)
=> fileNames.DesignFileSystem;
public void Save(StreamWriter writer)
{
SaveToFile(writer, SaveDesign, true);
}
}

View file

@ -0,0 +1,470 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Utility;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignManager
{
private readonly CustomizationService _customizations;
private readonly ItemManager _items;
private readonly SaveService _saveService;
private readonly DesignChanged _event;
private readonly List<Design> _designs = new();
public IReadOnlyList<Design> Designs
=> _designs;
public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations,
DesignChanged @event)
{
_saveService = saveService;
_items = items;
_customizations = customizations;
_event = @event;
CreateDesignFolder(saveService);
LoadDesigns();
MigrateOldDesigns();
}
/// <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>
public void LoadDesigns()
{
_designs.Clear();
List<(Design, string)> invalidNames = new();
var skipped = 0;
foreach (var file in _saveService.FileNames.Designs())
{
try
{
var text = File.ReadAllText(file.FullName);
var data = JObject.Parse(text);
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}");
++skipped;
}
}
var failed = MoveInvalidNames(invalidNames);
if (invalidNames.Count > 0)
Glamourer.Log.Information(
$"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.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
_event.Invoke(DesignChanged.Type.ReloadedAll, null!);
}
/// <summary> Create a new design of a given name. </summary>
public Design Create(string name)
{
var design = new Design(_items)
{
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Index = _designs.Count,
Name = name,
};
_designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
_saveService.ImmediateSave(design);
_event.Invoke(DesignChanged.Type.Created, design);
return design;
}
/// <summary> Delete a design. </summary>
public void Delete(Design design)
{
foreach (var d in _designs.Skip(design.Index + 1))
--d.Index;
_designs.RemoveAt(design.Index);
_saveService.ImmediateDelete(design);
_event.Invoke(DesignChanged.Type.Deleted, design);
}
/// <summary> Rename a design. </summary>
public void Rename(Design design, string newName)
{
var oldName = design.Name.Text;
if (oldName == newName)
return;
design.Name = newName;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
_event.Invoke(DesignChanged.Type.Renamed, design, oldName);
}
/// <summary> Change the description of a design. </summary>
public void ChangeDescription(Design design, string description)
{
var oldDescription = design.Description;
if (oldDescription == description)
return;
design.Description = description;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Changed description of design {design.Identifier}.");
_event.Invoke(DesignChanged.Type.ChangedDescription, design, oldDescription);
}
/// <summary> Add a new tag to a design. The tags remain sorted. </summary>
public void AddTag(Design design, string tag)
{
if (design.Tags.Contains(tag))
return;
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
design.LastEdit = DateTimeOffset.UtcNow;
var idx = design.Tags.IndexOf(tag);
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}.");
_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)
{
if (tagIdx < 0 || tagIdx >= design.Tags.Length)
return;
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);
Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}.");
_event.Invoke(DesignChanged.Type.RemovedTag, design, (oldTag, tagIdx));
}
/// <summary> Rename a tag from a design by its index. The tags stay sorted.</summary>
public void RenameTag(Design design, int tagIdx, string newTag)
{
var oldTag = design.Tags[tagIdx];
if (oldTag == newTag)
return;
design.Tags[tagIdx] = newTag;
Array.Sort(design.Tags);
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
_event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx));
}
/// <summary> Change a customization value. </summary>
public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value)
{
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))
return;
break;
case CustomizeIndex.Gender:
if (!_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1)))
return;
break;
default:
if (!design.DesignData.Customize.Set(idx, value))
return;
break;
}
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)
{
if (!design.SetApplyCustomize(idx, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {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.ValidateItem(slot, item.Id, out item).Length > 0)
return;
var old = design.DesignData.Item(slot);
if (!design.DesignData.SetItem(slot, item))
return;
Glamourer.Log.Debug(
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}).");
_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 (item.Type == currentMain.Type)
{
if (_items.ValidateWeapons(item.Id, currentOff.Id, out _, out _).Length != 0)
return;
}
else
{
var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type)
? item.Id
: ItemManager.NothingId(item.Type.Offhand());
if (_items.ValidateWeapons(item.Id, newOffId, out _, out newOff).Length != 0)
return;
}
design.DesignData.SetItem(EquipSlot.MainHand, item);
design.DesignData.SetItem(EquipSlot.OffHand, newOff);
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug(
$"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.Id}) to {item.Name} ({item.Id}).");
_event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff));
return;
case EquipSlot.OffHand:
if (item.Type != currentOff.Type)
return;
if (_items.ValidateWeapons(currentMain.Id, item.Id, out _, out _).Length > 0)
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.Id}) to {item.Name} ({item.Id}).");
_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 ChangeApplyEquip(Design design, EquipSlot slot, bool value)
{
if (!design.SetApplyEquip(slot, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}.");
_event.Invoke(DesignChanged.Type.ApplyEquip, design, slot);
}
/// <summary> Change the stain for any equipment piece. </summary>
public void ChangeStain(Design design, EquipSlot slot, StainId stain)
{
if (_items.ValidateStain(stain, out _).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 stain of {slot} equipment piece to {stain.Value}.");
_event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot));
}
/// <summary> Change whether to apply a specific stain. </summary>
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
{
if (!design.SetApplyStain(slot, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}.");
_event.Invoke(DesignChanged.Type.ApplyStain, design, slot);
}
private void MigrateOldDesigns()
{
if (!File.Exists(_saveService.FileNames.MigrationDesignFile))
return;
var errors = 0;
var skips = 0;
var successes = 0;
try
{
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)
{
try
{
var actualName = Path.GetFileName(name);
var design = new Design(_items)
{
CreationDate = File.GetCreationTimeUtc(_saveService.FileNames.MigrationDesignFile),
LastEdit = File.GetLastWriteTimeUtc(_saveService.FileNames.MigrationDesignFile),
Identifier = CreateNewGuid(),
Name = actualName,
};
design.MigrateBase64(_items, base64);
if (!_designs.Any(d => d.Name == design.Name && d.CreationDate == design.CreationDate))
{
Add(design, $"Migrated old design to {design.Identifier}.");
migratedFileSystemPaths.Add(design.Identifier.ToString(), name);
++successes;
}
else
{
Glamourer.Log.Debug(
"Skipped migrating old design because a design of the same name and creation date already existed.");
++skips;
}
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}");
++errors;
}
}
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}");
}
try
{
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}");
}
}
/// <summary> Try to ensure existence of the design folder. </summary>
private static void CreateDesignFolder(SaveService service)
{
var ret = service.FileNames.DesignDirectory;
if (Directory.Exists(ret))
return;
try
{
Directory.CreateDirectory(ret);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not create design folder directory at {ret}:\n{ex}");
}
}
/// <summary> Move all files that were discovered to have names not corresponding to their identifier to correct names, if possible. </summary>
/// <returns>The number of files that could not be moved.</returns>
private int MoveInvalidNames(IEnumerable<(Design, string)> invalidNames)
{
var failed = 0;
foreach (var (design, name) in invalidNames)
{
try
{
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)}.");
}
catch (Exception ex)
{
++failed;
Glamourer.Log.Error($"Failed to move invalid design file from {Path.GetFileName(name)}:\n{ex}");
}
}
return failed;
}
/// <summary> Create new GUIDs until we have one that is not in use. </summary>
private Guid CreateNewGuid()
{
while (true)
{
var guid = Guid.NewGuid();
if (_designs.All(d => d.Identifier != guid))
return guid;
}
}
/// <summary>
/// Try to add an external design to the list.
/// 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 bool Add(Design design, string? message)
{
if (_designs.Any(d => d == design || d.Identifier == design.Identifier))
return false;
design.Index = _designs.Count;
_designs.Add(design);
if (!message.IsNullOrEmpty())
Glamourer.Log.Debug(message);
_saveService.ImmediateSave(design);
_event.Invoke(DesignChanged.Type.Created, design);
return true;
}
}

View file

@ -0,0 +1,76 @@
using System;
using Glamourer.Designs;
using OtterGui.Classes;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a Design is edited in any way.
/// <list type="number">
/// <item>Parameter is the type of the change </item>
/// <item>Parameter is the changed Design. </item>
/// <item>Parameter is any additional data depending on the type of change. </item>
/// </list>
/// </summary>
public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Design, object?>, DesignChanged.Priority>
{
public enum Type
{
/// <summary> A new design was created. Data is null. </summary>
Created,
/// <summary> An existing design was deleted. Data is null. </summary>
Deleted,
/// <summary> Invoked on full reload. Design and Data are null. </summary>
ReloadedAll,
/// <summary> An existing design was renamed. Data is the prior name [string]. </summary>
Renamed,
/// <summary> An existing design had its description changed. Data is the prior description [string]. </summary>
ChangedDescription,
/// <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. 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. 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 customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. </summary>
Customize,
/// <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 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. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Stain,
/// <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 is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyEquip,
/// <summary> An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyStain,
}
public enum Priority
{
DesignFileSystem = 0,
}
public DesignChanged()
: base(nameof(DesignChanged))
{ }
public void Invoke(Type type, Design design, object? data = null)
=> Invoke(this, type, design, data);
}

View file

@ -19,7 +19,6 @@ using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using static OtterGui.Raii.ImRaii;
namespace Glamourer.Gui.Tabs;
@ -37,11 +36,15 @@ public unsafe class DebugTab : ITab
private readonly ActorService _actors;
private readonly CustomizationService _customization;
private readonly DesignManager _designManager;
private readonly DesignFileSystem _designFileSystem;
private int _gameObjectIndex;
public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects,
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier,
ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager)
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra,
ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager,
DesignFileSystem designFileSystem, DesignManager designManager)
{
_changeCustomizeService = changeCustomizeService;
_visorService = visorService;
@ -53,6 +56,8 @@ public unsafe class DebugTab : ITab
_items = items;
_customization = customization;
_objectManager = objectManager;
_designFileSystem = designFileSystem;
_designManager = designManager;
}
public ReadOnlySpan<byte> Label
@ -63,7 +68,7 @@ public unsafe class DebugTab : ITab
DrawInteropHeader();
DrawGameDataHeader();
DrawPenumbraHeader();
DrawDesignManager();
DrawDesigns();
}
#region Interop
@ -79,14 +84,14 @@ public unsafe class DebugTab : ITab
private void DrawModelEvaluation()
{
using var tree = TreeNode("Model Evaluation");
using var tree = ImRaii.TreeNode("Model Evaluation");
if (!tree)
return;
ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0);
var actor = (Actor)_objects.GetObjectAddress(_gameObjectIndex);
var model = actor.Model;
using var table = Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
using var table = ImRaii.Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.TableHeader("Actor");
@ -127,13 +132,13 @@ public unsafe class DebugTab : ITab
private void DrawObjectManager()
{
using var tree = TreeNode("Object Manager");
using var tree = ImRaii.TreeNode("Object Manager");
if (!tree)
return;
_objectManager.Update();
using (var table = Table("##data", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
using (var table = ImRaii.Table("##data", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
{
if (!table)
return;
@ -169,7 +174,7 @@ public unsafe class DebugTab : ITab
}
var filterChanged = ImGui.InputTextWithHint("##Filter", "Filter...", ref _objectFilter, 64);
using var table2 = Table("##data2", 3,
using var table2 = ImRaii.Table("##data2", 3,
ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY,
new Vector2(-1, 20 * ImGui.GetTextLineHeightWithSpacing()));
if (!table2)
@ -195,7 +200,7 @@ public unsafe class DebugTab : ITab
private void DrawVisor(Actor actor, Model model)
{
using var id = PushId("Visor");
using var id = ImRaii.PushId("Visor");
ImGuiUtil.DrawTableColumn("Visor State");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsVisorToggled.ToString() : "No Character");
ImGuiUtil.DrawTableColumn(model.IsHuman ? _visorService.GetVisorState(model).ToString() : "No Human");
@ -215,7 +220,7 @@ public unsafe class DebugTab : ITab
private void DrawHatState(Actor actor, Model model)
{
using var id = PushId("HatState");
using var id = ImRaii.PushId("HatState");
ImGuiUtil.DrawTableColumn("Hat State");
ImGuiUtil.DrawTableColumn(actor.IsCharacter
? actor.AsCharacter->DrawData.IsHatHidden ? "Hidden" : actor.GetArmor(EquipSlot.Head).ToString()
@ -240,7 +245,7 @@ public unsafe class DebugTab : ITab
private void DrawWeaponState(Actor actor, Model model)
{
using var id = PushId("WeaponState");
using var id = ImRaii.PushId("WeaponState");
ImGuiUtil.DrawTableColumn("Weapon State");
ImGuiUtil.DrawTableColumn(actor.IsCharacter
? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible"
@ -272,7 +277,7 @@ public unsafe class DebugTab : ITab
private void DrawWetness(Actor actor, Model model)
{
using var id = PushId("Wetness");
using var id = ImRaii.PushId("Wetness");
ImGuiUtil.DrawTableColumn("Wetness");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->IsGPoseWet ? "GPose" : "None" : "No Character");
var modelString = model.IsCharacterBase
@ -298,10 +303,10 @@ public unsafe class DebugTab : ITab
private void DrawEquip(Actor actor, Model model)
{
using var id = PushId("Equipment");
using var id = ImRaii.PushId("Equipment");
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
using var id2 = PushId((int)slot);
using var id2 = ImRaii.PushId((int)slot);
ImGuiUtil.DrawTableColumn(slot.ToName());
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetArmor(slot).ToString() : "No Character");
ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetArmor(slot).ToString() : "No Human");
@ -323,7 +328,7 @@ public unsafe class DebugTab : ITab
private void DrawCustomize(Actor actor, Model model)
{
using var id = PushId("Customize");
using var id = ImRaii.PushId("Customize");
var actorCustomize = new Customize(actor.IsCharacter
? *(Penumbra.GameData.Structs.CustomizeData*)&actor.AsCharacter->DrawData.CustomizeData
: new Penumbra.GameData.Structs.CustomizeData());
@ -332,7 +337,7 @@ public unsafe class DebugTab : ITab
: new Penumbra.GameData.Structs.CustomizeData());
foreach (var type in Enum.GetValues<CustomizeIndex>())
{
using var id2 = PushId((int)type);
using var id2 = ImRaii.PushId((int)type);
ImGuiUtil.DrawTableColumn(type.ToDefaultName());
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actorCustomize[type].Value.ToString("X2") : "No Character");
ImGuiUtil.DrawTableColumn(model.IsHuman ? modelCustomize[type].Value.ToString("X2") : "No Human");
@ -373,7 +378,7 @@ public unsafe class DebugTab : ITab
if (!ImGui.CollapsingHeader("Penumbra"))
return;
using var table = Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
@ -410,7 +415,7 @@ public unsafe class DebugTab : ITab
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0);
ImGui.TableNextColumn();
using (var disabled = Disabled(!_penumbra.Available))
using (var disabled = ImRaii.Disabled(!_penumbra.Available))
{
if (ImGui.SmallButton("Redraw"))
_penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw);
@ -441,8 +446,8 @@ public unsafe class DebugTab : ITab
private void DrawIdentifierService()
{
using var disabled = Disabled(!_items.IdentifierService.Valid);
using var tree = TreeNode("Identifier Service");
using var disabled = ImRaii.Disabled(!_items.IdentifierService.Valid);
using var tree = ImRaii.TreeNode("Identifier Service");
if (!tree || !_items.IdentifierService.Valid)
return;
@ -486,7 +491,7 @@ public unsafe class DebugTab : ITab
private void DrawRestrictedGear()
{
using var tree = TreeNode("Restricted Gear Service");
using var tree = ImRaii.TreeNode("Restricted Gear Service");
if (!tree)
return;
@ -537,8 +542,8 @@ public unsafe class DebugTab : ITab
private void DrawActorService()
{
using var disabled = Disabled(!_actors.Valid);
using var tree = TreeNode("Actor Service");
using var disabled = ImRaii.Disabled(!_actors.Valid);
using var tree = ImRaii.TreeNode("Actor Service");
if (!tree || !_actors.Valid)
return;
@ -554,14 +559,14 @@ public unsafe class DebugTab : ITab
private static void DrawNameTable(string label, ref string filter, IEnumerable<(uint, string)> names)
{
using var _ = PushId(label);
using var tree = TreeNode(label);
using var _ = ImRaii.PushId(label);
using var tree = ImRaii.TreeNode(label);
if (!tree)
return;
var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref filter, 256);
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter,
using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter,
new Vector2(-1, 10 * height));
if (!table)
return;
@ -588,13 +593,13 @@ public unsafe class DebugTab : ITab
private void DrawItemService()
{
using var disabled = Disabled(!_items.ItemService.Valid);
using var tree = TreeNode("Item Manager");
using var disabled = ImRaii.Disabled(!_items.ItemService.Valid);
using var tree = ImRaii.TreeNode("Item Manager");
if (!tree || !_items.ItemService.Valid)
return;
disabled.Dispose();
TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})",
ImRaii.TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})",
ImGuiTreeNodeFlags.Leaf).Dispose();
DrawNameTable("All Items (Main)", ref _itemFilter,
_items.ItemService.AwaitedService.AllItems(true).Select(p => (p.Item1,
@ -616,13 +621,13 @@ public unsafe class DebugTab : ITab
private void DrawStainService()
{
using var tree = TreeNode("Stain Service");
using var tree = ImRaii.TreeNode("Stain Service");
if (!tree)
return;
var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _stainFilter, 256);
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
using var table = Table("##table", 4,
using var table = ImRaii.Table("##table", 4,
ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit,
new Vector2(-1, 10 * height));
if (!table)
@ -652,8 +657,8 @@ public unsafe class DebugTab : ITab
private void DrawCustomizationService()
{
using var disabled = Disabled(!_customization.Valid);
using var tree = TreeNode("Customization Service");
using var disabled = ImRaii.Disabled(!_customization.Valid);
using var tree = ImRaii.TreeNode("Customization Service");
if (!tree || !_customization.Valid)
return;
@ -668,11 +673,11 @@ public unsafe class DebugTab : ITab
private void DrawCustomizationInfo(CustomizationSet set)
{
using var tree = TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}");
using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}");
if (!tree)
return;
using var table = Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
using var table = ImRaii.Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
@ -697,11 +702,42 @@ public unsafe class DebugTab : ITab
private DesignData _parse64 = new();
private Exception? _parse64Failure;
private void DrawDesignManager()
private void DrawDesigns()
{
if (!ImGui.CollapsingHeader("Designs"))
return;
DrawDesignManager();
DrawDesignTester();
}
private void DrawDesignManager()
{
using var tree = ImRaii.TreeNode($"Design Manager ({_designManager.Designs.Count} Designs)###Design Manager");
if (!tree)
return;
foreach (var (design, idx) in _designManager.Designs.WithIndex())
{
using var t = ImRaii.TreeNode($"{design.Name}##{idx}");
if (!t)
continue;
DrawDesign(design);
var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomize, design.DoApplyHatVisible(),
design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected());
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtil.TextWrapped(base64);
}
}
private void DrawDesignTester()
{
using var tree = ImRaii.TreeNode("Base64 Design Tester");
if (!tree)
return;
ImGui.SetNextItemWidth(-1);
ImGui.InputTextWithHint("##base64", "Base 64 input...", ref _base64, 2048);
if (ImGui.IsItemDeactivatedAfterEdit())
{
@ -721,7 +757,7 @@ public unsafe class DebugTab : ITab
{
_parse64 = DesignBase64Migration.MigrateBase64(_items, _base64, out var ef, out var cf, out var wp, out var ah, out var av,
out var aw);
_restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, ah, av, wp, aw);
_restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, ah, av, aw, wp);
_restoreBytes = Convert.FromBase64String(_restore);
}
catch (Exception ex)
@ -737,14 +773,14 @@ public unsafe class DebugTab : ITab
}
else if (_restore.Length > 0)
{
DrawDesignData(_parse64);
using var font = PushFont(UiBuilder.MonoFont);
DrawDesignData(_parse64, true);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
ImGui.TextUnformatted(_base64);
using (var style = PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 }))
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 }))
{
foreach (var (c1, c2) in _restore.Zip(_base64))
{
using var color = PushColor(ImGuiCol.Text, 0xFF4040D0, c1 != c2);
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040D0, c1 != c2);
ImGui.TextUnformatted(c1.ToString());
ImGui.SameLine();
}
@ -754,11 +790,11 @@ public unsafe class DebugTab : ITab
foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex())
{
using (var group = Group())
using (var group = ImRaii.Group())
{
ImGui.TextUnformatted(idx.ToString("D2"));
ImGui.TextUnformatted(b1.ToString("X2"));
using var color = PushColor(ImGuiCol.Text, 0xFF4040D0, b1 != b2);
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040D0, b1 != b2);
ImGui.TextUnformatted(b2.ToString("X2"));
}
@ -768,10 +804,10 @@ public unsafe class DebugTab : ITab
if (_parse64Failure != null && _base64Bytes.Length > 0)
{
using var font = PushFont(UiBuilder.MonoFont);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
foreach (var (b, idx) in _base64Bytes.WithIndex())
{
using (var group = Group())
using (var group = ImRaii.Group())
{
ImGui.TextUnformatted(idx.ToString("D2"));
ImGui.TextUnformatted(b.ToString("X2"));
@ -782,9 +818,9 @@ public unsafe class DebugTab : ITab
}
}
private static void DrawDesignData(in DesignData data)
private static void DrawDesignData(in DesignData data, bool createTable)
{
using var table = Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
using var table = createTable ? ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit) : null;
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
var item = data.Item(slot);
@ -822,5 +858,79 @@ public unsafe class DebugTab : ITab
ImGui.TableNextRow();
}
private void DrawDesign(Design design)
{
using var table = ImRaii.Table("##equip", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
ImGuiUtil.DrawTableColumn("Name");
ImGuiUtil.DrawTableColumn(design.Name);
ImGuiUtil.DrawTableColumn($"({design.Index})");
ImGui.TableNextColumn();
ImGui.TextUnformatted("Description (Hover)");
ImGuiUtil.HoverTooltip(design.Description);
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Identifier");
ImGuiUtil.DrawTableColumn(design.Identifier.ToString());
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Design File System Path");
ImGuiUtil.DrawTableColumn(_designFileSystem.FindLeaf(design, out var leaf) ? leaf.FullName() : "No Path Known");
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Creation");
ImGuiUtil.DrawTableColumn(design.CreationDate.ToString());
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Update");
ImGuiUtil.DrawTableColumn(design.LastEdit.ToString());
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Tags");
ImGuiUtil.DrawTableColumn(string.Join(", ", design.Tags));
ImGui.TableNextRow();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
var item = design.DesignData.Item(slot);
var apply = design.DoApplyEquip(slot);
var stain = design.DesignData.Stain(slot);
var applyStain = design.DoApplyStain(slot);
ImGuiUtil.DrawTableColumn(slot.ToName());
ImGuiUtil.DrawTableColumn(item.Name);
ImGuiUtil.DrawTableColumn(item.Id.ToString());
ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep");
ImGuiUtil.DrawTableColumn(stain.ToString());
ImGuiUtil.DrawTableColumn(applyStain ? "Apply" : "Keep");
}
ImGuiUtil.DrawTableColumn("Hat Visible");
ImGuiUtil.DrawTableColumn(design.DesignData.IsHatVisible().ToString());
ImGuiUtil.DrawTableColumn(design.DoApplyHatVisible() ? "Apply" : "Keep");
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Visor Toggled");
ImGuiUtil.DrawTableColumn(design.DesignData.IsVisorToggled().ToString());
ImGuiUtil.DrawTableColumn(design.DoApplyVisorToggle() ? "Apply" : "Keep");
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Weapon Visible");
ImGuiUtil.DrawTableColumn(design.DesignData.IsWeaponVisible().ToString());
ImGuiUtil.DrawTableColumn(design.DoApplyWeaponVisible() ? "Apply" : "Keep");
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Model ID");
ImGuiUtil.DrawTableColumn(design.DesignData.ModelId.ToString());
ImGui.TableNextRow();
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
var value = design.DesignData.Customize[index];
var apply = design.DoApplyCustomize(index);
ImGuiUtil.DrawTableColumn(index.ToDefaultName());
ImGuiUtil.DrawTableColumn(value.Value.ToString());
ImGuiUtil.DrawTableColumn(apply ? "Apply" : "Keep");
ImGui.TableNextRow();
}
ImGuiUtil.DrawTableColumn("Is Wet");
ImGuiUtil.DrawTableColumn(design.DesignData.IsWet().ToString());
ImGui.TableNextRow();
}
#endregion
}

View file

@ -0,0 +1,211 @@
using System;
using System.Linq;
using System.Security.AccessControl;
using Dalamud.Data;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
namespace Glamourer.Services;
public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationManager>
{
public CustomizationService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData))
{ }
/// <summary> In languages other than english the actual clan name may depend on gender. </summary>
public string ClanName(SubRace race, Gender gender)
{
if (gender == Gender.FemaleNpc)
gender = Gender.Female;
if (gender == Gender.MaleNpc)
gender = Gender.Male;
return (gender, race) switch
{
(Gender.Male, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderM),
(Gender.Male, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderM),
(Gender.Male, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodM),
(Gender.Male, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightM),
(Gender.Male, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkM),
(Gender.Male, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkM),
(Gender.Male, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunM),
(Gender.Male, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonM),
(Gender.Male, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfM),
(Gender.Male, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardM),
(Gender.Male, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenM),
(Gender.Male, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaM),
(Gender.Male, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM),
(Gender.Male, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM),
(Gender.Male, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaM),
(Gender.Male, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaM),
(Gender.Female, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderF),
(Gender.Female, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderF),
(Gender.Female, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodF),
(Gender.Female, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightF),
(Gender.Female, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkF),
(Gender.Female, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkF),
(Gender.Female, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunF),
(Gender.Female, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonF),
(Gender.Female, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfF),
(Gender.Female, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardF),
(Gender.Female, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenF),
(Gender.Female, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaF),
(Gender.Female, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM),
(Gender.Female, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM),
(Gender.Female, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaF),
(Gender.Female, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaF),
_ => "Unknown",
};
}
/// <summary>
/// Check that the given race and clan are valid.
/// The returned race and clan fit together and are valid.
/// The return value is an empty string if everything was correct and a warning otherwise.
/// </summary>
public string ValidateClan(SubRace clan, Race race, out Race actualRace, out SubRace actualClan)
{
if (AwaitedService.Clans.Contains(clan))
{
actualClan = clan;
actualRace = actualClan.ToRace();
if (race != actualRace)
return $"The race {race.ToName()} does not correspond to the clan {clan.ToName()}, changed to {actualRace.ToName()}.";
return string.Empty;
}
if (AwaitedService.Races.Contains(race))
{
actualRace = race;
actualClan = AwaitedService.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown);
// This should not happen.
if (actualClan == SubRace.Unknown)
{
actualRace = Race.Hyur;
actualClan = SubRace.Midlander;
return
$"The clan {clan.ToName()} is invalid and the race {race.ToName()} does not correspond to any clan, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}.";
}
return $"The clan {clan.ToName()} is invalid, but the race {race.ToName()} is known, reset to {actualClan.ToName()}.";
}
actualRace = Race.Hyur;
actualClan = SubRace.Midlander;
return
$"Both the clan {clan.ToName()} and the race {race.ToName()} are invalid, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}.";
}
/// <summary>
/// Check that the given gender is valid for that race.
/// The returned gender is valid for the race.
/// The return value is an empty string if everything was correct and a warning otherwise.
/// </summary>
public string ValidateGender(Race race, Gender gender, out Gender actualGender)
{
if (!AwaitedService.Genders.Contains(gender))
{
actualGender = Gender.Male;
return $"The gender {gender.ToName()} is unknown, reset to {Gender.Male.ToName()}.";
}
// TODO: Female Hrothgar
if (gender == Gender.Female && race == Race.Hrothgar)
{
actualGender = Gender.Male;
return $"{Race.Hrothgar.ToName()} do not currently support {Gender.Female.ToName()} characters, reset to {Gender.Male.ToName()}.";
}
actualGender = gender;
return string.Empty;
}
/// <summary>
/// Check that the given model id is valid.
/// The returned model id is 0.
/// The return value is an empty string if everything was correct and a warning otherwise.
/// </summary>
public string ValidateModelId(uint modelId, out uint actualModelId)
{
actualModelId = 0;
return modelId != 0 ? $"Model IDs different from 0 are not currently allowed, reset {modelId} to 0." : string.Empty;
}
/// <summary>
/// Validate a single customization value against a given set of race and gender (and face).
/// The returned actualValue is either the correct value or the one with index 0.
/// The return value is an empty string or a warning message.
/// </summary>
public static string ValidateCustomizeValue(CustomizationSet set, CustomizeValue face, CustomizeIndex index, CustomizeValue value,
out CustomizeValue actualValue)
{
var count = set.Count(index, face);
var idx = set.DataByValue(index, value, out var data, face);
if (idx >= 0 && idx < count)
{
actualValue = value;
return string.Empty;
}
var name = set.Option(index);
var newValue = set.Data(index, 0, face);
actualValue = newValue.Value;
return
$"Customization {name} for {set.Race.ToName()} {set.Gender.ToName()}s does not support value {value.Value}, reset to {newValue.Value.Value}.";
}
/// <summary> Change a clan while keeping all other customizations valid. </summary>
public bool ChangeClan(ref Customize customize, SubRace newClan)
{
if (customize.Clan == newClan)
return false;
if (ValidateClan(newClan, newClan.ToRace(), out var newRace, out newClan).Length > 0)
return false;
customize.Race = newRace;
customize.Clan = newClan;
// TODO Female Hrothgar
if (newRace == Race.Hrothgar)
customize.Gender = Gender.Male;
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
FixValues(set, ref customize);
return true;
}
/// <summary> Change a gender while keeping all other customizations valid. </summary>
public bool ChangeGender(ref Customize customize, Gender newGender)
{
if (customize.Gender == newGender)
return false;
// TODO Female Hrothgar
if (customize.Race is Race.Hrothgar)
return false;
if (ValidateGender(customize.Race, newGender, out newGender).Length > 0)
return false;
customize.Gender = newGender;
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
FixValues(set, ref customize);
return true;
}
private static void FixValues(CustomizationSet set, ref Customize customize)
{
foreach (var idx in Enum.GetValues<CustomizeIndex>().Where(set.IsAvailable))
{
if (ValidateCustomizeValue(set, customize.Face, idx, customize[idx], out var fixedValue).Length > 0)
customize[idx] = fixedValue;
}
}
}

View file

@ -22,6 +22,7 @@ public class FilenameService
DesignDirectory = Path.Combine(ConfigDirectory, "designs");
}
public IEnumerable<FileInfo> Designs()
{
if (!Directory.Exists(DesignDirectory))

View file

@ -75,7 +75,7 @@ public class ItemManager : IDisposable
if (itemId == SmallclothesId(slot))
return SmallClothesItem(slot);
if (!ItemService.AwaitedService.TryGetValue(itemId, slot is EquipSlot.MainHand, out var item))
if (!ItemService.AwaitedService.TryGetValue(itemId, slot is not EquipSlot.OffHand, out var item))
return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0);
if (item.Type.ToSlot() != slot)
@ -134,4 +134,88 @@ public class ItemManager : IDisposable
? item
: new EquipItem($"Unknown ({id.Value}-{type.Value}-{variant})", 0, 0, id, type, variant, 0);
}
/// <summary>
/// Check whether an item id resolves to an existing item of the correct slot (which should not be weapons.)
/// The returned item is either the resolved correct item, or the Nothing item for that slot.
/// The return value is an empty string if there was no problem and a warning otherwise.
/// </summary>
public string ValidateItem(EquipSlot slot, uint itemId, out EquipItem item)
{
if (slot is EquipSlot.MainHand or EquipSlot.OffHand)
throw new Exception("Internal Error: Used armor functionality for weapons.");
item = Resolve(slot, itemId);
if (item.Valid)
return string.Empty;
item = NothingItem(slot);
return $"The {slot.ToName()} item {itemId} does not exist, reset to Nothing.";
}
/// <summary>
/// Check whether a stain id is an existing stain.
/// The returned stain id is either the input or 0.
/// The return value is an empty string if there was no problem and a warning otherwise.
/// </summary>
public string ValidateStain(StainId stain, out StainId ret)
{
if (stain.Value == 0 || Stains.ContainsKey(stain))
{
ret = stain;
return string.Empty;
}
ret = 0;
return $"The Stain {stain} does not exist, reset to unstained.";
}
/// <summary>
/// Check whether a combination of an item id for a mainhand and for an offhand is valid.
/// The returned items are either the resolved correct items,
/// the correct mainhand and an appropriate offhand (implicit offhand or nothing),
/// or the default sword and a nothing offhand.
/// The return value is an empty string if there was no problem and a warning otherwise.
/// </summary>
public string ValidateWeapons(uint mainId, uint offId, out EquipItem main, out EquipItem off)
{
var ret = string.Empty;
main = Resolve(EquipSlot.MainHand, mainId);
if (!main.Valid)
{
main = DefaultSword;
ret = $"The mainhand weapon {mainId} does not exist, reset to default sword.";
}
var offhandType = main.Type.Offhand();
off = Resolve(offhandType, offId);
if (off.Valid)
return ret;
// Try implicit offhand.
off = Resolve(offhandType, mainId);
if (off.Valid)
{
// Can not be set to default sword before because then it could not be valid.
ret = $"The offhand weapon {offId} does not exist, reset to implied offhand.";
}
else
{
if (FullEquipTypeExtensions.OffhandTypes.Contains(offhandType))
{
main = DefaultSword;
off = NothingItem(FullEquipType.Shield);
ret =
$"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing.";
}
else
{
off = NothingItem(offhandType);
if (ret.Length == 0)
ret = $"The offhand weapon {offId} does not exist, reset to no offhand.";
}
}
return ret;
}
}

View file

@ -1,4 +1,5 @@
using Dalamud.Plugin;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui;
using Glamourer.Gui.Tabs;
@ -21,6 +22,7 @@ public static class ServiceManager
.AddInterop()
.AddEvents()
.AddData()
.AddDesigns()
.AddUi()
.AddApi();
@ -44,7 +46,8 @@ public static class ServiceManager
private static IServiceCollection AddEvents(this IServiceCollection services)
=> services.AddSingleton<VisorStateChanged>()
.AddSingleton<UpdatedSlot>();
.AddSingleton<UpdatedSlot>()
.AddSingleton<DesignChanged>();
private static IServiceCollection AddData(this IServiceCollection services)
=> services.AddSingleton<IdentifierService>()
@ -61,6 +64,10 @@ public static class ServiceManager
.AddSingleton<PenumbraService>()
.AddSingleton<ObjectManager>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()
.AddSingleton<DesignFileSystem>();
private static IServiceCollection AddUi(this IServiceCollection services)
=> services.AddSingleton<DebugTab>()
.AddSingleton<MainWindow>()

View file

@ -7,11 +7,9 @@ using Penumbra.GameData.Actors;
using System;
using System.Threading.Tasks;
using Dalamud.Game;
using Glamourer.Customization;
using Glamourer.Interop.Penumbra;
using Penumbra.GameData.Data;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
namespace Glamourer.Services;
@ -97,55 +95,3 @@ public sealed class ActorService : AsyncServiceWrapper<ActorManager>
() => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx)))
{ }
}
public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationManager>
{
public CustomizationService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData))
{ }
/// <summary> In languages other than english the actual clan name may depend on gender. </summary>
public string ClanName(SubRace race, Gender gender)
{
if (gender == Gender.FemaleNpc)
gender = Gender.Female;
if (gender == Gender.MaleNpc)
gender = Gender.Male;
return (gender, race) switch
{
(Gender.Male, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderM),
(Gender.Male, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderM),
(Gender.Male, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodM),
(Gender.Male, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightM),
(Gender.Male, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkM),
(Gender.Male, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkM),
(Gender.Male, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunM),
(Gender.Male, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonM),
(Gender.Male, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfM),
(Gender.Male, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardM),
(Gender.Male, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenM),
(Gender.Male, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaM),
(Gender.Male, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM),
(Gender.Male, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM),
(Gender.Male, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaM),
(Gender.Male, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaM),
(Gender.Female, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderF),
(Gender.Female, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderF),
(Gender.Female, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodF),
(Gender.Female, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightF),
(Gender.Female, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkF),
(Gender.Female, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkF),
(Gender.Female, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunF),
(Gender.Female, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonF),
(Gender.Female, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfF),
(Gender.Female, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardF),
(Gender.Female, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenF),
(Gender.Female, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaF),
(Gender.Female, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM),
(Gender.Female, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM),
(Gender.Female, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaF),
(Gender.Female, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaF),
_ => "Unknown",
};
}
}

View file

@ -19,9 +19,10 @@ public class DesignManager
public const string DesignFolderName = "designs";
public readonly string DesignFolder;
private readonly ItemManager _items;
private readonly SaveService _saveService;
private readonly List<Design> _designs = new();
private readonly CustomizationManager _customizations;
private readonly ItemManager _items;
private readonly SaveService _saveService;
private readonly List<Design> _designs = new();
public enum DesignChangeType
{