This commit is contained in:
Ottermandias 2023-01-31 17:45:42 +01:00
parent 33c2ed7903
commit e27a194cc6
47 changed files with 2037 additions and 1167 deletions

View file

@ -0,0 +1,88 @@
using System.Runtime.InteropServices;
using Glamourer.Customization;
using Glamourer.Interop;
using Penumbra.GameData.Structs;
using Penumbra.String.Functions;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Designs;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct CharacterData
{
public uint ModelId;
public CustomizeData CustomizeData;
public CharacterWeapon MainHand;
public CharacterWeapon OffHand;
public CharacterArmor Head;
public CharacterArmor Body;
public CharacterArmor Hands;
public CharacterArmor Legs;
public CharacterArmor Feet;
public CharacterArmor Ears;
public CharacterArmor Neck;
public CharacterArmor Wrists;
public CharacterArmor RFinger;
public CharacterArmor LFinger;
public unsafe Customize Customize
{
get
{
fixed (CustomizeData* ptr = &CustomizeData)
{
return new Customize(ptr);
}
}
}
public unsafe CharacterEquip Equipment
{
get
{
fixed (CharacterArmor* ptr = &Head)
{
return new CharacterEquip(ptr);
}
}
}
public static readonly CharacterData Default
= new()
{
ModelId = 0,
CustomizeData = Customize.Default,
MainHand = CharacterWeapon.Empty,
OffHand = CharacterWeapon.Empty,
Head = CharacterArmor.Empty,
Body = CharacterArmor.Empty,
Hands = CharacterArmor.Empty,
Legs = CharacterArmor.Empty,
Feet = CharacterArmor.Empty,
Ears = CharacterArmor.Empty,
Neck = CharacterArmor.Empty,
Wrists = CharacterArmor.Empty,
RFinger = CharacterArmor.Empty,
LFinger = CharacterArmor.Empty,
};
public readonly unsafe CharacterData Clone()
{
var data = new CharacterData();
fixed (void* ptr = &this)
{
MemoryUtility.MemCpyUnchecked(&data, ptr, sizeof(CharacterData));
}
return data;
}
public void Load(IDesignable designable)
{
ModelId = designable.ModelId;
Customize.Load(designable.Customize);
Equipment.Load(designable.Equip);
MainHand = designable.MainHand;
OffHand = designable.OffHand;
}
}

View file

@ -2,106 +2,18 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Plugin;
using ImGuizmoNET;
using Dalamud.Utility;
using Glamourer.Customization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Filesystem;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public sealed class DesignFileSystem : FileSystem<Design>, IDisposable
{
public readonly string DesignFileSystemFile;
private readonly Design.Manager _designManager;
public DesignFileSystem(Design.Manager designManager, DalamudPluginInterface pi)
{
DesignFileSystemFile = Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json");
_designManager = designManager;
}
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 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));
}
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
{
if (type != FileSystemChangeType.Reload)
{
SaveFilesystem();
}
}
private void SaveFilesystem()
{
SaveToFile(new FileInfo(DesignFileSystemFile), SaveDesign, true);
Glamourer.Log.Verbose($"Saved design filesystem.");
}
private void Save()
=> Glamourer.Framework.RegisterDelayed(nameof(SaveFilesystem), SaveFilesystem);
private void OnDataChange(Design.Manager.DesignChangeType type, Design design, string? oldName, string? _2, int _3)
{
switch (type)
{
}
if (type == Design.Manager.DesignChangeType.Renamed && oldName != null)
{
var old = oldName.FixName();
if (Find(old, out var child) && child is not Folder)
{
Rename(child, design.Name);
}
}
}
// Used for saving and loading.
private static string DesignToIdentifier(Design design)
=> design.Identifier.ToString();
private static string DesignToName(Design design)
=> design.Name.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)
: (DesignToName(design), true);
}
public partial class Design
{
public partial class Manager
@ -109,7 +21,8 @@ public partial class Design
public const string DesignFolderName = "designs";
public readonly string DesignFolder;
private readonly List<Design> _designs = new();
private readonly FrameworkManager _framework;
private readonly List<Design> _designs = new();
public enum DesignChangeType
{
@ -121,18 +34,54 @@ public partial class Design
AddedTag,
RemovedTag,
ChangedTag,
Customize,
Equip,
Weapon,
Stain,
ApplyCustomize,
ApplyEquip,
Other,
}
public delegate void DesignChangeDelegate(DesignChangeType type, int designIdx, string? oldData = null, string? newData = null,
int tagIdx = -1);
public delegate void DesignChangeDelegate(DesignChangeType type, Design design, object? changeData = null);
public event DesignChangeDelegate? DesignChange;
public event DesignChangeDelegate DesignChange;
public IReadOnlyList<Design> Designs
=> _designs;
public Manager(DalamudPluginInterface pi)
=> DesignFolder = SetDesignFolder(pi);
public Manager(DalamudPluginInterface pi, FrameworkManager framework)
{
_framework = framework;
DesignFolder = SetDesignFolder(pi);
DesignChange += OnChange;
LoadDesigns();
MigrateOldDesigns(pi, Path.Combine(new DirectoryInfo(DesignFolder).Parent!.FullName, "Designs.json"));
}
private void OnChange(DesignChangeType type, Design design, object? _)
{
switch (type)
{
case DesignChangeType.Created:
SaveDesignInternal(design);
return;
case DesignChangeType.Renamed:
case DesignChangeType.ChangedDescription:
case DesignChangeType.AddedTag:
case DesignChangeType.RemovedTag:
case DesignChangeType.ChangedTag:
case DesignChangeType.Customize:
case DesignChangeType.Equip:
case DesignChangeType.Weapon:
case DesignChangeType.Stain:
case DesignChangeType.ApplyCustomize:
case DesignChangeType.ApplyEquip:
case DesignChangeType.Other:
SaveDesign(design);
return;
}
}
private static string SetDesignFolder(DalamudPluginInterface pi)
{
@ -153,9 +102,12 @@ public partial class Design
}
private string CreateFileName(Design design)
=> Path.Combine(DesignFolder, $"{design.Name.RemoveInvalidPathSymbols()}_{design.Identifier}.json");
=> Path.Combine(DesignFolder, $"{design.Identifier}.json");
public void SaveDesign(Design design)
=> _framework.RegisterDelayed($"{nameof(SaveDesign)}_{design.Identifier}", () => SaveDesignInternal(design));
private void SaveDesignInternal(Design design)
{
var fileName = CreateFileName(design);
try
@ -173,24 +125,54 @@ public partial class Design
public void LoadDesigns()
{
_designs.Clear();
List<(Design, string)> invalidNames = new();
var skipped = 0;
foreach (var file in new DirectoryInfo(DesignFolder).EnumerateFiles("*.json", SearchOption.TopDirectoryOnly))
{
try
{
var text = File.ReadAllText(file.FullName);
var data = JObject.Parse(text);
var design = LoadDesign(data);
var design = LoadDesign(data, out var changes);
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.");
// TODO something when changed?
design.Index = _designs.Count;
_designs.Add(design);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not load design, skipped:\n{ex}");
++skipped;
}
}
Glamourer.Log.Information($"Loaded {_designs.Count} designs.");
DesignChange?.Invoke(DesignChangeType.ReloadedAll, -1);
var failed = 0;
foreach (var (design, name) in invalidNames)
{
try
{
var correctName = CreateFileName(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}");
}
}
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)}");
DesignChange.Invoke(DesignChangeType.ReloadedAll, null!);
}
public Design Create(string name)
@ -198,13 +180,13 @@ public partial class Design
var design = new Design()
{
CreationDate = DateTimeOffset.UtcNow,
Identifier = Guid.NewGuid(),
Identifier = CreateNewGuid(),
Index = _designs.Count,
Name = name,
};
_designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
DesignChange?.Invoke(DesignChangeType.Created, design.Index);
DesignChange.Invoke(DesignChangeType.Created, design);
return design;
}
@ -218,7 +200,7 @@ public partial class Design
{
File.Delete(fileName);
Glamourer.Log.Debug($"Deleted design {design.Identifier}.");
DesignChange?.Invoke(DesignChangeType.Deleted, design.Index);
DesignChange.Invoke(DesignChangeType.Deleted, design);
}
catch (Exception ex)
{
@ -228,7 +210,7 @@ public partial class Design
public void Rename(Design design, string newName)
{
var oldName = design.Name;
var oldName = design.Name.Text;
var oldFileName = CreateFileName(design);
if (File.Exists(oldFileName))
try
@ -242,18 +224,15 @@ public partial class Design
}
design.Name = newName;
SaveDesign(design);
Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
DesignChange?.Invoke(DesignChangeType.Renamed, design.Index, oldName, newName);
DesignChange.Invoke(DesignChangeType.Renamed, design, oldName);
}
public void ChangeDescription(Design design, string description)
{
var oldDescription = design.Description;
design.Description = description;
SaveDesign(design);
Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
DesignChange?.Invoke(DesignChangeType.ChangedDescription, design.Index, oldDescription, description);
Glamourer.Log.Debug($"Changed description of design {design.Identifier}.");
DesignChange.Invoke(DesignChangeType.ChangedDescription, design);
}
public void AddTag(Design design, string tag)
@ -263,9 +242,8 @@ public partial class Design
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
var idx = design.Tags.IndexOf(tag);
SaveDesign(design);
Glamourer.Log.Debug($"Added tag at {idx} to design {design.Identifier}.");
DesignChange?.Invoke(DesignChangeType.AddedTag, design.Index, null, tag, idx);
Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}.");
DesignChange.Invoke(DesignChangeType.AddedTag, design);
}
public void RemoveTag(Design design, string tag)
@ -279,9 +257,8 @@ public partial class Design
{
var oldTag = design.Tags[tagIdx];
design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray();
SaveDesign(design);
Glamourer.Log.Debug($"Removed tag at {tagIdx} from design {design.Identifier}.");
DesignChange?.Invoke(DesignChangeType.RemovedTag, design.Index, oldTag, null, tagIdx);
Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}.");
DesignChange.Invoke(DesignChangeType.RemovedTag, design);
}
@ -294,8 +271,156 @@ public partial class Design
design.Tags[tagIdx] = newTag;
Array.Sort(design.Tags);
SaveDesign(design);
Glamourer.Log.Debug($"Renamed tag at {tagIdx} in design {design.Identifier} and resorted tags.");
DesignChange?.Invoke(DesignChangeType.ChangedTag, design.Index, oldTag, newTag, tagIdx);
Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
DesignChange.Invoke(DesignChangeType.ChangedTag, design);
}
public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value)
{
var old = design.GetCustomize(idx);
if (design.SetCustomize(idx, value))
{
Glamourer.Log.Debug($"Changed customize {idx} in design {design.Identifier} from {old.Value} to {value.Value}");
DesignChange.Invoke(DesignChangeType.Customize, design, idx);
}
}
public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
{
if (design.SetApplyCustomize(idx, value))
{
Glamourer.Log.Debug($"Set applying of customization {idx} to {value}.");
DesignChange.Invoke(DesignChangeType.ApplyCustomize, design, idx);
}
}
public void ChangeEquip(Design design, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var old = design.Armor(slot);
if (design.SetArmor(slot, itemId, item))
{
var n = design.Armor(slot);
Glamourer.Log.Debug(
$"Set {slot} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId}).");
DesignChange.Invoke(DesignChangeType.Equip, design, slot);
}
}
public void ChangeWeapon(Design design, uint itemId, EquipSlot offhand, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var (old, change, n) = offhand == EquipSlot.OffHand
? (design.WeaponOff, design.SetOffhand(itemId, item), design.WeaponOff)
: (design.WeaponMain, design.SetMainhand(itemId, item), design.WeaponMain);
if (change)
{
Glamourer.Log.Debug(
$"Set {offhand} weapon in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId}).");
DesignChange.Invoke(DesignChangeType.Weapon, design, offhand);
}
}
public void ChangeApplyEquip(Design design, EquipSlot slot, bool value)
{
if (design.SetApplyEquip(slot, value))
{
Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}.");
DesignChange.Invoke(DesignChangeType.ApplyEquip, design, slot);
}
}
public void ChangeStain(Design design, EquipSlot slot, StainId stain)
{
if (design.SetStain(slot, stain))
{
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}.");
DesignChange.Invoke(DesignChangeType.Stain, design, slot);
}
}
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
{
if (design.SetApplyStain(slot, value))
{
Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}.");
DesignChange.Invoke(DesignChangeType.Stain, design, slot);
}
}
private Guid CreateNewGuid()
{
while (true)
{
var guid = Guid.NewGuid();
if (_designs.All(d => d.Identifier != guid))
return guid;
}
}
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);
DesignChange.Invoke(DesignChangeType.Created, design);
return true;
}
private void MigrateOldDesigns(DalamudPluginInterface pi, string filePath)
{
if (!File.Exists(filePath))
return;
var errors = 0;
var successes = 0;
try
{
var text = File.ReadAllText(filePath);
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()
{
CreationDate = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
};
design.MigrateBase64(base64);
Add(design, $"Migrated old design to {design.Identifier}.");
migratedFileSystemPaths.Add(design.Identifier.ToString(), name);
++successes;
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}");
++errors;
}
}
DesignFileSystem.MigrateOldPaths(pi, migratedFileSystemPaths);
Glamourer.Log.Information($"Successfully migrated {successes} old designs. Failed to migrate {errors} designs.");
}
catch (Exception e)
{
Glamourer.Log.Error($"Could not migrate old design file {filePath}:\n{e}");
}
try
{
File.Move(filePath, Path.ChangeExtension(filePath, ".json.bak"));
Glamourer.Log.Information($"Moved migrated design file {filePath} to backup file.");
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not move migrated design file {filePath} to backup file:\n{ex}");
}
}
}
}

View file

@ -1,44 +1,339 @@
using System;
using System.Linq;
using Glamourer.Customization;
using Glamourer.Util;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public partial class Design
public partial class Design : DesignBase
{
public const int FileVersion = 1;
public Guid Identifier { get; private init; }
public DateTimeOffset CreationDate { get; private init; }
public string Name { get; private set; } = string.Empty;
public LowerString Name { get; private set; } = LowerString.Empty;
public string Description { get; private set; } = string.Empty;
public string[] Tags { get; private set; } = Array.Empty<string>();
public int Index { get; private set; }
private EquipFlag _applyEquip;
private CustomizeFlag _applyCustomize;
public QuadBool Wetness { get; private set; } = QuadBool.NullFalse;
public QuadBool Visor { get; private set; } = QuadBool.NullFalse;
public QuadBool Hat { get; private set; } = QuadBool.NullFalse;
public QuadBool Weapon { get; private set; } = QuadBool.NullFalse;
public bool WriteProtected { get; private set; }
public bool DoApplyEquip(EquipSlot slot)
=> _applyEquip.HasFlag(slot.ToFlag());
public bool DoApplyStain(EquipSlot slot)
=> _applyEquip.HasFlag(slot.ToStainFlag());
public bool DoApplyCustomize(CustomizeIndex idx)
=> _applyCustomize.HasFlag(idx.ToFlag());
private bool SetApplyEquip(EquipSlot slot, bool value)
{
var newValue = value ? _applyEquip | slot.ToFlag() : _applyEquip & ~slot.ToFlag();
if (newValue == _applyEquip)
return false;
_applyEquip = newValue;
return true;
}
private bool SetApplyStain(EquipSlot slot, bool value)
{
var newValue = value ? _applyEquip | slot.ToStainFlag() : _applyEquip & ~slot.ToStainFlag();
if (newValue == _applyEquip)
return false;
_applyEquip = newValue;
return true;
}
private bool SetApplyCustomize(CustomizeIndex idx, bool value)
{
var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag();
if (newValue == _applyCustomize)
return false;
_applyCustomize = newValue;
return true;
}
private Design()
{ }
public JObject JsonSerialize()
{
var ret = new JObject
{
[nameof(Identifier)] = Identifier,
[nameof(CreationDate)] = CreationDate,
[nameof(Name)] = Name,
[nameof(Description)] = Description,
[nameof(Tags)] = JArray.FromObject(Tags),
[nameof(FileVersion)] = FileVersion,
[nameof(Identifier)] = Identifier,
[nameof(CreationDate)] = CreationDate,
[nameof(Name)] = Name.Text,
[nameof(Description)] = Description,
[nameof(Tags)] = JArray.FromObject(Tags),
[nameof(WriteProtected)] = WriteProtected,
[nameof(CharacterData.Equipment)] = SerializeEquipment(),
[nameof(CharacterData.Customize)] = SerializeCustomize(),
};
return ret;
}
public static Design LoadDesign(JObject json)
=> new()
public JObject SerializeEquipment()
{
static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain)
=> new()
{
[nameof(Item.ItemId)] = itemId,
[nameof(Item.Stain)] = stain.Value,
["Apply"] = apply,
["ApplyStain"] = applyStain,
};
var ret = new JObject()
{
CreationDate = json[nameof(CreationDate)]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException(nameof(CreationDate)),
Identifier = json[nameof(Identifier)]?.ToObject<Guid>() ?? throw new ArgumentNullException(nameof(Identifier)),
Name = json[nameof(Name)]?.ToObject<string>() ?? throw new ArgumentNullException(nameof(Name)),
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
[nameof(MainHand)] =
Serialize(MainHand, CharacterData.MainHand.Stain, DoApplyEquip(EquipSlot.MainHand), DoApplyStain(EquipSlot.MainHand)),
[nameof(OffHand)] = Serialize(OffHand, CharacterData.OffHand.Stain, DoApplyEquip(EquipSlot.OffHand), DoApplyStain(EquipSlot.OffHand)),
};
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var armor = Armor(slot);
ret[slot.ToString()] = Serialize(armor.ItemId, armor.Stain, DoApplyEquip(slot), DoApplyStain(slot));
}
ret[nameof(Hat)] = Hat.ToJObject("Show", "Apply");
ret[nameof(Weapon)] = Weapon.ToJObject("Show", "Apply");
ret[nameof(Visor)] = Visor.ToJObject("IsToggled", "Apply");
return ret;
}
public JObject SerializeCustomize()
{
var ret = new JObject()
{
[nameof(ModelId)] = ModelId,
};
var customize = CharacterData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
var data = customize[idx];
ret[idx.ToString()] = new JObject()
{
["Value"] = data.Value,
["Apply"] = true,
};
}
ret[nameof(Wetness)] = Wetness.ToJObject("IsWet", "Apply");
return ret;
}
public static Design LoadDesign(JObject json, out bool changes)
{
var version = json[nameof(FileVersion)]?.ToObject<int>() ?? 0;
return version switch
{
1 => LoadDesignV1(json, out changes),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
}
private static Design LoadDesignV1(JObject json, out bool changes)
{
static string[] ParseTags(JObject json)
{
var tags = json["Tags"]?.ToObject<string[]>() ?? Array.Empty<string>();
return tags.OrderBy(t => t).Distinct().ToArray();
}
var design = new Design()
{
CreationDate = json["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate"),
Identifier = json["Identifier"]?.ToObject<Guid>() ?? throw new ArgumentNullException("Identifier"),
Name = new LowerString(json["Name"]?.ToObject<string>() ?? throw new ArgumentNullException("Name")),
Description = json["Description"]?.ToObject<string>() ?? string.Empty,
Tags = ParseTags(json),
};
private static string[] ParseTags(JObject json)
changes = LoadEquip(json["Equipment"], design);
changes |= LoadCustomize(json["Customize"], design);
return design;
}
private static bool LoadEquip(JToken? equip, Design design)
{
var tags = json[nameof(Tags)]?.ToObject<string[]>() ?? Array.Empty<string>();
return tags.OrderBy(t => t).Distinct().ToArray();
if (equip == null)
return true;
static (uint, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item)
{
var id = item?["ItemId"]?.ToObject<uint>() ?? ItemManager.NothingId(slot);
var stain = (StainId)(item?["Stain"]?.ToObject<byte>() ?? 0);
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
return (id, stain, apply, applyStain);
}
var changes = false;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
changes |= !design.SetArmor(slot, id);
changes |= !design.SetStain(slot, stain);
design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain);
}
var main = equip["MainHand"];
if (main == null)
{
changes = true;
}
else
{
var id = main["ItemId"]?.ToObject<uint>() ?? Glamourer.Items.DefaultSword.RowId;
var stain = (StainId)(main["Stain"]?.ToObject<byte>() ?? 0);
var apply = main["Apply"]?.ToObject<bool>() ?? false;
var applyStain = main["ApplyStain"]?.ToObject<bool>() ?? false;
changes |= !design.SetMainhand(id);
changes |= !design.SetStain(EquipSlot.MainHand, stain);
design.SetApplyEquip(EquipSlot.MainHand, apply);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
}
var off = equip["OffHand"];
if (off == null)
{
changes = true;
}
else
{
var id = off["ItemId"]?.ToObject<uint>() ?? ItemManager.NothingId(design.MainhandType.Offhand());
var stain = (StainId)(off["Stain"]?.ToObject<byte>() ?? 0);
var apply = off["Apply"]?.ToObject<bool>() ?? false;
var applyStain = off["ApplyStain"]?.ToObject<bool>() ?? false;
changes |= !design.SetOffhand(id);
changes |= !design.SetStain(EquipSlot.OffHand, stain);
design.SetApplyEquip(EquipSlot.OffHand, apply);
design.SetApplyStain(EquipSlot.OffHand, applyStain);
}
design.Hat = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.Weapon = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
design.Visor = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
return changes;
}
private static bool LoadCustomize(JToken? json, Design design)
{
if (json == null)
return true;
var customize = design.CharacterData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
customize[idx] = data;
design.SetApplyCustomize(idx, apply);
}
design.Wetness = QuadBool.FromJObject(json["Wetness"], "IsWet", "Apply", QuadBool.NullFalse);
return false;
}
public void MigrateBase64(string base64)
{
static void CheckSize(int length, int requiredLength)
{
if (length != requiredLength)
throw new Exception(
$"Can not parse Base64 string into CharacterSave:\n\tInvalid size {length} instead of {requiredLength}.");
}
var bytes = Convert.FromBase64String(base64);
byte applicationFlags;
ushort equipFlags;
switch (bytes[0])
{
case 1:
{
CheckSize(bytes.Length, 86);
applicationFlags = bytes[1];
equipFlags = BitConverter.ToUInt16(bytes, 2);
break;
}
case 2:
{
CheckSize(bytes.Length, 91);
applicationFlags = bytes[1];
equipFlags = BitConverter.ToUInt16(bytes, 2);
Hat = Hat.SetValue((bytes[90] & 0x01) == 0);
Visor = Visor.SetValue((bytes[90] & 0x10) != 0);
Weapon = Weapon.SetValue((bytes[90] & 0x02) == 0);
break;
}
default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}.");
}
_applyCustomize = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0;
Wetness = (applicationFlags & 0x02) != 0 ? QuadBool.True : QuadBool.NullFalse;
Hat = Hat.SetEnabled((applicationFlags & 0x04) != 0);
Weapon = Weapon.SetEnabled((applicationFlags & 0x08) != 0);
Visor = Visor.SetEnabled((applicationFlags & 0x10) != 0);
WriteProtected = (applicationFlags & 0x20) != 0;
CharacterData.ModelId = 0;
SetApplyEquip(EquipSlot.MainHand, (equipFlags & 0x0001) != 0);
SetApplyEquip(EquipSlot.OffHand, (equipFlags & 0x0002) != 0);
SetApplyStain(EquipSlot.MainHand, (equipFlags & 0x0001) != 0);
SetApplyStain(EquipSlot.OffHand, (equipFlags & 0x0002) != 0);
var flag = 0x0002u;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
flag <<= 1;
var apply = (equipFlags & flag) != 0;
SetApplyEquip(slot, apply);
SetApplyStain(slot, apply);
}
unsafe
{
fixed (byte* ptr = bytes)
{
CharacterData.CustomizeData.Read(ptr + 4);
var cur = (CharacterWeapon*)(ptr + 30);
UpdateMainhand(cur[0]);
SetStain(EquipSlot.MainHand, cur[0].Stain);
UpdateOffhand(cur[1]);
SetStain(EquipSlot.OffHand, cur[1].Stain);
var eq = (CharacterArmor*)(cur + 2);
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{
UpdateArmor(slot, eq[idx], true);
SetStain(slot, eq[idx].Stain);
}
}
}
}
}

View file

@ -0,0 +1,330 @@
using System;
using Glamourer.Customization;
using Glamourer.Util;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignBase
{
protected CharacterData CharacterData = CharacterData.Default;
public FullEquipType MainhandType { get; protected set; }
public uint Head { get; protected set; } = ItemManager.NothingId(EquipSlot.Head);
public uint Body { get; protected set; } = ItemManager.NothingId(EquipSlot.Body);
public uint Hands { get; protected set; } = ItemManager.NothingId(EquipSlot.Hands);
public uint Legs { get; protected set; } = ItemManager.NothingId(EquipSlot.Legs);
public uint Feet { get; protected set; } = ItemManager.NothingId(EquipSlot.Feet);
public uint Ears { get; protected set; } = ItemManager.NothingId(EquipSlot.Ears);
public uint Neck { get; protected set; } = ItemManager.NothingId(EquipSlot.Neck);
public uint Wrists { get; protected set; } = ItemManager.NothingId(EquipSlot.Wrists);
public uint RFinger { get; protected set; } = ItemManager.NothingId(EquipSlot.RFinger);
public uint LFinger { get; protected set; } = ItemManager.NothingId(EquipSlot.RFinger);
public uint MainHand { get; protected set; }
public uint OffHand { get; protected set; }
public string HeadName { get; protected set; } = ItemManager.Nothing;
public string BodyName { get; protected set; } = ItemManager.Nothing;
public string HandsName { get; protected set; } = ItemManager.Nothing;
public string LegsName { get; protected set; } = ItemManager.Nothing;
public string FeetName { get; protected set; } = ItemManager.Nothing;
public string EarsName { get; protected set; } = ItemManager.Nothing;
public string NeckName { get; protected set; } = ItemManager.Nothing;
public string WristsName { get; protected set; } = ItemManager.Nothing;
public string RFingerName { get; protected set; } = ItemManager.Nothing;
public string LFingerName { get; protected set; } = ItemManager.Nothing;
public string MainhandName { get; protected set; }
public string OffhandName { get; protected set; }
public Customize Customize()
=> CharacterData.Customize;
public CharacterEquip Equipment()
=> CharacterData.Equipment;
public DesignBase()
{
MainHand = Glamourer.Items.DefaultSword.RowId;
(_, CharacterData.MainHand.Set, CharacterData.MainHand.Type, CharacterData.MainHand.Variant, MainhandName, MainhandType) =
Glamourer.Items.Resolve(MainHand, Glamourer.Items.DefaultSword);
OffHand = ItemManager.NothingId(MainhandType.Offhand());
(_, CharacterData.OffHand.Set, CharacterData.OffHand.Type, CharacterData.OffHand.Variant, OffhandName, _) =
Glamourer.Items.Resolve(OffHand, MainhandType);
}
public uint ModelId
=> CharacterData.ModelId;
public Item Armor(EquipSlot slot)
{
return slot switch
{
EquipSlot.Head => new Item(HeadName, Head, CharacterData.Head),
EquipSlot.Body => new Item(BodyName, Body, CharacterData.Body),
EquipSlot.Hands => new Item(HandsName, Hands, CharacterData.Hands),
EquipSlot.Legs => new Item(LegsName, Legs, CharacterData.Legs),
EquipSlot.Feet => new Item(FeetName, Feet, CharacterData.Feet),
EquipSlot.Ears => new Item(EarsName, Ears, CharacterData.Ears),
EquipSlot.Neck => new Item(NeckName, Neck, CharacterData.Neck),
EquipSlot.Wrists => new Item(WristsName, Wrists, CharacterData.Wrists),
EquipSlot.RFinger => new Item(RFingerName, RFinger, CharacterData.RFinger),
EquipSlot.LFinger => new Item(LFingerName, LFinger, CharacterData.LFinger),
_ => throw new Exception("Invalid equip slot for item."),
};
}
public Weapon WeaponMain
=> new(MainhandName, MainHand, CharacterData.MainHand, MainhandType);
public Weapon WeaponOff
=> Designs.Weapon.Offhand(OffhandName, OffHand, CharacterData.OffHand, MainhandType);
public CustomizeValue GetCustomize(CustomizeIndex idx)
=> Customize()[idx];
protected bool SetCustomize(CustomizeIndex idx, CustomizeValue value)
{
var c = Customize();
if (c[idx] == value)
return false;
c[idx] = value;
return true;
}
protected bool SetArmor(EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var (valid, set, variant, name) = Glamourer.Items.Resolve(slot, itemId, item);
if (!valid)
return false;
return SetArmor(slot, set, variant, name, itemId);
}
protected bool UpdateArmor(EquipSlot slot, CharacterArmor armor, bool force)
{
if (!force)
{
switch (slot)
{
case EquipSlot.Head when CharacterData.Head.Value == armor.Value: return false;
case EquipSlot.Body when CharacterData.Body.Value == armor.Value: return false;
case EquipSlot.Hands when CharacterData.Hands.Value == armor.Value: return false;
case EquipSlot.Legs when CharacterData.Legs.Value == armor.Value: return false;
case EquipSlot.Feet when CharacterData.Feet.Value == armor.Value: return false;
case EquipSlot.Ears when CharacterData.Ears.Value == armor.Value: return false;
case EquipSlot.Neck when CharacterData.Neck.Value == armor.Value: return false;
case EquipSlot.Wrists when CharacterData.Wrists.Value == armor.Value: return false;
case EquipSlot.RFinger when CharacterData.RFinger.Value == armor.Value: return false;
case EquipSlot.LFinger when CharacterData.LFinger.Value == armor.Value: return false;
}
}
var (valid, id, name) = Glamourer.Items.Identify(slot, armor.Set, armor.Variant);
if (!valid)
return false;
return SetArmor(slot, armor.Set, armor.Variant, name, id);
}
protected bool SetMainhand(uint mainId, Lumina.Excel.GeneratedSheets.Item? main = null)
{
if (mainId == MainHand)
return false;
var (valid, set, weapon, variant, name, type) = Glamourer.Items.Resolve(mainId, main);
if (!valid)
return false;
var fixOffhand = type.Offhand() != MainhandType.Offhand();
MainHand = mainId;
MainhandName = name;
MainhandType = type;
CharacterData.MainHand.Set = set;
CharacterData.MainHand.Type = weapon;
CharacterData.MainHand.Variant = variant;
if (fixOffhand)
SetOffhand(ItemManager.NothingId(type.Offhand()));
return true;
}
protected bool SetOffhand(uint offId, Lumina.Excel.GeneratedSheets.Item? off = null)
{
if (offId == OffHand)
return false;
var (valid, set, weapon, variant, name, type) = Glamourer.Items.Resolve(offId, MainhandType, off);
if (!valid)
return false;
OffHand = offId;
OffhandName = name;
CharacterData.OffHand.Set = set;
CharacterData.OffHand.Type = weapon;
CharacterData.OffHand.Variant = variant;
return true;
}
protected bool UpdateMainhand(CharacterWeapon weapon)
{
if (weapon.Value == CharacterData.MainHand.Value)
return false;
var (valid, id, name, type) = Glamourer.Items.Identify(EquipSlot.MainHand, weapon.Set, weapon.Type, (byte)weapon.Variant);
if (!valid || id == MainHand)
return false;
var fixOffhand = type.Offhand() != MainhandType.Offhand();
MainHand = id;
MainhandName = name;
MainhandType = type;
CharacterData.MainHand.Set = weapon.Set;
CharacterData.MainHand.Type = weapon.Type;
CharacterData.MainHand.Variant = weapon.Variant;
CharacterData.MainHand.Stain = weapon.Stain;
if (fixOffhand)
SetOffhand(ItemManager.NothingId(type.Offhand()));
return true;
}
protected bool UpdateOffhand(CharacterWeapon weapon)
{
if (weapon.Value == CharacterData.OffHand.Value)
return false;
var (valid, id, name, _) = Glamourer.Items.Identify(EquipSlot.OffHand, weapon.Set, weapon.Type, (byte)weapon.Variant, MainhandType);
if (!valid || id == OffHand)
return false;
OffHand = id;
OffhandName = name;
CharacterData.OffHand.Set = weapon.Set;
CharacterData.OffHand.Type = weapon.Type;
CharacterData.OffHand.Variant = weapon.Variant;
CharacterData.OffHand.Stain = weapon.Stain;
return true;
}
protected bool SetStain(EquipSlot slot, StainId id)
{
return slot switch
{
EquipSlot.MainHand => SetIfDifferent(ref CharacterData.MainHand.Stain, id),
EquipSlot.OffHand => SetIfDifferent(ref CharacterData.OffHand.Stain, id),
EquipSlot.Head => SetIfDifferent(ref CharacterData.Head.Stain, id),
EquipSlot.Body => SetIfDifferent(ref CharacterData.Body.Stain, id),
EquipSlot.Hands => SetIfDifferent(ref CharacterData.Hands.Stain, id),
EquipSlot.Legs => SetIfDifferent(ref CharacterData.Legs.Stain, id),
EquipSlot.Feet => SetIfDifferent(ref CharacterData.Feet.Stain, id),
EquipSlot.Ears => SetIfDifferent(ref CharacterData.Ears.Stain, id),
EquipSlot.Neck => SetIfDifferent(ref CharacterData.Neck.Stain, id),
EquipSlot.Wrists => SetIfDifferent(ref CharacterData.Wrists.Stain, id),
EquipSlot.RFinger => SetIfDifferent(ref CharacterData.RFinger.Stain, id),
EquipSlot.LFinger => SetIfDifferent(ref CharacterData.LFinger.Stain, id),
_ => false,
};
}
protected static bool SetIfDifferent<T>(ref T old, T value) where T : IEquatable<T>
{
if (old.Equals(value))
return false;
old = value;
return true;
}
private bool SetArmor(EquipSlot slot, SetId set, byte variant, string name, uint id)
{
var changes = false;
switch (slot)
{
case EquipSlot.Head:
changes |= SetIfDifferent(ref CharacterData.Head.Set, set);
changes |= SetIfDifferent(ref CharacterData.Head.Variant, variant);
changes |= HeadName != name;
HeadName = name;
changes |= Head != id;
Head = id;
return changes;
case EquipSlot.Body:
changes |= SetIfDifferent(ref CharacterData.Body.Set, set);
changes |= SetIfDifferent(ref CharacterData.Body.Variant, variant);
changes |= BodyName != name;
BodyName = name;
changes |= Body != id;
Body = id;
return changes;
case EquipSlot.Hands:
changes |= SetIfDifferent(ref CharacterData.Hands.Set, set);
changes |= SetIfDifferent(ref CharacterData.Hands.Variant, variant);
changes |= HandsName != name;
HandsName = name;
changes |= Hands != id;
Hands = id;
return changes;
case EquipSlot.Legs:
changes |= SetIfDifferent(ref CharacterData.Legs.Set, set);
changes |= SetIfDifferent(ref CharacterData.Legs.Variant, variant);
changes |= LegsName != name;
LegsName = name;
changes |= Legs != id;
Legs = id;
return changes;
case EquipSlot.Feet:
changes |= SetIfDifferent(ref CharacterData.Feet.Set, set);
changes |= SetIfDifferent(ref CharacterData.Feet.Variant, variant);
changes |= FeetName != name;
FeetName = name;
changes |= Feet != id;
Feet = id;
return changes;
case EquipSlot.Ears:
changes |= SetIfDifferent(ref CharacterData.Ears.Set, set);
changes |= SetIfDifferent(ref CharacterData.Ears.Variant, variant);
changes |= EarsName != name;
EarsName = name;
changes |= Ears != id;
Ears = id;
return changes;
case EquipSlot.Neck:
changes |= SetIfDifferent(ref CharacterData.Neck.Set, set);
changes |= SetIfDifferent(ref CharacterData.Neck.Variant, variant);
changes |= NeckName != name;
NeckName = name;
changes |= Neck != id;
Neck = id;
return changes;
case EquipSlot.Wrists:
changes |= SetIfDifferent(ref CharacterData.Wrists.Set, set);
changes |= SetIfDifferent(ref CharacterData.Wrists.Variant, variant);
changes |= WristsName != name;
WristsName = name;
changes |= Wrists != id;
Wrists = id;
return changes;
case EquipSlot.RFinger:
changes |= SetIfDifferent(ref CharacterData.RFinger.Set, set);
changes |= SetIfDifferent(ref CharacterData.RFinger.Variant, variant);
changes |= RFingerName != name;
RFingerName = name;
changes |= RFinger != id;
RFinger = id;
return changes;
case EquipSlot.LFinger:
changes |= SetIfDifferent(ref CharacterData.LFinger.Set, set);
changes |= SetIfDifferent(ref CharacterData.LFinger.Variant, variant);
changes |= LFingerName != name;
LFingerName = name;
changes |= LFinger != id;
LFinger = id;
return changes;
default: return false;
}
}
}

