mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
..
This commit is contained in:
parent
27f151c55a
commit
d10cb3137f
14 changed files with 1366 additions and 373 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
200
Glamourer/Designs/DesignFileSystem.cs
Normal file
200
Glamourer/Designs/DesignFileSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
470
Glamourer/Designs/DesignManager.cs
Normal file
470
Glamourer/Designs/DesignManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
76
Glamourer/Events/DesignChanged.cs
Normal file
76
Glamourer/Events/DesignChanged.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
211
Glamourer/Services/CustomizationService.cs
Normal file
211
Glamourer/Services/CustomizationService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ public class FilenameService
|
|||
DesignDirectory = Path.Combine(ConfigDirectory, "designs");
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<FileInfo> Designs()
|
||||
{
|
||||
if (!Directory.Exists(DesignDirectory))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue