This commit is contained in:
Ottermandias 2023-07-07 20:24:44 +02:00
parent 5c003d8cd4
commit f8e9cc8988
43 changed files with 2215 additions and 668 deletions

View file

@ -33,9 +33,6 @@ public class CustomizationManager : ICustomizationManager
public ImGuiScene.TextureWrap GetIcon(uint iconId)
=> _options!.GetIcon(iconId);
public void RemoveIcon(uint iconId)
=> _options!.RemoveIcon(iconId);
public string GetName(CustomName name)
=> _options!.GetName(name);
}

View file

@ -38,9 +38,6 @@ public partial class CustomizationOptions
internal ImGuiScene.TextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id);
internal void RemoveIcon(uint id)
=> _icons.RemoveIcon(id);
private readonly IconStorage _icons;
private static readonly int ListSize = Clans.Length * Genders.Length;

View file

@ -249,7 +249,7 @@ public class CustomizationSet
_ => index switch
{
CustomizeIndex.Face => Faces.Count,
CustomizeIndex.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face.Value].Count : 0,
CustomizeIndex.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count ? HairByFace[face.Value].Count : HairStyles.Count,
CustomizeIndex.SkinColor => SkinColors.Count,
CustomizeIndex.EyeColorRight => EyeColors.Count,
CustomizeIndex.HairColor => HairColors.Count,

View file

@ -48,6 +48,7 @@ public enum CustomizeFlag : ulong
public static class CustomizeFlagExtensions
{
public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul);
public const CustomizeFlag AllRelevant = All & ~CustomizeFlag.BodyType & ~CustomizeFlag.Race;
public const CustomizeFlag RedrawRequired = CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face | CustomizeFlag.BodyType;
public static bool RequiresRedraw(this CustomizeFlag flags)

View file

@ -12,6 +12,5 @@ public interface ICustomizationManager
public CustomizationSet GetList(SubRace race, Gender gender);
public ImGuiScene.TextureWrap GetIcon(uint iconId);
public void RemoveIcon(uint iconId);
public string GetName(CustomName name);
}

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Designs;
@ -45,24 +44,24 @@ public partial class GlamourerIpc
public void ApplyAll(string base64, string characterName)
=> ApplyDesign(CreateTemporaryFromBase64(base64, true, true), FindActors(characterName));
=> ApplyDesign(_designConverter.FromBase64(base64, true, true), FindActors(characterName));
public void ApplyAllToCharacter(string base64, Character? character)
=> ApplyDesign(CreateTemporaryFromBase64(base64, true, true), FindActors(character));
=> ApplyDesign(_designConverter.FromBase64(base64, true, true), FindActors(character));
public void ApplyOnlyEquipment(string base64, string characterName)
=> ApplyDesign(CreateTemporaryFromBase64(base64, false, true), FindActors(characterName));
=> ApplyDesign(_designConverter.FromBase64(base64, false, true), FindActors(characterName));
public void ApplyOnlyEquipmentToCharacter(string base64, Character? character)
=> ApplyDesign(CreateTemporaryFromBase64(base64, false, true), FindActors(character));
=> ApplyDesign(_designConverter.FromBase64(base64, false, true), FindActors(character));
public void ApplyOnlyCustomization(string base64, string characterName)
=> ApplyDesign(CreateTemporaryFromBase64(base64, true, false), FindActors(characterName));
=> ApplyDesign(_designConverter.FromBase64(base64, true, false), FindActors(characterName));
public void ApplyOnlyCustomizationToCharacter(string base64, Character? character)
=> ApplyDesign(CreateTemporaryFromBase64(base64, true, false), FindActors(character));
=> ApplyDesign(_designConverter.FromBase64(base64, true, false), FindActors(character));
private void ApplyDesign(Design? design, IEnumerable<ActorIdentifier> actors)
private void ApplyDesign(DesignBase? design, IEnumerable<ActorIdentifier> actors)
{
if (design == null)
return;
@ -80,33 +79,4 @@ public partial class GlamourerIpc
_stateManager.ApplyDesign(design, state);
}
}
private Design? CreateTemporaryFromBase64(string base64, bool customize, bool equip)
{
try
{
var ret = new Design(_items);
ret.MigrateBase64(_items, base64);
if (!customize)
{
ret.ApplyCustomize = 0;
ret.SetApplyWetness(false);
}
if (!equip)
{
ret.ApplyEquip = 0;
ret.SetApplyHatVisible(false);
ret.SetApplyWeaponVisible(false);
ret.SetApplyVisorToggle(false);
}
return ret;
}
catch (Exception ex)
{
Glamourer.Log.Error($"[IPC] Could not parse base64 string [{base64}]:\n{ex}");
return null;
}
}
}

View file

@ -3,6 +3,7 @@ using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using Glamourer.Designs;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.State;
@ -21,13 +22,17 @@ public partial class GlamourerIpc : IDisposable
private readonly ObjectManager _objects;
private readonly ActorService _actors;
private readonly ItemManager _items;
private readonly DesignConverter _designConverter;
public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors, ItemManager items)
public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors, ItemManager items,
DesignConverter designConverter)
{
_stateManager = stateManager;
_objects = objects;
_actors = actors;
_items = items;
_designConverter = designConverter;
_apiVersionProvider = new FuncProvider<int>(pi, LabelApiVersion, ApiVersion);
_apiVersionsProvider = new FuncProvider<(int Major, int Minor)>(pi, LabelApiVersions, ApiVersions);

View file

@ -21,7 +21,7 @@ public class AutoDesign
All = Armor | Accessories | Customizations | Weapons | Stains,
}
public Design Design;
public Design Design = null!;
public JobGroup Jobs;
public Type ApplicationType;

View file

@ -25,9 +25,12 @@ public class AutoDesignApplier : IDisposable
private readonly CustomizationService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly ItemUnlockManager _itemUnlocks;
private readonly AutomationChanged _event;
private readonly ObjectManager _objects;
public AutoDesignApplier(Configuration config, AutoDesignManager manager, CodeService code, StateManager state, JobService jobs,
CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks)
CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks,
AutomationChanged @event, ObjectManager objects)
{
_config = config;
_manager = manager;
@ -38,14 +41,59 @@ public class AutoDesignApplier : IDisposable
_actors = actors;
_itemUnlocks = itemUnlocks;
_customizeUnlocks = customizeUnlocks;
_event = @event;
_objects = objects;
_jobs.JobChanged += OnJobChange;
_event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier);
}
public void Dispose()
{
_event.Unsubscribe(OnAutomationChange);
_jobs.JobChanged -= OnJobChange;
}
private void OnAutomationChange(AutomationChanged.Type type, AutoDesignSet? set, object? _)
{
if (!_config.EnableAutoDesigns || set is not { Enabled: true })
return;
switch (type)
{
case AutomationChanged.Type.ChangeIdentifier:
case AutomationChanged.Type.ToggleSet:
case AutomationChanged.Type.AddedDesign:
case AutomationChanged.Type.DeletedDesign:
case AutomationChanged.Type.MovedDesign:
case AutomationChanged.Type.ChangedDesign:
case AutomationChanged.Type.ChangedConditions:
_objects.Update();
if (_objects.TryGetValue(set.Identifier, out var data))
{
if (_state.GetOrCreate(set.Identifier, data.Objects[0], out var state))
{
Reduce(data.Objects[0], state, set, false);
foreach (var actor in data.Objects)
_state.ReapplyState(actor);
}
}
else if (_objects.TryGetValueAllWorld(set.Identifier, out data))
{
foreach (var actor in data.Objects)
{
var id = actor.GetIdentifier(_actors.AwaitedService);
if (_state.GetOrCreate(id, actor, out var state))
{
Reduce(actor, state, set, false);
_state.ReapplyState(actor);
}
}
}
break;
}
}
private void OnJobChange(Actor actor, Job _)
{
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id))
@ -242,28 +290,28 @@ public class AutoDesignApplier : IDisposable
{
if (applyHat && (totalMetaFlags & 0x01) == 0)
{
if (!respectManual || state[ActorState.MetaFlag.HatState] is not StateChanged.Source.Manual)
if (!respectManual || state[ActorState.MetaIndex.HatState] is not StateChanged.Source.Manual)
_state.ChangeHatState(state, design.IsHatVisible(), StateChanged.Source.Fixed);
totalMetaFlags |= 0x01;
}
if (applyVisor && (totalMetaFlags & 0x02) == 0)
{
if (!respectManual || state[ActorState.MetaFlag.VisorState] is not StateChanged.Source.Manual)
if (!respectManual || state[ActorState.MetaIndex.VisorState] is not StateChanged.Source.Manual)
_state.ChangeVisorState(state, design.IsVisorToggled(), StateChanged.Source.Fixed);
totalMetaFlags |= 0x02;
}
if (applyWeapon && (totalMetaFlags & 0x04) == 0)
{
if (!respectManual || state[ActorState.MetaFlag.WeaponState] is not StateChanged.Source.Manual)
if (!respectManual || state[ActorState.MetaIndex.WeaponState] is not StateChanged.Source.Manual)
_state.ChangeWeaponState(state, design.IsWeaponVisible(), StateChanged.Source.Fixed);
totalMetaFlags |= 0x04;
}
if (applyWet && (totalMetaFlags & 0x08) == 0)
{
if (!respectManual || state[ActorState.MetaFlag.Wetness] is not StateChanged.Source.Manual)
if (!respectManual || state[ActorState.MetaIndex.Wetness] is not StateChanged.Source.Manual)
_state.ChangeWetness(state, design.IsWet(), StateChanged.Source.Fixed);
totalMetaFlags |= 0x08;
}

View file

@ -1,29 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class Design : ISavable
public sealed class Design : DesignBase, ISavable
{
#region Data
internal Design(ItemManager items)
: base(items)
{ }
internal Design(DesignBase other)
: base(other)
{ }
internal Design(Design other)
: base(other)
{
DesignData.SetDefaultEquipment(items);
Tags = Tags.ToArray();
Description = Description;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
}
// Metadata
public const int FileVersion = 1;
public new const int FileVersion = 1;
public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; internal init; }
@ -32,185 +41,19 @@ public class Design : ISavable
public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; internal set; } = Array.Empty<string>();
public int Index { get; internal set; }
internal DesignData DesignData;
public SortedList<Mod, ModSettings> AssociatedMods { get; private set; } = new();
public string Incognito
=> Identifier.ToString()[..8];
/// <summary> Unconditionally apply a design to a designdata. </summary>
/// <returns>Whether a redraw is required for the changes to take effect.</returns>
public (bool, CustomizeFlag, EquipFlag) ApplyDesign(ref DesignData data)
{
var modelChanged = data.ModelId != DesignData.ModelId;
data.ModelId = DesignData.ModelId;
CustomizeFlag customizeFlags = 0;
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
if (!DoApplyCustomize(index))
continue;
if (data.Customize.Set(index, DesignData.Customize[index]))
customizeFlags |= index.ToFlag();
}
EquipFlag equipFlags = 0;
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
{
if (DoApplyEquip(slot))
if (data.SetItem(slot, DesignData.Item(slot)))
equipFlags |= slot.ToFlag();
if (DoApplyStain(slot))
if (data.SetStain(slot, DesignData.Stain(slot)))
equipFlags |= slot.ToStainFlag();
}
if (DoApplyHatVisible())
data.SetHatVisible(DesignData.IsHatVisible());
if (DoApplyVisorToggle())
data.SetVisor(DesignData.IsVisorToggled());
if (DoApplyWeaponVisible())
data.SetWeaponVisible(DesignData.IsWeaponVisible());
if (DoApplyWetness())
data.SetIsWet(DesignData.IsWet());
return (modelChanged, customizeFlags, equipFlags);
}
#endregion
#region Application Data
[Flags]
private enum DesignFlags : byte
{
ApplyHatVisible = 0x01,
ApplyVisorState = 0x02,
ApplyWeaponVisible = 0x04,
ApplyWetness = 0x08,
WriteProtected = 0x10,
}
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);
public bool DoApplyVisorToggle()
=> _designFlags.HasFlag(DesignFlags.ApplyVisorState);
public bool DoApplyWeaponVisible()
=> _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible);
public bool DoApplyWetness()
=> _designFlags.HasFlag(DesignFlags.ApplyWetness);
public bool WriteProtected()
=> _designFlags.HasFlag(DesignFlags.WriteProtected);
public bool SetApplyHatVisible(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyVisorToggle(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyWeaponVisible(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyWetness(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyWetness : _designFlags & ~DesignFlags.ApplyWetness;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetWriteProtected(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
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());
internal bool SetApplyEquip(EquipSlot slot, bool value)
{
var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag();
if (newValue == ApplyEquip)
return false;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyStain(EquipSlot slot, bool value)
{
var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag();
if (newValue == ApplyEquip)
return false;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{
var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag();
if (newValue == ApplyCustomize)
return false;
ApplyCustomize = newValue;
return true;
}
#endregion
#region Serialization
private JObject JsonSerialize()
public new JObject JsonSerialize()
{
var ret = new JObject
var ret = new JObject()
{
["FileVersion"] = FileVersion,
["Identifier"] = Identifier,
@ -222,59 +65,32 @@ public class Design : ISavable
["WriteProtected"] = WriteProtected(),
["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(),
};
["Mods"] = SerializeMods(),
}
;
return ret;
}
private JObject SerializeEquipment()
private JArray SerializeMods()
{
static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain)
=> new()
var ret = new JArray();
foreach (var (mod, settings) in AssociatedMods)
{
["ItemId"] = itemId,
["Stain"] = stain.Value,
["Apply"] = apply,
["ApplyStain"] = applyStain,
var obj = new JObject()
{
["Name"] = mod.Name,
["Directory"] = mod.DirectoryName,
["Enabled"] = settings.Enabled,
};
var ret = new JObject();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
if (settings.Enabled)
{
var item = DesignData.Item(slot);
var stain = DesignData.Stain(slot);
ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot));
obj["Priority"] = settings.Priority;
obj["Settings"] = JObject.FromObject(settings.Settings);
}
ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply");
ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply");
ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply");
return ret;
ret.Add(obj);
}
private JObject SerializeCustomize()
{
var ret = new JObject()
{
["ModelId"] = DesignData.ModelId,
};
var customize = DesignData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
ret[idx.ToString()] = new JObject()
{
["Value"] = customize[idx].Value,
["Apply"] = DoApplyCustomize(idx),
};
}
ret["Wetness"] = new JObject()
{
["Value"] = DesignData.IsWet(),
["Apply"] = DoApplyWetness(),
};
return ret;
}
@ -287,7 +103,7 @@ public class Design : ISavable
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch
{
1 => LoadDesignV1(customizations, items, json),
FileVersion => LoadDesignV1(customizations, items, json),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
}
@ -314,128 +130,37 @@ public class Design : ISavable
if (design.LastEdit < creationDate)
design.LastEdit = creationDate;
LoadEquip(items, json["Equipment"], design);
LoadCustomize(customizations, json["Customize"], design);
LoadEquip(items, json["Equipment"], design, design.Name);
LoadCustomize(customizations, json["Customize"], design, design.Name);
LoadMods(json["Mods"], design);
return design;
}
private static void LoadEquip(ItemManager items, JToken? equip, Design design)
private static void LoadMods(JToken? mods, Design design)
{
if (equip == null)
{
design.DesignData.SetDefaultEquipment(items);
Glamourer.Chat.NotificationMessage("The loaded design does not contain any equipment data, reset to default.", "Warning",
NotificationType.Warning);
if (mods is not JArray array)
return;
foreach (var tok in array)
{
var name = tok["Name"]?.ToObject<string>();
var directory = tok["Directory"]?.ToObject<string>();
var enabled = tok["Enabled"]?.ToObject<bool>();
if (name == null || directory == null || enabled == null)
{
Glamourer.Chat.NotificationMessage("The loaded design contains an invalid mod, skipped.", "Warning", NotificationType.Warning);
continue;
}
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);
}
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()]);
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);
}
{
var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.MainHand))
id = items.DefaultSword.Id;
var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield);
PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off));
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);
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
}
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyHatVisible(metaValue.Enabled);
design.DesignData.SetHatVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyWeaponVisible(metaValue.Enabled);
design.DesignData.SetWeaponVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
design.SetApplyVisorToggle(metaValue.Enabled);
design.DesignData.SetVisor(metaValue.ForcedValue);
}
private static void LoadCustomize(CustomizationService customizations, JToken? json, Design design)
{
if (json == null)
{
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",
var settingsDict = tok["Settings"]?.ToObject<Dictionary<string, string[]>>() ?? new Dictionary<string, string[]>();
var settings = new SortedList<string, IList<string>>(settingsDict.Count);
foreach (var (key, value) in settingsDict)
settings.Add(key, value);
var priority = tok["Priority"]?.ToObject<int>() ?? 0;
if (!design.AssociatedMods.TryAdd(new Mod(name, directory), new ModSettings(settings, priority, enabled.Value)))
Glamourer.Chat.NotificationMessage("The loaded design contains a mod more than once, skipped.", "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;
PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId));
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);
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);
}
var wetness = QuadBool.FromJObject(json["Wetness"], "Value", "Apply", QuadBool.NullFalse);
design.DesignData.SetIsWet(wetness.ForcedValue);
design.SetApplyWetness(wetness.Enabled);
}
#endregion
@ -459,39 +184,4 @@ public class Design : ISavable
=> 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);
SetApplyWetness(DesignData.IsWet());
}
//
//public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip)
//{
// var ret = new Design(items);
// ret.MigrateBase64(items, base64);
// if (!customize)
// ret._applyCustomize = 0;
// if (!equip)
// ret._applyEquip = 0;
// ret.Wetness = ret.Wetness.SetEnabled(customize);
// ret.Visor = ret.Visor.SetEnabled(equip);
// ret.Hat = ret.Hat.SetEnabled(equip);
// ret.Weapon = ret.Weapon.SetEnabled(equip);
// return ret;
//}
// Outdated.
//public string CreateOldBase64()
// => DesignBase64Migration.CreateOldBase64(in ModelData, _applyEquip, _applyCustomize, Wetness == QuadBool.True, Hat.ForcedValue,
// Hat.Enabled,
// Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f);
}

View file

@ -0,0 +1,375 @@
using System;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignBase
{
public const int FileVersion = 1;
internal DesignBase(ItemManager items)
{
DesignData.SetDefaultEquipment(items);
}
internal DesignBase(DesignBase clone)
{
DesignData = clone.DesignData;
ApplyCustomize = clone.ApplyCustomize & CustomizeFlagExtensions.All;
ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
_designFlags = clone._designFlags & (DesignFlags) 0x0F;
}
internal DesignData DesignData = new();
#region Application Data
[Flags]
private enum DesignFlags : byte
{
ApplyHatVisible = 0x01,
ApplyVisorState = 0x02,
ApplyWeaponVisible = 0x04,
ApplyWetness = 0x08,
WriteProtected = 0x10,
}
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);
public bool DoApplyVisorToggle()
=> _designFlags.HasFlag(DesignFlags.ApplyVisorState);
public bool DoApplyWeaponVisible()
=> _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible);
public bool DoApplyWetness()
=> _designFlags.HasFlag(DesignFlags.ApplyWetness);
public bool WriteProtected()
=> _designFlags.HasFlag(DesignFlags.WriteProtected);
public bool SetApplyHatVisible(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyVisorToggle(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyWeaponVisible(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyWetness(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyWetness : _designFlags & ~DesignFlags.ApplyWetness;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetWriteProtected(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool DoApplyEquip(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToFlag());
public bool DoApplyStain(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToStainFlag());
public bool DoApplyCustomize(CustomizeIndex idx)
=> idx is not CustomizeIndex.Race and not CustomizeIndex.BodyType && ApplyCustomize.HasFlag(idx.ToFlag());
internal bool SetApplyEquip(EquipSlot slot, bool value)
{
var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag();
if (newValue == ApplyEquip)
return false;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyStain(EquipSlot slot, bool value)
{
var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag();
if (newValue == ApplyEquip)
return false;
ApplyEquip = newValue;
return true;
}
internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{
var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag();
if (newValue == ApplyCustomize)
return false;
ApplyCustomize = newValue;
return true;
}
#endregion
#region Serialization
public JObject JsonSerialize()
{
var ret = new JObject
{
["FileVersion"] = FileVersion,
["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(),
};
return ret;
}
protected JObject SerializeEquipment()
{
static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain)
=> new()
{
["ItemId"] = itemId,
["Stain"] = stain.Value,
["Apply"] = apply,
["ApplyStain"] = applyStain,
};
var ret = new JObject();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
var item = DesignData.Item(slot);
var stain = DesignData.Stain(slot);
ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot));
}
ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply");
ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply");
ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply");
return ret;
}
protected JObject SerializeCustomize()
{
var ret = new JObject()
{
["ModelId"] = DesignData.ModelId,
};
var customize = DesignData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
ret[idx.ToString()] = new JObject()
{
["Value"] = customize[idx].Value,
["Apply"] = DoApplyCustomize(idx),
};
}
ret["Wetness"] = new JObject()
{
["Value"] = DesignData.IsWet(),
["Apply"] = DoApplyWetness(),
};
return ret;
}
#endregion
#region Deserialization
public static DesignBase LoadDesignBase(CustomizationService customizations, ItemManager items, JObject json)
{
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch
{
FileVersion => LoadDesignV1Base(customizations, items, json),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
}
private static DesignBase LoadDesignV1Base(CustomizationService customizations, ItemManager items, JObject json)
{
var ret = new DesignBase(items);
LoadEquip(items, json["Equipment"], ret, "Temporary Design");
LoadCustomize(customizations, json["Customize"], ret, "Temporary Design");
return ret;
}
protected static void LoadEquip(ItemManager items, JToken? equip, DesignBase design, string name)
{
if (equip == null)
{
design.DesignData.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)
{
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);
}
void PrintWarning(string msg)
{
if (msg.Length > 0)
Glamourer.Chat.NotificationMessage($"{msg} ({name})", "Warning", NotificationType.Warning);
}
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
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);
}
{
var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.MainHand))
id = items.DefaultSword.Id;
var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield);
PrintWarning(items.ValidateWeapons(id, idOff, out var main, out var off));
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);
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
}
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyHatVisible(metaValue.Enabled);
design.DesignData.SetHatVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyWeaponVisible(metaValue.Enabled);
design.DesignData.SetWeaponVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
design.SetApplyVisorToggle(metaValue.Enabled);
design.DesignData.SetVisor(metaValue.ForcedValue);
}
protected static void LoadCustomize(CustomizationService customizations, JToken? json, DesignBase design, string name)
{
if (json == null)
{
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} ({name})", "Warning", NotificationType.Warning);
}
design.DesignData.ModelId = json["ModelId"]?.ToObject<uint>() ?? 0;
PrintWarning(customizations.ValidateModelId(design.DesignData.ModelId, out design.DesignData.ModelId));
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);
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);
}
var wetness = QuadBool.FromJObject(json["Wetness"], "Value", "Apply", QuadBool.NullFalse);
design.DesignData.SetIsWet(wetness.ForcedValue);
design.SetApplyWetness(wetness.Enabled);
}
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);
SetApplyWetness(DesignData.IsWet());
}
#endregion
}

View file

@ -0,0 +1,121 @@
using System;
using System.Diagnostics;
using System.Text;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public class DesignConverter
{
public const byte Version = 3;
private readonly ItemManager _items;
private readonly DesignManager _designs;
private readonly CustomizationService _customize;
public DesignConverter(ItemManager items, DesignManager designs, CustomizationService customize)
{
_items = items;
_designs = designs;
_customize = customize;
}
public JObject ShareJObject(DesignBase design)
=> design.JsonSerialize();
public JObject ShareJObject(Design design)
=> design.JsonSerialize();
public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
var design = Convert(state, equipFlags, customizeFlags);
return ShareJObject(design);
}
public string ShareBase64(DesignBase design)
=> ShareBase64(ShareJObject(design));
public string ShareBase64(ActorState state)
=> ShareBase64(ShareJObject(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All));
public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
var design = _designs.CreateTemporary();
design.ApplyEquip = equipFlags & EquipFlagExtensions.All;
design.ApplyCustomize = customizeFlags & CustomizeFlagExtensions.All;
design.SetApplyHatVisible(design.DoApplyEquip(EquipSlot.Head));
design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head));
design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand));
design.SetApplyWetness(design.DesignData.IsWet());
design.DesignData = state.ModelData;
return design;
}
public DesignBase? FromBase64(string base64, bool customize, bool equip)
{
var bytes = System.Convert.FromBase64String(base64);
DesignBase ret;
try
{
switch (bytes[0])
{
case (byte)'{':
var jObj1 = JObject.Parse(Encoding.UTF8.GetString(bytes));
ret = jObj1["Identifier"] != null
? Design.LoadDesign(_customize, _items, jObj1)
: DesignBase.LoadDesignBase(_customize, _items, jObj1);
break;
case 1:
case 2:
ret = _designs.CreateTemporary();
ret.MigrateBase64(_items, base64);
break;
case Version:
var version = bytes.DecompressToString(out var decompressed);
var jObj2 = JObject.Parse(decompressed);
Debug.Assert(version == Version);
ret = jObj2["Identifier"] != null
? Design.LoadDesign(_customize, _items, jObj2)
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
break;
default: throw new Exception($"Unknown Version {bytes[0]}.");
}
}
catch (Exception ex)
{
Glamourer.Log.Error($"[DesignConverter] Could not parse base64 string [{base64}]:\n{ex}");
return null;
}
if (!customize)
{
ret.ApplyCustomize = 0;
ret.SetApplyWetness(false);
}
if (!equip)
{
ret.ApplyEquip = 0;
ret.SetApplyHatVisible(false);
ret.SetApplyWeaponVisible(false);
ret.SetApplyVisorToggle(false);
}
return ret;
}
private static string ShareBase64(JObject jObj)
{
var json = jObj.ToString(Formatting.None);
var compressed = json.Compress(Version);
return System.Convert.ToBase64String(compressed);
}
}

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Glamourer.Events;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -101,26 +102,22 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
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;
CreateDuplicateLeaf(Root, design.Name.Text, design);
return;
case DesignChanged.Type.Deleted:
if (FindLeaf(design, out var leaf))
Delete(leaf);
break;
if (FindLeaf(design, out var leaf1))
Delete(leaf1);
return;
case DesignChanged.Type.ReloadedAll:
Reload();
break;
return;
case DesignChanged.Type.Renamed when data is string oldName:
if (!FindLeaf(design, out var leaf2))
return;
var old = oldName.FixName();
if (Find(old, out var child) && child is not Folder)
Rename(child, design.Name);
break;
if (old == leaf2.Name || leaf2.Name.IsDuplicateName(out var baseName, out _) && baseName == old)
RenameWithDuplicates(leaf2, design.Name);
return;
}
}

View file

@ -5,7 +5,9 @@ using System.Linq;
using Dalamud.Utility;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
@ -78,16 +80,20 @@ public class DesignManager
_event.Invoke(DesignChanged.Type.ReloadedAll, null!);
}
/// <summary> Create a new temporary design without adding it to the manager. </summary>
public DesignBase CreateTemporary()
=> new(_items);
/// <summary> Create a new design of a given name. </summary>
public Design Create(string name)
public Design CreateEmpty(string name)
{
var design = new Design(_items)
{
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Index = _designs.Count,
Name = name,
Index = _designs.Count,
};
_designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
@ -96,6 +102,43 @@ public class DesignManager
return design;
}
/// <summary> Create a new design cloning a given temporary design. </summary>
public Design CreateClone(DesignBase clone, string name)
{
var design = new Design(clone)
{
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = name,
Index = _designs.Count,
};
_designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier} by cloning Temporary Design.");
_saveService.ImmediateSave(design);
_event.Invoke(DesignChanged.Type.Created, design);
return design;
}
/// <summary> Create a new design cloning a given design. </summary>
public Design CreateClone(Design clone, string name)
{
var design = new Design(clone)
{
CreationDate = DateTimeOffset.UtcNow,
LastEdit = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = name,
Index = _designs.Count,
};
_designs.Add(design);
Glamourer.Log.Debug(
$"Added new design {design.Identifier} by cloning {clone.Identifier.ToString()}.");
_saveService.ImmediateSave(design);
_event.Invoke(DesignChanged.Type.Created, design);
return design;
}
/// <summary> Delete a design. </summary>
public void Delete(Design design)
{
@ -181,6 +224,41 @@ public class DesignManager
_event.Invoke(DesignChanged.Type.ChangedTag, design, (oldTag, newTag, tagIdx));
}
/// <summary> Add an associated mod to a design. </summary>
public void AddMod(Design design, Mod mod, ModSettings settings)
{
if (!design.AssociatedMods.TryAdd(mod, settings))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Added associated mod {mod.DirectoryName} to design {design.Identifier}.");
_event.Invoke(DesignChanged.Type.AddedMod, design, (mod, settings));
}
/// <summary> Remove an associated mod from a design. </summary>
public void RemoveMod(Design design, Mod mod)
{
if (!design.AssociatedMods.Remove(mod, out var settings))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Removed associated mod {mod.DirectoryName} from design {design.Identifier}.");
_event.Invoke(DesignChanged.Type.RemovedMod, design, (mod, settings));
}
/// <summary> Set the write protection status of a design. </summary>
public void SetWriteProtection(Design design, bool value)
{
if (!design.SetWriteProtected(value))
return;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set design {design.Identifier} to {(value ? "no longer be " : string.Empty)} write-protected.");
_event.Invoke(DesignChanged.Type.WriteProtection, design, value);
}
/// <summary> Change a customization value. </summary>
public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value)
{
@ -210,6 +288,7 @@ public class DesignManager
break;
}
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug($"Changed customize {idx.ToDefaultName()} in design {design.Identifier} from {oldValue.Value} to {value.Value}.");
_saveService.QueueSave(design);
_event.Invoke(DesignChanged.Type.Customize, design, (oldValue, value, idx));
@ -237,6 +316,7 @@ public class DesignManager
if (!design.DesignData.SetItem(slot, item))
return;
design.LastEdit = DateTimeOffset.UtcNow;
Glamourer.Log.Debug(
$"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}).");
_saveService.QueueSave(design);
@ -257,7 +337,6 @@ public class DesignManager
if (item.Type != currentMain.Type)
{
var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type)
? item.Id
: ItemManager.NothingId(item.Type.Offhand());
@ -332,6 +411,87 @@ public class DesignManager
_event.Invoke(DesignChanged.Type.ApplyStain, design, slot);
}
/// <summary> Change the bool value of one of the meta flags. </summary>
public void ChangeMeta(Design design, ActorState.MetaIndex metaIndex, bool value)
{
var change = metaIndex switch
{
ActorState.MetaIndex.Wetness => design.DesignData.SetIsWet(value),
ActorState.MetaIndex.HatState => design.DesignData.SetHatVisible(value),
ActorState.MetaIndex.VisorState => design.DesignData.SetVisor(value),
ActorState.MetaIndex.WeaponState => design.DesignData.SetWeaponVisible(value),
_ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null),
};
if (!change)
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set value of {metaIndex} to {value}.");
_event.Invoke(DesignChanged.Type.Other, design, (metaIndex, false, value));
}
/// <summary> Change the application value of one of the meta flags. </summary>
public void ChangeApplyMeta(Design design, ActorState.MetaIndex metaIndex, bool value)
{
var change = metaIndex switch
{
ActorState.MetaIndex.Wetness => design.SetApplyWetness(value),
ActorState.MetaIndex.HatState => design.SetApplyHatVisible(value),
ActorState.MetaIndex.VisorState => design.SetApplyVisorToggle(value),
ActorState.MetaIndex.WeaponState => design.SetApplyWeaponVisible(value),
_ => throw new ArgumentOutOfRangeException(nameof(metaIndex), metaIndex, null),
};
if (!change)
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of {metaIndex} to {value}.");
_event.Invoke(DesignChanged.Type.Other, design, (metaIndex, true, value));
}
/// <summary> Apply an entire design based on its appliance rules piece by piece. </summary>
public void ApplyDesign(Design design, DesignBase other)
{
if (other.DoApplyEquip(EquipSlot.MainHand))
ChangeWeapon(design, EquipSlot.MainHand, other.DesignData.Item(EquipSlot.MainHand));
if (other.DoApplyEquip(EquipSlot.OffHand))
ChangeWeapon(design, EquipSlot.OffHand, other.DesignData.Item(EquipSlot.OffHand));
if (other.DoApplyStain(EquipSlot.MainHand))
ChangeStain(design, EquipSlot.MainHand, other.DesignData.Stain(EquipSlot.MainHand));
if (other.DoApplyStain(EquipSlot.OffHand))
ChangeStain(design, EquipSlot.OffHand, other.DesignData.Stain(EquipSlot.OffHand));
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
if (other.DoApplyEquip(slot))
ChangeEquip(design, slot, other.DesignData.Item(slot));
if (other.DoApplyStain(slot))
ChangeStain(design, slot, other.DesignData.Stain(slot));
}
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
if (other.DoApplyCustomize(index))
ChangeCustomize(design, index, other.DesignData.Customize[index]);
}
if (other.DoApplyHatVisible())
design.DesignData.SetHatVisible(other.DesignData.IsHatVisible());
if (other.DoApplyVisorToggle())
design.DesignData.SetVisor(other.DesignData.IsVisorToggled());
if (other.DoApplyWeaponVisible())
design.DesignData.SetWeaponVisible(other.DesignData.IsWeaponVisible());
if (other.DoApplyWetness())
design.DesignData.SetIsWet(other.DesignData.IsWet());
}
private void MigrateOldDesigns()
{
if (!File.Exists(_saveService.FileNames.MigrationDesignFile))

View file

@ -56,8 +56,11 @@ public sealed class AutomationChanged : EventWrapper<Action<AutomationChanged.Ty
public enum Priority
{
/// <seealso cref="Gui.Tabs.AutomationTab.SetSelector.OnAutomationChanged"/>
/// <seealso cref="Gui.Tabs.AutomationTab.SetSelector.OnAutomationChange"/>
SetSelector = 0,
/// <seealso cref="AutoDesignApplier.OnAutomationChange"/>
AutoDesignApplier,
}
public AutomationChanged()

View file

@ -40,6 +40,12 @@ public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Desi
/// <summary> An existing design had an existing tag renamed. Data is the old name of the tag, the new name of the tag, and the index it had before being resorted [(string, string, int)]. </summary>
ChangedTag,
/// <summary> An existing design had a new associated mod added. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
AddedMod,
/// <summary> An existing design had an existing associated mod removed. Data is the Mod and its Settings [(Mod, ModSettings)]. </summary>
RemovedMod,
/// <summary> An existing design had a customization changed. Data is the old value, the new value and the type [(CustomizeValue, CustomizeValue, CustomizeIndex)]. </summary>
Customize,
@ -61,7 +67,10 @@ public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Desi
/// <summary> An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyStain,
/// <summary> An existing design changed one of the meta flags. Data is null. </summary>
/// <summary> An existing design changed its write protection status. Data is the new value [bool]. </summary>
WriteProtection,
/// <summary> An existing design changed one of the meta flags. Data is the flag, whether it was about their applying and the new value [(MetaFlag, bool, bool)]. </summary>
Other,
}

View file

@ -40,6 +40,7 @@ public sealed class StateChanged : EventWrapper<Action<StateChanged.Type, StateC
Game,
Manual,
Fixed,
Ipc,
}
public enum Priority

View file

@ -32,7 +32,6 @@ public class Glamourer : IDalamudPlugin
{
_services = ServiceManager.CreateProvider(pluginInterface, Log);
Chat = _services.GetRequiredService<ChatService>();
_services.GetRequiredService<BackupService>(); // call backup service.
_services.GetRequiredService<GlamourerWindowSystem>(); // initialize ui.
_services.GetRequiredService<CommandService>(); // initialize commands.
_services.GetRequiredService<VisorService>();

View file

@ -16,6 +16,7 @@ public enum ColorId
DisabledAutoSet,
AutomationActorAvailable,
AutomationActorUnavailable,
HeaderButtons,
}
public static class Colors
@ -36,6 +37,7 @@ public static class Colors
ColorId.DisabledAutoSet => (0xFF808080, "Disabled Automation Set", "An automation set that is currently disabled." ),
ColorId.AutomationActorAvailable => (0xFFFFFFFF, "Automation Actor Available", "A character associated with the given automated design set is currently visible." ),
ColorId.AutomationActorUnavailable => (0xFF808080, "Automation Actor Unavailable", "No character associated with the given automated design set is currently visible." ),
ColorId.HeaderButtons => (0xFFFFF0C0, "Header Buttons", "The text and border color of buttons in the header, like the Incognito toggle." ),
_ => (0x00000000, string.Empty, string.Empty ),
// @formatter:on
};

View file

@ -121,17 +121,20 @@ public partial class CustomizationDrawer
PercentageInputInt();
ImGui.TextUnformatted(_set.Option(CustomizeIndex.LegacyTattoo));
if (_set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0)
ImGui.TextUnformatted("(Using Face 1)");
}
private void DrawMultiIcons()
{
var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark];
using var _ = ImRaii.Group();
using var group = ImRaii.Group();
var face = _set.DataByValue(CustomizeIndex.Face, _customize.Face, out _, _customize.Face) < 0 ? _set.Faces[0].Value : _customize.Face;
foreach (var (featureIdx, idx) in options.WithIndex())
{
using var id = SetId(featureIdx);
var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero;
var feature = _set.Data(featureIdx, 0, _customize.Face);
var feature = _set.Data(featureIdx, 0, face);
var icon = featureIdx == CustomizeIndex.LegacyTattoo
? _legacyTattoo ?? _service.AwaitedService.GetIcon(feature.IconId)
: _service.AwaitedService.GetIcon(feature.IconId);

View file

@ -1,15 +1,22 @@
using System.Numerics;
using System;
using System.Numerics;
using System.Xml.Linq;
using Dalamud.Interface;
using Glamourer.Events;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.ActorTab;
@ -19,6 +26,8 @@ public class ActorPanel
private readonly StateManager _stateManager;
private readonly CustomizationDrawer _customizationDrawer;
private readonly EquipmentDrawer _equipmentDrawer;
private readonly HumanModelList _humans;
private readonly IdentifierService _identification;
private ActorIdentifier _identifier;
private string _actorName = string.Empty;
@ -27,12 +36,14 @@ public class ActorPanel
private ActorState? _state;
public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer,
EquipmentDrawer equipmentDrawer)
EquipmentDrawer equipmentDrawer, HumanModelList humans, IdentifierService identification)
{
_selector = selector;
_stateManager = stateManager;
_customizationDrawer = customizationDrawer;
_equipmentDrawer = equipmentDrawer;
_humans = humans;
_identification = identification;
}
public void Draw()
@ -47,15 +58,16 @@ public class ActorPanel
private void DrawHeader()
{
var frameHeight = ImGui.GetFrameHeightWithSpacing();
var color = !_identifier.IsValid ? ImGui.GetColorU32(ImGuiCol.Text) : _data.Valid ? ColorId.ActorAvailable.Value() : ColorId.ActorUnavailable.Value();
var color = !_identifier.IsValid ? ImGui.GetColorU32(ImGuiCol.Text) :
_data.Valid ? ColorId.ActorAvailable.Value() : ColorId.ActorUnavailable.Value();
var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGuiUtil.DrawTextButton($"{_actorName}##playerHeader", new Vector2(-frameHeight, ImGui.GetFrameHeight()), buttonColor, color);
ImGui.SameLine();
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value())
.Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()))
using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.HeaderButtons.Value())
.Push(ImGuiCol.Border, ColorId.HeaderButtons.Value()))
{
if (ImGuiUtil.DrawDisabledButton(
$"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode",
@ -79,13 +91,9 @@ public class ActorPanel
return (_selector.IncognitoMode ? _identifier.Incognito(null) : _identifier.ToString(), Actor.Null);
}
private unsafe void DrawPanel()
private void DrawHumanPanel()
{
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
if (!child || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state))
return;
if (_customizationDrawer.Draw(_state.ModelData.Customize, false))
if (_customizationDrawer.Draw(_state!.ModelData.Customize, false))
_stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
@ -122,6 +130,79 @@ public class ActorPanel
}
}
private void DrawMonsterPanel()
{
var names = _identification.AwaitedService.ModelCharaNames(_state!.ModelData.ModelId);
var turnHuman = ImGui.Button("Turn Human");
ImGui.Separator();
using (var box = ImRaii.ListBox("##MonsterList",
new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetTextLineHeightWithSpacing())))
{
if (names.Count == 0)
ImGui.TextUnformatted("Unknown Monster");
else
ImGuiClip.ClippedDraw(names, p => ImGui.TextUnformatted($"{p.Name} ({p.Kind.ToName()} #{p.Id})"),
ImGui.GetTextLineHeightWithSpacing());
}
ImGui.Separator();
ImGui.TextUnformatted("Customization Data");
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var b in _state.ModelData.Customize.Data)
{
using (var g = ImRaii.Group())
{
ImGui.TextUnformatted($" {b:X2}");
ImGui.TextUnformatted($"{b,3}");
}
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize("XXX").X)
ImGui.NewLine();
}
if (ImGui.GetCursorPosX() != 0)
ImGui.NewLine();
}
ImGui.Separator();
ImGui.TextUnformatted("Equipment Data");
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var b in _state.ModelData.GetEquipmentBytes())
{
using (var g = ImRaii.Group())
{
ImGui.TextUnformatted($" {b:X2}");
ImGui.TextUnformatted($"{b,3}");
}
ImGui.SameLine();
if (ImGui.GetContentRegionAvail().X < ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize("XXX").X)
ImGui.NewLine();
}
if (ImGui.GetCursorPosX() != 0)
ImGui.NewLine();
}
if (turnHuman)
_stateManager.TurnHuman(_state, StateChanged.Source.Manual);
}
private unsafe void DrawPanel()
{
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
if (!child || !_selector.HasSelection || !_stateManager.GetOrCreate(_identifier, _actor, out _state))
return;
if (_humans.IsHuman(_state.ModelData.ModelId))
DrawHumanPanel();
else
DrawMonsterPanel();
}
private unsafe void RevertButton()
{

View file

@ -99,9 +99,10 @@ public class ActorSelector
_identifier = _objects.Player.GetIdentifier(_actors.AwaitedService);
ImGui.SameLine();
Actor targetActor = _targets.Target?.Address;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth,
"Select the current target, if it is in the list.", _objects.IsInGPose || !targetActor, true))
_identifier = targetActor.GetIdentifier(_actors.AwaitedService);
var (id, data) = _objects.TargetData;
var tt = data.Valid ? $"Select the current target {id} in the list." :
id.IsValid ? $"The target {id} is not in the list." : "No target selected.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth, tt, _objects.IsInGPose || !data.Valid, true))
_identifier = id;
}
}

View file

@ -68,8 +68,8 @@ public class SetPanel
ImGuiUtil.DrawTextButton(_selector.SelectionName, new Vector2(-frameHeight, ImGui.GetFrameHeight()), buttonColor);
ImGui.SameLine();
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value())
.Push(ImGuiCol.Border, ColorId.FolderExpanded.Value());
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HeaderButtons.Value())
.Push(ImGuiCol.Border, ColorId.HeaderButtons.Value());
if (ImGuiUtil.DrawDisabledButton(
$"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode",
new Vector2(frameHeight, ImGui.GetFrameHeight()), string.Empty, false, true))

View file

@ -46,12 +46,12 @@ public class SetSelector : IDisposable
_config = config;
_actors = actors;
_objects = objects;
_event.Subscribe(OnAutomationChanged, AutomationChanged.Priority.SetSelector);
_event.Subscribe(OnAutomationChange, AutomationChanged.Priority.SetSelector);
}
public void Dispose()
{
_event.Unsubscribe(OnAutomationChanged);
_event.Unsubscribe(OnAutomationChange);
}
public string SelectionName
@ -60,7 +60,7 @@ public class SetSelector : IDisposable
public string GetSetName(AutoDesignSet? set, int index)
=> set == null ? "No Selection" : IncognitoMode ? $"Auto Design Set #{index + 1}" : set.Name;
private void OnAutomationChanged(AutomationChanged.Type type, AutoDesignSet? set, object? data)
private void OnAutomationChange(AutomationChanged.Type type, AutoDesignSet? set, object? data)
{
switch (type)
{

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
@ -19,7 +20,9 @@ using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Unlocks;
using Glamourer.Utility;
using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
@ -54,6 +57,7 @@ public unsafe class DebugTab : ITab
private readonly DesignManager _designManager;
private readonly DesignFileSystem _designFileSystem;
private readonly AutoDesignManager _autoDesignManager;
private readonly DesignConverter _designConverter;
private readonly PenumbraChangedItemTooltip _penumbraTooltip;
@ -70,7 +74,7 @@ public unsafe class DebugTab : ITab
DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config,
PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface,
AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks,
ItemUnlockManager itemUnlocks)
ItemUnlockManager itemUnlocks, DesignConverter designConverter)
{
_changeCustomizeService = changeCustomizeService;
_visorService = visorService;
@ -95,6 +99,7 @@ public unsafe class DebugTab : ITab
_code = code;
_customizeUnlocks = customizeUnlocks;
_itemUnlocks = itemUnlocks;
_designConverter = designConverter;
}
public ReadOnlySpan<byte> Label
@ -817,6 +822,7 @@ public unsafe class DebugTab : ITab
DrawDesignManager();
DrawDesignTester();
DrawDesignConverter();
}
private void DrawDesignManager()
@ -927,6 +933,83 @@ public unsafe class DebugTab : ITab
}
}
private string _clipboardText = string.Empty;
private byte[] _clipboardData = Array.Empty<byte>();
private byte[] _dataUncompressed = Array.Empty<byte>();
private byte _version = 0;
private string _textUncompressed = string.Empty;
private JObject? _json = null;
private DesignBase? _tmpDesign = null;
private Exception? _clipboardProblem = null;
private void DrawDesignConverter()
{
using var tree = ImRaii.TreeNode("Design Converter");
if (!tree)
return;
if (ImGui.Button("Import Clipboard"))
{
_clipboardText = string.Empty;
_clipboardData = Array.Empty<byte>();
_dataUncompressed = Array.Empty<byte>();
_textUncompressed = string.Empty;
_json = null;
_tmpDesign = null;
_clipboardProblem = null;
try
{
_clipboardText = ImGui.GetClipboardText();
_clipboardData = Convert.FromBase64String(_clipboardText);
_version = _clipboardData.Decompress(out _dataUncompressed);
_textUncompressed = Encoding.UTF8.GetString(_dataUncompressed);
_json = JObject.Parse(_textUncompressed);
_tmpDesign = _designConverter.FromBase64(_clipboardText, true, true);
}
catch (Exception ex)
{
_clipboardProblem = ex;
}
}
if (_clipboardText.Length > 0)
{
using var f = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtil.TextWrapped(_clipboardText);
}
if (_clipboardData.Length > 0)
{
using var f = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtil.TextWrapped(string.Join(" ", _clipboardData.Select(b => b.ToString("X2"))));
}
if (_dataUncompressed.Length > 0)
{
using var f = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtil.TextWrapped(string.Join(" ", _dataUncompressed.Select(b => b.ToString("X2"))));
}
if (_textUncompressed.Length > 0)
{
using var f = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtil.TextWrapped(_textUncompressed);
}
if (_json != null)
ImGui.TextUnformatted("JSON Parsing Successful!");
if (_tmpDesign != null)
DrawDesign(_tmpDesign);
if (_clipboardProblem != null)
{
using var f = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtil.TextWrapped(_clipboardProblem.ToString());
}
}
public void DrawState(ActorData data, ActorState state)
{
using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
@ -955,20 +1038,20 @@ public unsafe class DebugTab : ITab
return $"{item.Name} ({item.ModelId.Value}{(item.WeaponType != 0 ? $"-{item.WeaponType.Value}" : string.Empty)}-{item.Variant})";
}
PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaFlag.ModelId]);
PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaIndex.ModelId]);
ImGui.TableNextRow();
PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaFlag.Wetness]);
PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaIndex.Wetness]);
ImGui.TableNextRow();
if (state.BaseData.ModelId == 0 && state.ModelData.ModelId == 0)
{
PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaFlag.HatState]);
PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaIndex.HatState]);
ImGui.TableNextRow();
PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(),
state[ActorState.MetaFlag.VisorState]);
state[ActorState.MetaIndex.VisorState]);
ImGui.TableNextRow();
PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(),
state[ActorState.MetaFlag.WeaponState]);
state[ActorState.MetaIndex.WeaponState]);
ImGui.TableNextRow();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
@ -1053,33 +1136,36 @@ public unsafe class DebugTab : ITab
}
}
private void DrawDesign(Design design)
private void DrawDesign(DesignBase design)
{
using var table = ImRaii.Table("##equip", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (design is Design d)
{
ImGuiUtil.DrawTableColumn("Name");
ImGuiUtil.DrawTableColumn(design.Name);
ImGuiUtil.DrawTableColumn($"({design.Index})");
ImGuiUtil.DrawTableColumn(d.Name);
ImGuiUtil.DrawTableColumn($"({d.Index})");
ImGui.TableNextColumn();
ImGui.TextUnformatted("Description (Hover)");
ImGuiUtil.HoverTooltip(design.Description);
ImGuiUtil.HoverTooltip(d.Description);
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Identifier");
ImGuiUtil.DrawTableColumn(design.Identifier.ToString());
ImGuiUtil.DrawTableColumn(d.Identifier.ToString());
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Design File System Path");
ImGuiUtil.DrawTableColumn(_designFileSystem.FindLeaf(design, out var leaf) ? leaf.FullName() : "No Path Known");
ImGuiUtil.DrawTableColumn(_designFileSystem.FindLeaf(d, out var leaf) ? leaf.FullName() : "No Path Known");
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Creation");
ImGuiUtil.DrawTableColumn(design.CreationDate.ToString());
ImGuiUtil.DrawTableColumn(d.CreationDate.ToString());
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Update");
ImGuiUtil.DrawTableColumn(design.LastEdit.ToString());
ImGuiUtil.DrawTableColumn(d.LastEdit.ToString());
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn("Tags");
ImGuiUtil.DrawTableColumn(string.Join(", ", design.Tags));
ImGuiUtil.DrawTableColumn(string.Join(", ", d.Tags));
ImGui.TableNextRow();
}
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
@ -1174,9 +1260,7 @@ public unsafe class DebugTab : ITab
foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key)))
{
using var t = ImRaii.TreeNode(identifier.ToString());
if (!t)
return;
if (t)
DrawState(ActorData.Invalid, state);
}
}

View file

@ -0,0 +1,173 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Designs;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
namespace Glamourer.Gui.Tabs.DesignTab;
public class DesignDetailTab
{
private readonly SaveService _saveService;
private readonly DesignFileSystemSelector _selector;
private readonly DesignFileSystem _fileSystem;
private readonly DesignManager _manager;
private readonly TagButtons _tagButtons = new();
private string? _newPath;
private string? _newDescription;
private string? _newName;
private bool _editDescriptionMode;
public DesignDetailTab(SaveService saveService, DesignFileSystemSelector selector, DesignManager manager, DesignFileSystem fileSystem)
{
_saveService = saveService;
_selector = selector;
_manager = manager;
_fileSystem = fileSystem;
}
public void Draw()
{
if (!ImGui.CollapsingHeader("Design Details"))
return;
DrawDesignInfoTable();
DrawDescription();
ImGui.NewLine();
}
private void DrawDesignInfoTable()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
using var table = ImRaii.Table("Details", 2);
if (!table)
return;
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X);
ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch);
ImGuiUtil.DrawFrameColumn("Design Name");
ImGui.TableNextColumn();
var width = new Vector2(ImGui.GetContentRegionAvail().X, 0);
var name = _newName ?? _selector.Selected!.Name;
ImGui.SetNextItemWidth(width.X);
if (ImGui.InputText("##Name", ref name, 128))
_newName = name;
if (ImGui.IsItemDeactivatedAfterEdit())
{
_manager.Rename(_selector.Selected!, name);
_newName = null;
}
var identifier = _selector.Selected!.Identifier.ToString();
ImGuiUtil.DrawFrameColumn("Unique Identifier");
ImGui.TableNextColumn();
var fileName = _saveService.FileNames.DesignFile(_selector.Selected!);
using (var mono = ImRaii.PushFont(UiBuilder.MonoFont))
{
if (ImGui.Button(identifier, width))
try
{
Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true });
}
catch (Exception ex)
{
Glamourer.Chat.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", "Failure",
NotificationType.Warning);
}
}
ImGuiUtil.HoverTooltip($"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.");
ImGuiUtil.DrawFrameColumn("Full Selector Path");
ImGui.TableNextColumn();
var path = _newPath ?? _selector.SelectedLeaf!.FullName();
ImGui.SetNextItemWidth(width.X);
if (ImGui.InputText("##Path", ref path, 1024))
_newPath = path;
if (ImGui.IsItemDeactivatedAfterEdit())
try
{
_fileSystem.RenameAndMove(_selector.SelectedLeaf!, path);
_newPath = null;
}
catch (Exception ex)
{
Glamourer.Chat.NotificationMessage(ex, ex.Message, "Could not rename or move design", "Error", NotificationType.Error);
}
ImGuiUtil.DrawFrameColumn("Creation Date");
ImGui.TableNextColumn();
ImGuiUtil.DrawTextButton(_selector.Selected!.CreationDate.LocalDateTime.ToString("F"), width, 0);
ImGuiUtil.DrawFrameColumn("Last Update Date");
ImGui.TableNextColumn();
ImGuiUtil.DrawTextButton(_selector.Selected!.LastEdit.LocalDateTime.ToString("F"), width, 0);
ImGuiUtil.DrawFrameColumn("Tags");
ImGui.TableNextColumn();
DrawTags();
}
private void DrawTags()
{
var idx = _tagButtons.Draw(string.Empty, string.Empty, _selector.Selected!.Tags, out var editedTag);
if (idx < 0)
return;
if (idx < _selector.Selected!.Tags.Length)
{
if (editedTag.Length == 0)
_manager.RemoveTag(_selector.Selected!, idx);
else
_manager.RenameTag(_selector.Selected!, idx, editedTag);
}
else
{
_manager.AddTag(_selector.Selected!, editedTag);
}
}
private void DrawDescription()
{
var desc = _selector.Selected!.Description;
var size = new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeightWithSpacing());
if (!_editDescriptionMode)
{
using (var textBox = ImRaii.ListBox("##desc", size))
{
ImGuiUtil.TextWrapped(desc);
}
if (ImGui.Button("Edit Description"))
_editDescriptionMode = true;
}
else
{
var edit = _newDescription ?? desc;
if (ImGui.InputTextMultiline("##desc", ref edit, (uint)Math.Max(2000, 4 * edit.Length), size))
_newDescription = edit;
if (ImGui.IsItemDeactivatedAfterEdit())
{
_manager.ChangeDescription(_selector.Selected!, edit);
_newDescription = null;
}
if (ImGui.Button("Stop Editing"))
_editDescriptionMode = false;
}
}
}

View file

@ -1,6 +1,9 @@
using System.Numerics;
using System;
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Designs;
using Glamourer.Events;
using ImGuiNET;
@ -16,6 +19,11 @@ public sealed class DesignFileSystemSelector : FileSystemSelector<Design, Design
private readonly DesignManager _designManager;
private readonly DesignChanged _event;
private readonly Configuration _config;
private readonly DesignConverter _converter;
private string? _clipboardText;
private Design? _cloneDesign = null;
private string _newName = string.Empty;
public bool IncognitoMode
{
@ -27,24 +35,37 @@ public sealed class DesignFileSystemSelector : FileSystemSelector<Design, Design
}
}
public new DesignFileSystem.Leaf? SelectedLeaf
=> base.SelectedLeaf;
public struct DesignState
{ }
public DesignFileSystemSelector(DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState, DesignChanged @event,
Configuration config)
Configuration config, DesignConverter converter)
: base(fileSystem, keyState)
{
_designManager = designManager;
_event = @event;
_config = config;
_converter = converter;
_event.Subscribe(OnDesignChange, DesignChanged.Priority.DesignFileSystemSelector);
AddButton(NewDesignButton, 0);
AddButton(ImportDesignButton, 10);
AddButton(CloneDesignButton, 20);
AddButton(DeleteButton, 1000);
}
protected override void DrawPopups()
{
DrawNewDesignPopup();
}
protected override void DrawLeafName(FileSystem<Design>.Leaf leaf, in DesignState state, bool selected)
{
var flag = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
var name = IncognitoMode ? leaf.Value.Incognito : leaf.Name;
var name = IncognitoMode ? leaf.Value.Incognito : leaf.Value.Name.Text;
using var _ = ImRaii.TreeNode(name, flag);
}
@ -78,11 +99,51 @@ public sealed class DesignFileSystemSelector : FileSystemSelector<Design, Design
case DesignChanged.Type.AddedTag:
case DesignChanged.Type.ChangedTag:
case DesignChanged.Type.RemovedTag:
case DesignChanged.Type.AddedMod:
case DesignChanged.Type.RemovedMod:
case DesignChanged.Type.Created:
case DesignChanged.Type.Deleted:
SetFilterDirty();
break;
}
}
private void NewDesignButton(Vector2 size)
{
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new design with default configuration.", false,
true))
ImGui.OpenPopup("##NewDesign");
}
private void ImportDesignButton(Vector2 size)
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, "Try to import a design from your clipboard.", false,
true))
return;
try
{
_clipboardText = ImGui.GetClipboardText();
ImGui.OpenPopup("##NewDesign");
}
catch
{
Glamourer.Chat.NotificationMessage("Could not import data from clipboard.", "Failure", NotificationType.Error);
}
}
private void CloneDesignButton(Vector2 size)
{
var tt = SelectedLeaf == null
? "No design selected."
: "Clone the currently selected design to a duplicate";
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clone.ToIconString(), size, tt, SelectedLeaf == null, true))
return;
_cloneDesign = Selected!;
ImGui.OpenPopup("##NewDesign");
}
private void DeleteButton(Vector2 size)
{
var keys = _config.DeleteDesignModifier.IsActive();
@ -91,10 +152,40 @@ public sealed class DesignFileSystemSelector : FileSystemSelector<Design, Design
: "Delete the currently selected design entirely from your drive.\n"
+ "This can not be undone.";
if (!keys)
tt += $"\nHold {_config.DeleteDesignModifier} while clicking to delete the mod.";
tt += $"\nHold {_config.DeleteDesignModifier} while clicking to delete the design.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true)
&& Selected != null)
_designManager.Delete(Selected);
}
private void DrawNewDesignPopup()
{
if (!ImGuiUtil.OpenNameField("##NewDesign", ref _newName))
return;
if (_clipboardText != null)
{
var design = _converter.FromBase64(_clipboardText, true, true);
if (design is Design d)
_designManager.CreateClone(d, _newName);
else if (design != null)
_designManager.CreateClone(design, _newName);
else
Glamourer.Chat.NotificationMessage("Could not create a design, clipboard did not contain valid design data.", "Failure",
NotificationType.Error);
_clipboardText = null;
}
else if (_cloneDesign != null)
{
_designManager.CreateClone(_cloneDesign, _newName);
_cloneDesign = null;
}
else
{
_designManager.CreateEmpty(_newName);
}
_newName = string.Empty;
}
}

View file

@ -1,10 +1,19 @@
using System.Numerics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
@ -20,9 +29,14 @@ public class DesignPanel
private readonly CustomizationDrawer _customizationDrawer;
private readonly StateManager _state;
private readonly EquipmentDrawer _equipmentDrawer;
private readonly CustomizationService _customizationService;
private readonly ModAssociationsTab _modAssociations;
private readonly DesignDetailTab _designDetails;
private readonly DesignConverter _converter;
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects,
StateManager state, EquipmentDrawer equipmentDrawer)
StateManager state, EquipmentDrawer equipmentDrawer, CustomizationService customizationService, PenumbraService penumbra,
ModAssociationsTab modAssociations, DesignDetailTab designDetails, DesignConverter converter)
{
_selector = selector;
_customizationDrawer = customizationDrawer;
@ -30,32 +44,229 @@ public class DesignPanel
_objects = objects;
_state = state;
_equipmentDrawer = equipmentDrawer;
_customizationService = customizationService;
_modAssociations = modAssociations;
_designDetails = designDetails;
_converter = converter;
}
private void DrawHeader()
{
var selection = _selector.Selected;
var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
var frameHeight = ImGui.GetFrameHeightWithSpacing();
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGuiUtil.DrawTextButton(SelectionName, new Vector2(-frameHeight, ImGui.GetFrameHeight()), buttonColor);
ImGuiUtil.DrawTextButton(SelectionName, new Vector2(selection != null ? -2 * frameHeight : -frameHeight, ImGui.GetFrameHeight()),
buttonColor);
ImGui.SameLine();
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value())
.Push(ImGuiCol.Border, ColorId.FolderExpanded.Value());
using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.HeaderButtons.Value())
.Push(ImGuiCol.Text, ColorId.HeaderButtons.Value());
var hoverText = string.Empty;
if (selection != null)
{
if (ImGuiUtil.DrawDisabledButton(
$"{(selection.WriteProtected() ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock).ToIconString()}###Locked",
new Vector2(frameHeight, ImGui.GetFrameHeight()), string.Empty, false, true))
_manager.SetWriteProtection(selection, !selection.WriteProtected());
if (ImGui.IsItemHovered())
hoverText = selection.WriteProtected() ? "Make this design editable." : "Write-protect this design.";
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(
$"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode",
new Vector2(frameHeight, ImGui.GetFrameHeight()), string.Empty, false, true))
_selector.IncognitoMode = !_selector.IncognitoMode;
var hovered = ImGui.IsItemHovered();
color.Pop(2);
if (hovered)
ImGui.SetTooltip(_selector.IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on.");
if (ImGui.IsItemHovered())
hoverText = _selector.IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on.";
if (hoverText.Length > 0)
ImGui.SetTooltip(hoverText);
}
private string SelectionName
=> _selector.Selected == null ? "No Selection" : _selector.IncognitoMode ? _selector.Selected.Incognito : _selector.Selected.Name.Text;
private void DrawMetaData()
{
if (!ImGui.CollapsingHeader("MetaData"))
return;
using (var group1 = ImRaii.Group())
{
var apply = _selector.Selected!.DesignData.IsHatVisible();
if (ImGui.Checkbox("Hat Visible", ref apply))
_manager.ChangeMeta(_selector.Selected, ActorState.MetaIndex.HatState, apply);
apply = _selector.Selected.DesignData.IsWeaponVisible();
if (ImGui.Checkbox("Weapon Visible", ref apply))
_manager.ChangeMeta(_selector.Selected, ActorState.MetaIndex.WeaponState, apply);
}
ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2);
using (var group2 = ImRaii.Group())
{
var apply = _selector.Selected.DesignData.IsVisorToggled();
if (ImGui.Checkbox("Visor Toggled", ref apply))
_manager.ChangeMeta(_selector.Selected, ActorState.MetaIndex.VisorState, apply);
apply = _selector.Selected.DesignData.IsWet();
if (ImGui.Checkbox("Force Wetness", ref apply))
_manager.ChangeMeta(_selector.Selected, ActorState.MetaIndex.Wetness, apply);
}
}
private void DrawEquipment()
{
if (!ImGui.CollapsingHeader("Equipment"))
return;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var stain = _selector.Selected!.DesignData.Stain(slot);
if (_equipmentDrawer.DrawStain(stain, slot, out var newStain))
_manager.ChangeStain(_selector.Selected!, slot, newStain.RowIndex);
ImGui.SameLine();
var armor = _selector.Selected!.DesignData.Item(slot);
if (_equipmentDrawer.DrawArmor(armor, slot, out var newArmor, _selector.Selected!.DesignData.Customize.Gender,
_selector.Selected!.DesignData.Customize.Race))
_manager.ChangeEquip(_selector.Selected!, slot, newArmor);
}
var mhStain = _selector.Selected!.DesignData.Stain(EquipSlot.MainHand);
if (_equipmentDrawer.DrawStain(mhStain, EquipSlot.MainHand, out var newMhStain))
_manager.ChangeStain(_selector.Selected!, EquipSlot.MainHand, newMhStain.RowIndex);
ImGui.SameLine();
var mh = _selector.Selected!.DesignData.Item(EquipSlot.MainHand);
if (_equipmentDrawer.DrawMainhand(mh, true, out var newMh))
_manager.ChangeWeapon(_selector.Selected!, EquipSlot.MainHand, newMh);
if (newMh.Type.Offhand() is not FullEquipType.Unknown)
{
var ohStain = _selector.Selected!.DesignData.Stain(EquipSlot.OffHand);
if (_equipmentDrawer.DrawStain(ohStain, EquipSlot.OffHand, out var newOhStain))
_manager.ChangeStain(_selector.Selected!, EquipSlot.OffHand, newOhStain.RowIndex);
ImGui.SameLine();
var oh = _selector.Selected!.DesignData.Item(EquipSlot.OffHand);
if (_equipmentDrawer.DrawMainhand(oh, false, out var newOh))
_manager.ChangeWeapon(_selector.Selected!, EquipSlot.OffHand, newOh);
}
}
private void DrawCustomize()
{
if (ImGui.CollapsingHeader("Customization"))
_customizationDrawer.Draw(_selector.Selected!.DesignData.Customize, _selector.Selected!.WriteProtected());
}
private void DrawApplicationRules()
{
if (!ImGui.CollapsingHeader("Application Rules"))
return;
using (var group1 = ImRaii.Group())
{
var set = _customizationService.AwaitedService.GetList(_selector.Selected!.DesignData.Customize.Clan,
_selector.Selected!.DesignData.Customize.Gender);
var all = CustomizationExtensions.All.Where(set.IsAvailable).Select(c => c.ToFlag()).Aggregate((a, b) => a | b);
var flags = (_selector.Selected!.ApplyCustomize & all) == 0 ? 0 : (_selector.Selected!.ApplyCustomize & all) == all ? 3 : 1;
if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3))
{
var newFlags = flags == 3;
foreach (var index in CustomizationExtensions.All)
_manager.ChangeApplyCustomize(_selector.Selected!, index, newFlags);
}
var applyClan = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Clan);
if (ImGui.Checkbox($"Apply {CustomizeIndex.Clan.ToDefaultName()}", ref applyClan))
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, applyClan);
var applyGender = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Gender);
if (ImGui.Checkbox($"Apply {CustomizeIndex.Gender.ToDefaultName()}", ref applyGender))
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, applyGender);
foreach (var index in CustomizationExtensions.All.Where(set.IsAvailable))
{
var apply = _selector.Selected!.DoApplyCustomize(index);
if (ImGui.Checkbox($"Apply {index.ToDefaultName()}", ref apply))
_manager.ChangeApplyCustomize(_selector.Selected!, index, apply);
}
}
ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2);
using (var group2 = ImRaii.Group())
{
void ApplyEquip(string label, EquipFlag all, bool stain, IEnumerable<EquipSlot> slots)
{
var flags = (uint)(all & _selector.Selected!.ApplyEquip);
var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)all);
if (stain)
foreach (var slot in slots)
{
var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToStainFlag()) : _selector.Selected!.DoApplyStain(slot);
if (ImGui.Checkbox($"Apply {slot.ToName()} Dye", ref apply) || bigChange)
_manager.ChangeApplyStain(_selector.Selected!, slot, apply);
}
else
foreach (var slot in slots)
{
var apply = bigChange ? ((EquipFlag)flags).HasFlag(slot.ToFlag()) : _selector.Selected!.DoApplyEquip(slot);
if (ImGui.Checkbox($"Apply {slot.ToName()}", ref apply) || bigChange)
_manager.ChangeApplyEquip(_selector.Selected!, slot, apply);
}
}
ApplyEquip("Weapons", AutoDesign.WeaponFlags, false, new[]
{
EquipSlot.MainHand,
EquipSlot.OffHand,
});
ImGui.NewLine();
ApplyEquip("Armor", AutoDesign.ArmorFlags, false, EquipSlotExtensions.EquipmentSlots);
ImGui.NewLine();
ApplyEquip("Accessories", AutoDesign.AccessoryFlags, false, EquipSlotExtensions.AccessorySlots);
ImGui.NewLine();
ApplyEquip("Dyes", AutoDesign.StainFlags, true,
EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.MainHand).Prepend(EquipSlot.OffHand));
ImGui.NewLine();
const uint all = 0x0Fu;
var flags = (_selector.Selected!.DoApplyHatVisible() ? 0x01u : 0x00)
| (_selector.Selected!.DoApplyVisorToggle() ? 0x02u : 0x00)
| (_selector.Selected!.DoApplyWeaponVisible() ? 0x04u : 0x00)
| (_selector.Selected!.DoApplyWetness() ? 0x08u : 0x00);
var bigChange = ImGui.CheckboxFlags("Apply All Meta Changes", ref flags, all);
var apply = bigChange ? (flags & 0x01) == 0x01 : _selector.Selected!.DoApplyHatVisible();
if (ImGui.Checkbox("Apply Hat Visibility", ref apply) || bigChange)
_manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.HatState, apply);
apply = bigChange ? (flags & 0x02) == 0x02 : _selector.Selected!.DoApplyVisorToggle();
if (ImGui.Checkbox("Apply Visor State", ref apply) || bigChange)
_manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.VisorState, apply);
apply = bigChange ? (flags & 0x04) == 0x04 : _selector.Selected!.DoApplyWeaponVisible();
if (ImGui.Checkbox("Apply Weapon Visibility", ref apply) || bigChange)
_manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.WeaponState, apply);
apply = bigChange ? (flags & 0x08) == 0x08 : _selector.Selected!.DoApplyWetness();
if (ImGui.Checkbox("Apply Wetness", ref apply) || bigChange)
_manager.ChangeApplyMeta(_selector.Selected!, ActorState.MetaIndex.Wetness, apply);
}
}
public void Draw()
{
using var group = ImRaii.Group();
@ -66,47 +277,86 @@ public class DesignPanel
if (!child || design == null)
return;
if (ImGui.Button("TEST"))
DrawButtonRow();
DrawMetaData();
DrawCustomize();
DrawEquipment();
_designDetails.Draw();
DrawApplicationRules();
_modAssociations.Draw();
}
private void DrawButtonRow()
{
DrawSetFromClipboard();
ImGui.SameLine();
DrawExportToClipboard();
ImGui.SameLine();
DrawApplyToSelf();
ImGui.SameLine();
DrawApplyToTarget();
}
private void DrawSetFromClipboard()
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Try to apply a design from your clipboard.", _selector.Selected!.WriteProtected(), true))
return;
try
{
var text = ImGui.GetClipboardText();
var design = _converter.FromBase64(text, true, true) ?? throw new Exception("The clipboard did not contain valid data.");
_manager.ApplyDesign(_selector.Selected!, design);
}
catch (Exception ex)
{
Glamourer.Chat.NotificationMessage(ex, $"Could not apply clipboard to {_selector.Selected!.Name}.",
$"Could not apply clipboard to design {_selector.Selected!.Identifier}", "Failure", NotificationType.Error);
}
}
private void DrawExportToClipboard()
{
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Copy.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Copy the current design to your clipboard.", false, true))
return;
try
{
var text = _converter.ShareBase64(_selector.Selected!);
ImGui.SetClipboardText(text);
}
catch (Exception ex)
{
Glamourer.Chat.NotificationMessage(ex, $"Could not copy {_selector.Selected!.Name} data to clipboard.",
$"Could not copy data from design {_selector.Selected!.Identifier} to clipboard", "Failure", NotificationType.Error);
}
}
private void DrawApplyToSelf()
{
var (id, data) = _objects.PlayerData;
if (!ImGuiUtil.DrawDisabledButton("Apply to Yourself", Vector2.Zero, "Apply the current design with its settings to your character.",
!data.Valid))
return;
if (data.Valid && _state.GetOrCreate(id, data.Objects[0], out var state))
_state.ApplyDesign(design, state);
if (_state.GetOrCreate(id, data.Objects[0], out var state))
_state.ApplyDesign(_selector.Selected!, state);
}
_customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected());
foreach (var slot in EquipSlotExtensions.EqdpSlots)
private void DrawApplyToTarget()
{
var stain = design.DesignData.Stain(slot);
if (_equipmentDrawer.DrawStain(stain, slot, out var newStain))
_manager.ChangeStain(design, slot, newStain.RowIndex);
var (id, data) = _objects.TargetData;
var tt = id.IsValid
? data.Valid
? "Apply the current design with its settings to your current target."
: "The current target can not be manipulated."
: "No valid target selected.";
if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, !data.Valid))
return;
ImGui.SameLine();
var armor = design.DesignData.Item(slot);
if (_equipmentDrawer.DrawArmor(armor, slot, out var newArmor, design.DesignData.Customize.Gender, design.DesignData.Customize.Race))
_manager.ChangeEquip(design, slot, newArmor);
}
var mhStain = design.DesignData.Stain(EquipSlot.MainHand);
if (_equipmentDrawer.DrawStain(mhStain, EquipSlot.MainHand, out var newMhStain))
_manager.ChangeStain(design, EquipSlot.MainHand, newMhStain.RowIndex);
ImGui.SameLine();
var mh = design.DesignData.Item(EquipSlot.MainHand);
if (_equipmentDrawer.DrawMainhand(mh, true, out var newMh))
_manager.ChangeWeapon(design, EquipSlot.MainHand, newMh);
if (newMh.Type.Offhand() is not FullEquipType.Unknown)
{
var ohStain = design.DesignData.Stain(EquipSlot.OffHand);
if (_equipmentDrawer.DrawStain(ohStain, EquipSlot.OffHand, out var newOhStain))
_manager.ChangeStain(design, EquipSlot.OffHand, newOhStain.RowIndex);
ImGui.SameLine();
var oh = design.DesignData.Item(EquipSlot.OffHand);
if (_equipmentDrawer.DrawMainhand(oh, false, out var newOh))
_manager.ChangeWeapon(design, EquipSlot.OffHand, newOh);
}
if (_state.GetOrCreate(id, data.Objects[0], out var state))
_state.ApplyDesign(_selector.Selected!, state);
}
}

View file

@ -0,0 +1,144 @@
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Utility;
using Glamourer.Designs;
using Glamourer.Interop.Penumbra;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
namespace Glamourer.Gui.Tabs.DesignTab;
public class ModAssociationsTab
{
private readonly PenumbraService _penumbra;
private readonly DesignFileSystemSelector _selector;
private readonly DesignManager _manager;
private readonly ModCombo _modCombo;
public ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelector selector, DesignManager manager)
{
_penumbra = penumbra;
_selector = selector;
_manager = manager;
_modCombo = new ModCombo(penumbra);
}
public void Draw()
{
if (!ImGui.CollapsingHeader("Mod Associations"))
return;
DrawApplyAllButton();
DrawTable();
}
private void DrawApplyAllButton()
{
var current = _penumbra.CurrentCollection;
if (!ImGuiUtil.DrawDisabledButton($"Try Applying All Associated Mods to {current}##applyAll",
new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, current is "<Unavailable>"))
return;
foreach (var (mod, settings) in _selector.Selected!.AssociatedMods)
_penumbra.SetMod(mod, settings);
}
private void DrawTable()
{
using var table = ImRaii.Table("Mods", 6, ImGuiTableFlags.RowBg);
if (!table)
return;
ImGui.TableSetupColumn("##Delete", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight());
ImGui.TableSetupColumn("Mod Name", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Directory Name", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("State").X);
ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Priority").X);
ImGui.TableSetupColumn("##Options", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Try Applyingm").X);
ImGui.TableHeadersRow();
Mod? removedMod = null;
foreach (var ((mod, settings), idx) in _selector.Selected!.AssociatedMods.WithIndex())
{
using var id = ImRaii.PushId(idx);
DrawAssociatedModRow(mod, settings, out removedMod);
}
DrawNewModRow();
if (removedMod.HasValue)
_manager.RemoveMod(_selector.Selected!, removedMod.Value);
}
private void DrawAssociatedModRow(Mod mod, ModSettings settings, out Mod? removedMod)
{
removedMod = null;
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), new Vector2(ImGui.GetFrameHeight()),
"Delete this mod from associations", false, true))
removedMod = mod;
ImGuiUtil.DrawTableColumn(mod.Name);
ImGuiUtil.DrawTableColumn(mod.DirectoryName);
ImGui.TableNextColumn();
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGuiUtil.DrawTextButton((settings.Enabled ? FontAwesomeIcon.Check : FontAwesomeIcon.Cross).ToIconString(),
new Vector2(ImGui.GetContentRegionAvail().X, 0), 0);
}
ImGui.TableNextColumn();
ImGuiUtil.RightAlign(settings.Priority.ToString());
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton("Try Applying", new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty,
!_penumbra.Available))
{
var text = _penumbra.SetMod(mod, settings);
if (text.Length > 0)
Glamourer.Chat.NotificationMessage(text, "Failure", NotificationType.Warning);
}
DrawAssociatedModTooltip(settings);
}
private static void DrawAssociatedModTooltip(ModSettings settings)
{
if (settings is not { Enabled: true, Settings.Count: > 0 } || !ImGui.IsItemHovered())
return;
using var t = ImRaii.Tooltip();
ImGui.TextUnformatted("This will also try to apply the following settings to the current collection:");
ImGui.NewLine();
using (var _ = ImRaii.Group())
{
ModCombo.DrawSettingsLeft(settings);
}
ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2);
using (var _ = ImRaii.Group())
{
ModCombo.DrawSettingsRight(settings);
}
}
private void DrawNewModRow()
{
var currentName = _modCombo.CurrentSelection.Mod.Name;
ImGui.TableNextColumn();
var tt = currentName.IsNullOrEmpty()
? "Please select a mod first."
: _selector.Selected!.AssociatedMods.ContainsKey(_modCombo.CurrentSelection.Mod)
? "The design already contains an association with the selected mod."
: string.Empty;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), new Vector2(ImGui.GetFrameHeight()), tt, tt.Length > 0,
true))
_manager.AddMod(_selector.Selected!, _modCombo.CurrentSelection.Mod, _modCombo.CurrentSelection.Settings);
ImGui.TableNextColumn();
_modCombo.Draw("##new", currentName.IsNullOrEmpty() ? "Select new Mod..." : currentName, string.Empty,
200 * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight());
}
}

View file

@ -0,0 +1,85 @@
using System;
using System.Numerics;
using Dalamud.Interface;
using Glamourer.Interop.Penumbra;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Widgets;
namespace Glamourer.Gui.Tabs.DesignTab;
public sealed class ModCombo : FilterComboCache<(Mod Mod, ModSettings Settings)>
{
public ModCombo(PenumbraService penumbra)
: base(penumbra.GetMods)
{ }
protected override string ToString((Mod Mod, ModSettings Settings) obj)
=> obj.Mod.Name;
protected override bool IsVisible(int globalIndex, LowerString filter)
=> filter.IsContained(Items[globalIndex].Mod.Name) || filter.IsContained(Items[globalIndex].Mod.DirectoryName);
protected override bool DrawSelectable(int globalIdx, bool selected)
{
using var id = ImRaii.PushId(globalIdx);
var (mod, settings) = Items[globalIdx];
bool ret;
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !settings.Enabled))
{
ret = ImGui.Selectable(mod.Name, selected);
}
if (ImGui.IsItemHovered())
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 2 * ImGuiHelpers.GlobalScale);
using var tt = ImRaii.Tooltip();
var namesDifferent = mod.Name != mod.DirectoryName;
ImGui.Dummy(new Vector2(300 * ImGuiHelpers.GlobalScale, 0));
using (var group = ImRaii.Group())
{
if (namesDifferent)
ImGui.TextUnformatted("Directory Name");
ImGui.TextUnformatted("Enabled");
ImGui.TextUnformatted("Priority");
DrawSettingsLeft(settings);
}
ImGui.SameLine(Math.Max(ImGui.GetItemRectSize().X + 3 * ImGui.GetStyle().ItemSpacing.X, 150 * ImGuiHelpers.GlobalScale));
using (var group = ImRaii.Group())
{
if (namesDifferent)
ImGui.TextUnformatted(mod.DirectoryName);
ImGui.TextUnformatted(settings.Enabled.ToString());
ImGui.TextUnformatted(settings.Priority.ToString());
DrawSettingsRight(settings);
}
}
return ret;
}
public static void DrawSettingsLeft(ModSettings settings)
{
foreach (var setting in settings.Settings)
{
ImGui.TextUnformatted(setting.Key);
for (var i = 1; i < setting.Value.Count; ++i)
ImGui.NewLine();
}
}
public static void DrawSettingsRight(ModSettings settings)
{
foreach (var setting in settings.Settings)
{
if (setting.Value.Count == 0)
ImGui.TextUnformatted("<None Enabled>");
else
foreach (var option in setting.Value)
ImGui.TextUnformatted(option);
}
}
}

View file

@ -42,7 +42,6 @@ public class UnlockOverview
if (ImGui.Selectable(type.ToName(), _selected1 == type))
{
ClearIcons(_selected1);
_selected1 = type;
_selected2 = SubRace.Unknown;
_selected3 = Gender.Unknown;
@ -59,7 +58,6 @@ public class UnlockOverview
if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint",
_selected2 == clan && _selected3 == gender))
{
ClearIcons(_selected1);
_selected1 = FullEquipType.Unknown;
_selected2 = clan;
_selected3 = gender;
@ -68,15 +66,6 @@ public class UnlockOverview
}
}
private void ClearIcons(FullEquipType type)
{
if (!_items.ItemService.AwaitedService.TryGetValue(type, out var items))
return;
foreach (var item in items)
_customizations.AwaitedService.RemoveIcon(item.IconId);
}
public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks,
CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureCache textureCache)
{

View file

@ -38,7 +38,12 @@ public unsafe class MetaService : IDisposable
if (!actor.IsCharacter)
return;
_hideHatGearHook.Original(&actor.AsCharacter->DrawData, 0, (byte)(value ? 1 : 0));
// The function seems to not do anything if the head is 0, sometimes?
var old = actor.AsCharacter->DrawData.Head.Id;
if (old == 0)
actor.AsCharacter->DrawData.Head.Id = 1;
_hideHatGearHook.Original(&actor.AsCharacter->DrawData, 0, (byte)(value ? 0 : 1));
actor.AsCharacter->DrawData.Head.Id = old;
}
public void SetWeaponState(Actor actor, bool value)

View file

@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
@ -17,13 +16,15 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
private readonly ClientState _clientState;
private readonly ObjectTable _objects;
private readonly ActorService _actors;
private readonly TargetManager _targets;
public ObjectManager(Framework framework, ClientState clientState, ObjectTable objects, ActorService actors)
public ObjectManager(Framework framework, ClientState clientState, ObjectTable objects, ActorService actors, TargetManager targets)
{
_framework = framework;
_clientState = clientState;
_objects = objects;
_actors = actors;
_targets = targets;
}
public DateTime LastUpdate { get; private set; }
@ -32,6 +33,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
public ushort World { get; private set; }
private readonly Dictionary<ActorIdentifier, ActorData> _identifiers = new(200);
private readonly Dictionary<ActorIdentifier, ActorData> _allWorldIdentifiers = new(200);
public IReadOnlyDictionary<ActorIdentifier, ActorData> Identifiers
=> _identifiers;
@ -45,6 +47,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
LastUpdate = lastUpdate;
World = (ushort)(_clientState.LocalPlayer?.CurrentWorld.Id ?? 0u);
_identifiers.Clear();
_allWorldIdentifiers.Clear();
for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i)
{
@ -106,6 +109,23 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
{
data.Objects.Add(character);
}
if (identifier.Type is not (IdentifierType.Player or IdentifierType.Owned))
return;
var allWorld = _actors.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue,
identifier.Kind,
identifier.DataId);
if (!_allWorldIdentifiers.TryGetValue(allWorld, out var allWorldData))
{
allWorldData = new ActorData(character, allWorld.ToString());
_allWorldIdentifiers[allWorld] = allWorldData;
}
else
{
allWorldData.Objects.Add(character);
}
}
public Actor GPosePlayer
@ -114,6 +134,9 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
public Actor Player
=> _objects.GetObjectAddress(0);
public Actor Target
=> _targets.Target?.Address ?? nint.Zero;
public (ActorIdentifier Identifier, ActorData Data) PlayerData
{
get
@ -121,7 +144,18 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
Update();
return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
? (ident, data)
: (ActorIdentifier.Invalid, ActorData.Invalid);
: (ident, ActorData.Invalid);
}
}
public (ActorIdentifier Identifier, ActorData Data) TargetData
{
get
{
Update();
return Target.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
? (ident, data)
: (ident, ActorData.Invalid);
}
}
@ -134,15 +168,16 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
public int Count
=> Identifiers.Count;
/// <summary> Also (inefficiently) handles All Worlds players. </summary>
/// <summary> Also handles All Worlds players. </summary>
public bool ContainsKey(ActorIdentifier key)
=> Identifiers.ContainsKey(key)
|| key.HomeWorld == ushort.MaxValue
&& Identifiers.Keys.FirstOrDefault(i => i.Type is IdentifierType.Player && i.PlayerName == key.PlayerName).IsValid;
=> Identifiers.ContainsKey(key) || _allWorldIdentifiers.ContainsKey(key);
public bool TryGetValue(ActorIdentifier key, out ActorData value)
=> Identifiers.TryGetValue(key, out value);
public bool TryGetValueAllWorld(ActorIdentifier key, out ActorData value)
=> _allWorldIdentifiers.TryGetValue(key, out value);
public ActorData this[ActorIdentifier key]
=> Identifiers[key];

View file

@ -1,4 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Dalamud.Logging;
using Dalamud.Plugin;
using Glamourer.Interop.Structs;
@ -8,6 +12,30 @@ using Penumbra.Api.Helpers;
namespace Glamourer.Interop.Penumbra;
using CurrentSettings = ValueTuple<PenumbraApiEc, (bool, int, IDictionary<string, IList<string>>, bool)?>;
public readonly record struct Mod(string Name, string DirectoryName) : IComparable<Mod>
{
public int CompareTo(Mod other)
{
var nameComparison = string.Compare(Name, other.Name, StringComparison.Ordinal);
if (nameComparison != 0)
return nameComparison;
return string.Compare(DirectoryName, other.DirectoryName, StringComparison.Ordinal);
}
}
public readonly record struct ModSettings(IDictionary<string, IList<string>> Settings, int Priority, bool Enabled)
{
public ModSettings()
: this(new Dictionary<string, IList<string>>(), 0, false)
{ }
public static ModSettings Empty
=> new();
}
public unsafe class PenumbraService : IDisposable
{
public const int RequiredPenumbraBreakingVersion = 4;
@ -23,6 +51,13 @@ public unsafe class PenumbraService : IDisposable
private FuncSubscriber<nint, (nint, string)> _drawObjectInfo;
private FuncSubscriber<int, int> _cutsceneParent;
private FuncSubscriber<int, (bool, bool, string)> _objectCollection;
private FuncSubscriber<IList<(string, string)>> _getMods;
private FuncSubscriber<ApiCollectionType, string> _currentCollection;
private FuncSubscriber<string, string, string, bool, CurrentSettings> _getCurrentSettings;
private FuncSubscriber<string, string, string, bool, PenumbraApiEc> _setMod;
private FuncSubscriber<string, string, string, int, PenumbraApiEc> _setModPriority;
private FuncSubscriber<string, string, string, string, string, PenumbraApiEc> _setModSetting;
private FuncSubscriber<string, string, string, string, IReadOnlyList<string>, PenumbraApiEc> _setModSettings;
private readonly EventSubscriber _initializedEvent;
private readonly EventSubscriber _disposedEvent;
@ -72,6 +107,90 @@ public unsafe class PenumbraService : IDisposable
remove => _modSettingChanged.Event -= value;
}
public IReadOnlyList<(Mod Mod, ModSettings Settings)> GetMods()
{
if (!Available)
return Array.Empty<(Mod Mod, ModSettings Settings)>();
try
{
var allMods = _getMods.Invoke();
var collection = _currentCollection.Invoke(ApiCollectionType.Current);
return allMods
.Select(m => (m.Item1, m.Item2, _getCurrentSettings.Invoke(collection, m.Item1, m.Item2, true)))
.Where(t => t.Item3.Item1 is PenumbraApiEc.Success)
.Select(t => (new Mod(t.Item2, t.Item1),
!t.Item3.Item2.HasValue
? ModSettings.Empty
: new ModSettings(t.Item3.Item2!.Value.Item3, t.Item3.Item2!.Value.Item2, t.Item3.Item2!.Value.Item1)))
.OrderByDescending(p => p.Item2.Enabled)
.ThenBy(p => p.Item1.Name)
.ThenBy(p => p.Item1.DirectoryName)
.ThenByDescending(p => p.Item2.Priority)
.ToList();
}
catch (Exception ex)
{
Glamourer.Log.Error($"Error fetching mods from Penumbra:\n{ex}");
return Array.Empty<(Mod Mod, ModSettings Settings)>();
}
}
public string CurrentCollection
=> Available ? _currentCollection.Invoke(ApiCollectionType.Current) : "<Unavailable>";
/// <summary>
/// Try to set all mod settings as desired. Only sets when the mod should be enabled.
/// If it is disabled, ignore all other settings.
/// </summary>
public string SetMod(Mod mod, ModSettings settings)
{
if (!Available)
return "Penumbra is not available.";
var sb = new StringBuilder();
try
{
var collection = _currentCollection.Invoke(ApiCollectionType.Current);
var ec = _setMod.Invoke(collection, mod.DirectoryName, mod.Name, settings.Enabled);
if (ec is PenumbraApiEc.ModMissing)
return $"The mod {mod.Name} [{mod.DirectoryName}] could not be found.";
Debug.Assert(ec is not PenumbraApiEc.CollectionMissing, "Missing collection should not be possible.");
if (!settings.Enabled)
return string.Empty;
ec = _setModPriority.Invoke(collection, mod.DirectoryName, mod.Name, settings.Priority);
Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged, "Setting Priority should not be able to fail.");
foreach (var (setting, list) in settings.Settings)
{
ec = list.Count == 1
? _setModSetting.Invoke(collection, mod.DirectoryName, mod.Name, setting, list[0])
: _setModSettings.Invoke(collection, mod.DirectoryName, mod.Name, setting, (IReadOnlyList<string>)list);
switch (ec)
{
case PenumbraApiEc.OptionGroupMissing:
sb.AppendLine($"Could not find the option group {setting} in mod {mod.Name}.");
break;
case PenumbraApiEc.OptionMissing:
sb.AppendLine($"Could not find all desired options in the option group {setting} in mod {mod.Name}.");
break;
}
Debug.Assert(ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged,
"Missing Mod or Collection should not be possible here.");
}
return sb.ToString();
}
catch (Exception ex)
{
return sb.AppendLine(ex.Message).ToString();
}
}
/// <summary> Obtain the name of the collection currently assigned to the player. </summary>
public string GetCurrentPlayerCollection()
{
@ -127,6 +246,14 @@ public unsafe class PenumbraService : IDisposable
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
_objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface);
_getMods = Ipc.GetMods.Subscriber(_pluginInterface);
_currentCollection = Ipc.GetCollectionForType.Subscriber(_pluginInterface);
_getCurrentSettings = Ipc.GetCurrentModSettings.Subscriber(_pluginInterface);
_setMod = Ipc.TrySetMod.Subscriber(_pluginInterface);
_setModPriority = Ipc.TrySetModPriority.Subscriber(_pluginInterface);
_setModSetting = Ipc.TrySetModSetting.Subscriber(_pluginInterface);
_setModSettings = Ipc.TrySetModSettings.Subscriber(_pluginInterface);
Available = true;
Glamourer.Log.Debug("Glamourer attached to Penumbra.");
}

View file