View file

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Plugin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using OtterGui.Filesystem;
namespace Glamourer.Designs;
public sealed class DesignFileSystem : FileSystem<Design>, IDisposable
{
public static string GetDesignFileSystemFile(DalamudPluginInterface pi)
=> Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json");
public readonly string DesignFileSystemFile;
private readonly FrameworkManager _framework;
private readonly Design.Manager _designManager;
public DesignFileSystem(Design.Manager designManager, DalamudPluginInterface pi, FrameworkManager framework)
{
DesignFileSystemFile = GetDesignFileSystemFile(pi);
_designManager = designManager;
_framework = framework;
_designManager.DesignChange += OnDataChange;
Changed += OnChange;
Reload();
}
private void Reload()
{
if (Load(new FileInfo(DesignFileSystemFile), _designManager.Designs, DesignToIdentifier, DesignToName))
SaveFilesystem();
Glamourer.Log.Debug("Reloaded design filesystem.");
}
public void Dispose()
{
_designManager.DesignChange -= 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 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));
}
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
{
if (type != FileSystemChangeType.Reload)
SaveFilesystem();
}
private void SaveFilesystem()
{
SaveToFile(new FileInfo(DesignFileSystemFile), SaveDesign, true);
Glamourer.Log.Verbose("Saved design filesystem.");
}
public void Save()
=> _framework.RegisterDelayed(nameof(SaveFilesystem), SaveFilesystem);
private void OnDataChange(Design.Manager.DesignChangeType type, Design design, object? data)
{
switch (type)
{
case Design.Manager.DesignChangeType.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 Design.Manager.DesignChangeType.Deleted:
if (FindLeaf(design, out var leaf))
Delete(leaf);
break;
case Design.Manager.DesignChangeType.ReloadedAll:
Reload();
break;
case Design.Manager.DesignChangeType.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(DalamudPluginInterface pi, Dictionary<string, string> oldPaths)
{
if (oldPaths.Count == 0)
return;
var file = GetDesignFileSystemFile(pi);
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}");
}
}
}

View file

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

View file

@ -1,350 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection.Metadata.Ecma335;
using Dalamud.Logging;
using System.Runtime;
using System.Text;
using Dalamud.Utility;
using Glamourer.Interop;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Structs;
using Glamourer.Saves;
using Penumbra.GameData.Actors;
namespace Glamourer.Designs;
public struct FixedCondition
{
private const ulong _territoryFlag = 1ul << 32;
private const ulong _jobFlag = 1ul << 33;
private ulong _data;
public static FixedCondition TerritoryCondition(ushort territoryType)
=> new() { _data = territoryType | _territoryFlag };
public static FixedCondition JobCondition(JobGroup group)
=> new() { _data = group.Id | _jobFlag };
public bool Check(Actor actor)
{
if ((_data & (_territoryFlag | _jobFlag)) == 0)
return true;
if ((_data & _territoryFlag) != 0)
return Dalamud.ClientState.TerritoryType == (ushort)_data;
if (actor && GameData.JobGroups(Dalamud.GameData).TryGetValue((ushort)_data, out var group) && group.Fits(actor.Job))
return true;
return true;
}
public override string ToString()
=> _data.ToString();
}
public class FixedDesign
{
public const int CurrentVersion = 0;
public string Name { get; private set; }
public bool Enabled;
public List<ActorIdentifier> Actors;
public List<(FixedCondition, Design)> Customization;
public List<(FixedCondition, Design)> Equipment;
public List<(FixedCondition, Design)> Weapons;
public FixedDesign(string name)
{
Name = name;
Actors = new List<ActorIdentifier>();
Customization = new List<(FixedCondition, Design)>();
Equipment = new List<(FixedCondition, Design)>();
Weapons = new List<(FixedCondition, Design)>();
}
public static FixedDesign? Load(JObject j)
{
try
{
var name = j[nameof(Name)]?.Value<string>();
if (name.IsNullOrEmpty())
return null;
var version = j["Version"]?.Value<int>();
if (version == null)
return null;
return version switch
{
CurrentVersion => LoadCurrentVersion(j, name),
_ => null,
};
}
catch (Exception e)
{
PluginLog.Error($"Error loading fixed design:\n{e}");
return null;
}
}
private static FixedDesign? LoadCurrentVersion(JObject j, string name)
{
var enabled = j[nameof(Enabled)]?.Value<bool>() ?? false;
var ret = new FixedDesign(name)
{
Enabled = enabled,
};
var actors = j[nameof(Actors)];
//foreach(var pair in actors?.Children().)
return null;
}
public void Save(FileInfo file)
{
try
{
using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew);
using var w = new StreamWriter(s, Encoding.UTF8);
using var j = new JsonTextWriter(w)
{
Formatting = Formatting.Indented,
};
j.WriteStartObject();
j.WritePropertyName(nameof(Name));
j.WriteValue(Name);
j.WritePropertyName("Version");
j.WriteValue(CurrentVersion);
j.WritePropertyName(nameof(Enabled));
j.WriteValue(Enabled);
j.WritePropertyName(nameof(Actors));
j.WriteStartArray();
foreach (var actor in Actors)
actor.ToJson().WriteTo(j);
j.WriteEndArray();
j.WritePropertyName(nameof(Customization));
j.WriteStartArray();
foreach (var (condition, design) in Customization)
{
j.WritePropertyName(condition.ToString());
j.WriteValue(design.Name);
}
j.WriteEndArray();
j.WritePropertyName(nameof(Equipment));
j.WriteStartArray();
foreach (var (condition, design) in Equipment)
{
j.WritePropertyName(condition.ToString());
j.WriteValue(design.Name);
}
j.WriteEndArray();
j.WritePropertyName(nameof(Weapons));
j.WriteStartArray();
foreach (var (condition, design) in Weapons)
{
j.WritePropertyName(condition.ToString());
j.WriteValue(design.Name);
}
j.WriteEndArray();
}
catch (Exception e)
{
PluginLog.Error($"Could not save collection {Name}:\n{e}");
}
}
public static bool Load(FileInfo path, [NotNullWhen(true)] out FixedDesign? result)
{
result = null;
return true;
}
}
public class FixedDesigns : IDisposable
{
//public class FixedDesign
//{
// public string Name;
// public JobGroup Jobs;
// public Design Design;
// public bool Enabled;
//
// public GlamourerConfig.FixedDesign ToSave()
// => new()
// {
// Name = Name,
// Path = Design.FullName(),
// Enabled = Enabled,
// JobGroups = Jobs.Id,
// };
//
// public FixedDesign(string name, Design design, bool enabled, JobGroup jobs)
// {
// Name = name;
// Design = design;
// Enabled = enabled;
// Jobs = jobs;
// }
//}
//
//public List<FixedDesign> Data;
//public Dictionary<string, List<FixedDesign>> EnabledDesigns;
//public readonly IReadOnlyDictionary<ushort, JobGroup> JobGroups;
//
//public bool EnableDesign(FixedDesign design)
//{
// var changes = !design.Enabled;
//
// if (!EnabledDesigns.TryGetValue(design.Name, out var designs))
// {
// EnabledDesigns[design.Name] = new List<FixedDesign> { design };
// // TODO
// changes = true;
// }
// else if (!designs.Contains(design))
// {
// designs.Add(design);
// changes = true;
// }
//
// design.Enabled = true;
// // TODO
// //if (Glamourer.Config.ApplyFixedDesigns)
// //{
// // var character =
// // CharacterFactory.Convert(Dalamud.Objects.FirstOrDefault(o
// // => o.ObjectKind == ObjectKind.Player && o.Name.ToString() == design.Name));
// // if (character != null)
// // OnPlayerChange(character);
// //}
//
// return changes;
//}
//
//public bool DisableDesign(FixedDesign design)
//{
// if (!design.Enabled)
// return false;
//
// design.Enabled = false;
// if (!EnabledDesigns.TryGetValue(design.Name, out var designs))
// return false;
// if (!designs.Remove(design))
// return false;
//
// if (designs.Count == 0)
// {
// EnabledDesigns.Remove(design.Name);
// // TODO
// }
//
// return true;
//}
//
//public FixedDesigns(DesignManager designs)
//{
// JobGroups = GameData.JobGroups(Dalamud.GameData);
// Data = new List<FixedDesign>(Glamourer.Config.FixedDesigns.Count);
// EnabledDesigns = new Dictionary<string, List<FixedDesign>>(Glamourer.Config.FixedDesigns.Count);
// var changes = false;
// for (var i = 0; i < Glamourer.Config.FixedDesigns.Count; ++i)
// {
// var save = Glamourer.Config.FixedDesigns[i];
// if (designs.FileSystem.Find(save.Path, out var d) && d is Design design)
// {
// if (!JobGroups.TryGetValue((ushort)save.JobGroups, out var jobGroup))
// jobGroup = JobGroups[1];
// Data.Add(new FixedDesign(save.Name, design, save.Enabled, jobGroup));
// if (save.Enabled)
// changes |= EnableDesign(Data.Last());
// }
// else
// {
// PluginLog.Warning($"{save.Path} does not exist anymore, removing {save.Name} from fixed designs.");
// Glamourer.Config.FixedDesigns.RemoveAt(i--);
// changes = true;
// }
// }
//
// if (changes)
// Glamourer.Config.Save();
//}
//
//private void OnPlayerChange(Character character)
//{
// //var name = character.Name.ToString();
// //if (!EnabledDesigns.TryGetValue(name, out var designs))
// // return;
// //
// //var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(character.ClassJob.Id));
// //if (design == null)
// // return;
// //
// //PluginLog.Debug("Redrawing {CharacterName} with {DesignName} for job {JobGroup}.", name, design.Design.FullName(),
// // design.Jobs.Name);
// //design.Design.Data.Apply(character);
// //Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(character);
// //Glamourer.Penumbra.RedrawObject(character, RedrawType.Redraw, false);
//}
//
//public void Add(string name, Design design, JobGroup group, bool enabled = false)
//{
// Data.Add(new FixedDesign(name, design, enabled, group));
// Glamourer.Config.FixedDesigns.Add(Data.Last().ToSave());
//
// if (enabled)
// EnableDesign(Data.Last());
//
// Glamourer.Config.Save();
//}
//
//public void Remove(FixedDesign design)
//{
// var idx = Data.IndexOf(design);
// if (idx < 0)
// return;
//
// Data.RemoveAt(idx);
// Glamourer.Config.FixedDesigns.RemoveAt(idx);
// if (design.Enabled)
// {
// EnabledDesigns.Remove(design.Name);
// // TODO
// }
//
// Glamourer.Config.Save();
//}
//
//public void Move(FixedDesign design, int newIdx)
//{
// if (newIdx < 0)
// newIdx = 0;
// if (newIdx >= Data.Count)
// newIdx = Data.Count - 1;
//
// var idx = Data.IndexOf(design);
// if (idx < 0 || idx == newIdx)
// return;
//
// Data.RemoveAt(idx);
// Data.Insert(newIdx, design);
// Glamourer.Config.FixedDesigns.RemoveAt(idx);
// Glamourer.Config.FixedDesigns.Insert(newIdx, design.ToSave());
// Glamourer.Config.Save();
//}
//
public void Dispose()
{
//Glamourer.Config.FixedDesigns = Data.Select(d => d.ToSave()).ToList();
//Glamourer.Config.Save();
}
}