@ -7,12 +7,22 @@ namespace Glamourer.Services;
public class BackupService
{
private readonly Logger _logger;
private readonly DirectoryInfo _configDirectory;
private readonly IReadOnlyList<FileInfo> _fileNames;
public BackupService(Logger logger, FilenameService fileNames)
{
var files = GlamourerFiles(fileNames);
Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files);
_logger = logger;
_fileNames = GlamourerFiles(fileNames);
_configDirectory = new DirectoryInfo(fileNames.ConfigDirectory);
Backup.CreateAutomaticBackup(logger, _configDirectory, _fileNames);
}
/// <summary> Create a permanent backup with a given name for migrations. </summary>
public void CreateMigrationBackup(string name)
=> Backup.CreatePermanentBackup(_logger, _configDirectory, _fileNames, name);
/// <summary> Collect all relevant files for glamourer configuration. </summary>
private static IReadOnlyList<FileInfo> GlamourerFiles(FilenameService fileNames)
{

View file

@ -10,14 +10,16 @@ public class ConfigMigrationService
{
private readonly SaveService _saveService;
private readonly FixedDesignMigrator _fixedDesignMigrator;
private readonly BackupService _backupService;
private Configuration _config = null!;
private JObject _data = null!;
public ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator)
public ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator, BackupService backupService)
{
_saveService = saveService;
_fixedDesignMigrator = fixedDesignMigrator;
_backupService = backupService;
}
public void Migrate(Configuration config)
@ -39,6 +41,7 @@ public class ConfigMigrationService
if (_config.Version > 1)
return;
_backupService.CreateMigrationBackup("pre_v1_to_v2_migration");
_fixedDesignMigrator.Migrate(_data["FixedDesigns"]);
_config.Version = 2;
var customizationColor = _data["CustomizationColor"]?.ToObject<uint>() ?? ColorId.CustomizationDesign.Data().DefaultColor;

View file

@ -18,6 +18,7 @@ using Glamourer.Unlocks;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
using Penumbra.GameData.Data;
namespace Glamourer.Services;
@ -73,7 +74,8 @@ public static class ServiceManager
.AddSingleton<ItemService>()
.AddSingleton<ActorService>()
.AddSingleton<CustomizationService>()
.AddSingleton<ItemManager>();
.AddSingleton<ItemManager>()
.AddSingleton<HumanModelList>();
private static IServiceCollection AddInterop(this IServiceCollection services)
=> services.AddSingleton<VisorService>()
@ -93,7 +95,8 @@ public static class ServiceManager
.AddSingleton<DesignFileSystem>()
.AddSingleton<AutoDesignManager>()
.AddSingleton<AutoDesignApplier>()
.AddSingleton<FixedDesignMigrator>();
.AddSingleton<FixedDesignMigrator>()
.AddSingleton<DesignConverter>();
private static IServiceCollection AddState(this IServiceCollection services)
=> services.AddSingleton<StateManager>()
@ -114,6 +117,8 @@ public static class ServiceManager
.AddSingleton<DesignFileSystemSelector>()
.AddSingleton<DesignPanel>()
.AddSingleton<DesignTab>()
.AddSingleton<ModAssociationsTab>()
.AddSingleton<DesignDetailTab>()
.AddSingleton<UnlockTable>()
.AddSingleton<UnlockOverview>()
.AddSingleton<UnlocksTab>()

View file

@ -12,7 +12,7 @@ namespace Glamourer.State;
public class ActorState
{
public enum MetaFlag
public enum MetaIndex
{
Wetness = EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices,
HatState,
@ -45,6 +45,6 @@ public class ActorState
public ref StateChanged.Source this[CustomizeIndex type]
=> ref _sources[EquipFlagExtensions.NumEquipFlags + (int)type];
public ref StateChanged.Source this[MetaFlag flag]
=> ref _sources[(int)flag];
public ref StateChanged.Source this[MetaIndex index]
=> ref _sources[(int)index];
}

View file

@ -102,16 +102,19 @@ public class StateListener : IDisposable
var actor = (Actor)actorPtr;
var identifier = actor.GetIdentifier(_actors.AwaitedService);
var modelId = *(uint*)modelPtr;
ref var modelId = ref *(uint*)modelPtr;
ref var customize = ref *(Customize*)customizePtr;
if (_manager.TryGetValue(identifier, out var state))
{
_autoDesignApplier.Reduce(actor, identifier, state);
switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr))
{
// TODO handle right
case UpdateState.Change: break;
case UpdateState.Transformed: break;
case UpdateState.NoChange:
modelId = state.ModelData.ModelId;
switch (UpdateBaseData(actor, state, customize))
{
case UpdateState.Transformed: break;
@ -171,7 +174,7 @@ public class StateListener : IDisposable
// Do nothing. But this usually can not happen because the hooked function also writes to game objects later.
case UpdateState.Transformed: break;
case UpdateState.Change:
if (state[slot, false] is not StateChanged.Source.Fixed)
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
{
state.ModelData.SetItem(slot, state.BaseData.Item(slot));
state[slot, false] = StateChanged.Source.Game;
@ -181,7 +184,7 @@ public class StateListener : IDisposable
apply = true;
}
if (state[slot, false] is not StateChanged.Source.Fixed)
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
{
state.ModelData.SetStain(slot, state.BaseData.Stain(slot));
state[slot, true] = StateChanged.Source.Game;
@ -246,7 +249,7 @@ public class StateListener : IDisposable
// Update model state if not on fixed design.
case UpdateState.Change:
var apply = false;
if (state[slot, false] is not StateChanged.Source.Fixed)
if (state[slot, false] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
{
state.ModelData.SetItem(slot, state.BaseData.Item(slot));
state[slot, false] = StateChanged.Source.Game;
@ -256,7 +259,7 @@ public class StateListener : IDisposable
apply = true;
}
if (state[slot, true] is not StateChanged.Source.Fixed)
if (state[slot, true] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
{
state.ModelData.SetStain(slot, state.BaseData.Stain(slot));
state[slot, true] = StateChanged.Source.Game;
@ -364,7 +367,7 @@ public class StateListener : IDisposable
{
// if base state changed, either overwrite the actual value if we have fixed values,
// or overwrite the stored model state with the new one.
if (state[ActorState.MetaFlag.VisorState] is StateChanged.Source.Fixed)
if (state[ActorState.MetaIndex.VisorState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc)
value.Value = state.ModelData.IsVisorToggled();
else
_manager.ChangeVisorState(state, value, StateChanged.Source.Game);
@ -394,7 +397,7 @@ public class StateListener : IDisposable
{
// if base state changed, either overwrite the actual value if we have fixed values,
// or overwrite the stored model state with the new one.
if (state[ActorState.MetaFlag.HatState] is StateChanged.Source.Fixed)
if (state[ActorState.MetaIndex.HatState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc)
value.Value = state.ModelData.IsHatVisible();
else
_manager.ChangeHatState(state, value, StateChanged.Source.Game);
@ -424,7 +427,7 @@ public class StateListener : IDisposable
{
// if base state changed, either overwrite the actual value if we have fixed values,
// or overwrite the stored model state with the new one.
if (state[ActorState.MetaFlag.WeaponState] is StateChanged.Source.Fixed)
if (state[ActorState.MetaIndex.WeaponState] is StateChanged.Source.Fixed or StateChanged.Source.Ipc)
value.Value = state.ModelData.IsWeaponVisible();
else
_manager.ChangeWeaponState(state, value, StateChanged.Source.Game);

View file

@ -192,6 +192,21 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
#region Change Values
/// <summary> Turn a non-human actor human. </summary>
public void TurnHuman(ActorState state, StateChanged.Source source)
{
if (state.ModelData.ModelId == 0)
return;
state.ModelData.ModelId = 0;
state[ActorState.MetaIndex.ModelId] = source;
ChangeCustomize(state, Customize.Default, CustomizeFlagExtensions.All, source);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
ChangeEquip(state, slot, ItemManager.NothingItem(slot), 0, source);
ChangeEquip(state, EquipSlot.MainHand, _items.DefaultSword, 0, source);
ChangeEquip(state, EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield), 0, source);
}
/// <summary> Change a customization value. </summary>
public void ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source)
{
@ -256,9 +271,14 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
if (source is StateChanged.Source.Manual)
if (type == StateChanged.Type.Equip)
{
if (slot is not EquipSlot.Head || state.ModelData.IsHatVisible())
_editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot));
}
else
{
_editor.ChangeWeapon(objects, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot));
}
// Meta.
Glamourer.Log.Verbose(
@ -283,9 +303,14 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
if (source is StateChanged.Source.Manual)
if (type == StateChanged.Type.Equip)
{
if (slot is not EquipSlot.Head || state.ModelData.IsHatVisible())
_editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot));
}
else
{
_editor.ChangeWeapon(objects, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot));
}
// Meta.
Glamourer.Log.Verbose(
@ -322,7 +347,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Update state data.
var old = state.ModelData.IsHatVisible();
state.ModelData.SetHatVisible(value);
state[ActorState.MetaFlag.HatState] = source;
state[ActorState.MetaIndex.HatState] = source;
// Update draw objects / game objects.
_objects.Update();
@ -333,7 +358,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Meta.
Glamourer.Log.Verbose(
$"Set Head Gear Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.HatState));
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaIndex.HatState));
}
/// <summary> Change weapon visibility. </summary>
@ -342,7 +367,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Update state data.
var old = state.ModelData.IsWeaponVisible();
state.ModelData.SetWeaponVisible(value);
state[ActorState.MetaFlag.WeaponState] = source;
state[ActorState.MetaIndex.WeaponState] = source;
// Update draw objects / game objects.
_objects.Update();
@ -353,7 +378,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Meta.
Glamourer.Log.Verbose(
$"Set Weapon Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.WeaponState));
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaIndex.WeaponState));
}
/// <summary> Change visor state. </summary>
@ -362,7 +387,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Update state data.
var old = state.ModelData.IsVisorToggled();
state.ModelData.SetVisor(value);
state[ActorState.MetaFlag.VisorState] = source;
state[ActorState.MetaIndex.VisorState] = source;
// Update draw objects.
_objects.Update();
@ -373,7 +398,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Meta.
Glamourer.Log.Verbose(
$"Set Visor State in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.VisorState));
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaIndex.VisorState));
}
/// <summary> Set GPose Wetness. </summary>
@ -382,7 +407,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Update state data.
var old = state.ModelData.IsWet();
state.ModelData.SetIsWet(value);
state[ActorState.MetaFlag.Wetness] = source;
state[ActorState.MetaIndex.Wetness] = source;
// Update draw objects / game objects.
_objects.Update();
@ -392,12 +417,12 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Meta.
Glamourer.Log.Verbose(
$"Set Wetness in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Other, state[ActorState.MetaFlag.Wetness], state, objects, (old, value, ActorState.MetaFlag.Wetness));
_event.Invoke(StateChanged.Type.Other, state[ActorState.MetaIndex.Wetness], state, objects, (old, value, ActorState.MetaIndex.Wetness));
}
#endregion
public void ApplyDesign(Design design, ActorState state)
public void ApplyDesign(DesignBase design, ActorState state)
{
void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain)
{
@ -416,6 +441,12 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
}
}
if (state.ModelData.ModelId != 0 && design.DesignData.ModelId == 0)
TurnHuman(state, StateChanged.Source.Manual);
if (design.DoApplyHatVisible())
ChangeHatState(state, design.DesignData.IsHatVisible(), StateChanged.Source.Manual);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot));
@ -428,8 +459,6 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
&& design.DesignData.Item(EquipSlot.OffHand).Type == state.BaseData.Item(EquipSlot.OffHand).Type,
design.DoApplyStain(EquipSlot.OffHand));
if (design.DoApplyHatVisible())
ChangeHatState(state, design.DesignData.IsHatVisible(), StateChanged.Source.Manual);
if (design.DoApplyWeaponVisible())
ChangeWeaponState(state, design.DesignData.IsWeaponVisible(), StateChanged.Source.Manual);
if (design.DoApplyVisorToggle())

View file

@ -111,12 +111,12 @@ public class ItemUnlockManager : ISavable, IDisposable
scan |= newArmoireState;
}
//var newAchievementState = uiState->Achievement.IsAchievementLoaded();
//if (newAchievementState != _lastAchievementState)
//{
// _lastAchievementState = newAchievementState;
// scan |= newAchievementState;
//}
var newAchievementState = uiState->Achievement.IsLoaded();
if (newAchievementState != _lastAchievementState)
{
_lastAchievementState = newAchievementState;
scan |= newAchievementState;
}
if (scan)
Scan();

View file

@ -0,0 +1,55 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using Penumbra.String.Functions;
namespace Glamourer.Utility;
public static class CompressExtensions
{
/// <summary> Compress a byte array with a prepended version. </summary>
public static unsafe byte[] Compress(this byte[] data, byte version)
{
using var compressedStream = new MemoryStream();
using var zipStream = new GZipStream(compressedStream, CompressionMode.Compress);
zipStream.Write(data, 0, data.Length);
zipStream.Flush();
var ret = new byte[compressedStream.Length + 1];
ret[0] = version;
fixed (byte* ptr1 = compressedStream.GetBuffer(), ptr2 = ret)
{
MemoryUtility.MemCpyUnchecked(ptr2 + 1, ptr1, (int)compressedStream.Length);
}
return ret;
}
/// <summary> Compress a string with a prepended version. </summary>
public static byte[] Compress(this string data, byte version)
{
var bytes = Encoding.UTF8.GetBytes(data);
return bytes.Compress(version);
}
/// <summary> Decompress a byte array into a returned version byte and an array of the remaining bytes. </summary>
public static byte Decompress(this byte[] compressed, out byte[] decompressed)
{
var ret = compressed[0];
using var compressedStream = new MemoryStream(compressed, 1, compressed.Length - 1);
using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
using var resultStream = new MemoryStream();
zipStream.CopyTo(resultStream);
decompressed = resultStream.ToArray();
return ret;
}
/// <summary> Decompress a byte array into a returned version byte and a string of the remaining bytes as UTF8. </summary>
public static byte DecompressToString(this byte[] compressed, out string decompressed)
{
var ret = compressed.Decompress(out var bytes);
decompressed = Encoding.UTF8.GetString(bytes);
return ret;
}
}