View file

@ -1,41 +0,0 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.State;
namespace Glamourer.Designs;
public class RevertableDesigns
{
public readonly Dictionary<string, CharacterSave> Saves = new();
public bool Add(Character actor)
{
//var name = actor.Name.ToString();
//if (Saves.TryGetValue(name, out var save))
// return false;
//
//save = new CharacterSave();
//save.LoadCharacter(actor);
//Saves[name] = save;
return true;
}
public bool RevertByNameWithoutApplication(string actorName)
{
if (!Saves.ContainsKey(actorName))
return false;
Saves.Remove(actorName);
return true;
}
public bool Revert(Character actor)
{
//if (!Saves.TryGetValue(actor.Name.ToString(), out var save))
// return false;
//
//save.Apply(actor);
//Saves.Remove(actor.Name.ToString());
return true;
}
}

View file

@ -0,0 +1,72 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public readonly struct Item
{
public readonly string Name;
public readonly uint ItemId;
public readonly CharacterArmor Model;
public SetId ModelBase
=> Model.Set;
public byte Variant
=> Model.Variant;
public StainId Stain
=> Model.Stain;
public Item(string name, uint itemId, CharacterArmor armor)
{
Name = name;
ItemId = itemId;
Model.Set = armor.Set;
Model.Variant = armor.Variant;
Model.Stain = armor.Stain;
}
}
public readonly struct Weapon
{
public readonly string Name = string.Empty;
public readonly uint ItemId;
public readonly FullEquipType Type;
public readonly bool Valid;
public readonly CharacterWeapon Model;
public SetId ModelBase
=> Model.Set;
public WeaponType WeaponBase
=> Model.Type;
public byte Variant
=> (byte)Model.Variant;
public StainId Stain
=> Model.Stain;
public Weapon(string name, uint itemId, CharacterWeapon weapon, FullEquipType type)
{
Name = name;
ItemId = itemId;
Type = type;
Valid = true;
Model.Set = weapon.Set;
Model.Type = weapon.Type;
Model.Variant = (byte)weapon.Variant;
Model.Stain = weapon.Stain;
}
public static Weapon Offhand(string name, uint itemId, CharacterWeapon weapon, FullEquipType type)
{
var offType = type.Offhand();
return offType is FullEquipType.Unknown
? new Weapon()
: new Weapon(name, itemId, weapon, offType);
}
}