mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 10:17:23 +01:00
,
This commit is contained in:
parent
7463aafa13
commit
27f151c55a
32 changed files with 1744 additions and 151 deletions
|
|
@ -93,13 +93,13 @@ public unsafe struct Customize
|
|||
public void Load(Customize other)
|
||||
=> Data.Read(&other.Data);
|
||||
|
||||
public void Write(nint target)
|
||||
public readonly void Write(nint target)
|
||||
=> Data.Write((void*)target);
|
||||
|
||||
public bool LoadBase64(string data)
|
||||
=> Data.LoadBase64(data);
|
||||
|
||||
public string WriteBase64()
|
||||
public readonly string WriteBase64()
|
||||
=> Data.WriteBase64();
|
||||
|
||||
public static CustomizeFlag Compare(Customize lhs, Customize rhs)
|
||||
|
|
|
|||
74
Glamourer.GameData/Structs/EquipFlag.cs
Normal file
74
Glamourer.GameData/Structs/EquipFlag.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
using System;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Glamourer.Structs;
|
||||
|
||||
[Flags]
|
||||
public enum EquipFlag : uint
|
||||
{
|
||||
Head = 0x00000001,
|
||||
Body = 0x00000002,
|
||||
Hands = 0x00000004,
|
||||
Legs = 0x00000008,
|
||||
Feet = 0x00000010,
|
||||
Ears = 0x00000020,
|
||||
Neck = 0x00000040,
|
||||
Wrist = 0x00000080,
|
||||
RFinger = 0x00000100,
|
||||
LFinger = 0x00000200,
|
||||
Mainhand = 0x00000400,
|
||||
Offhand = 0x00000800,
|
||||
HeadStain = 0x00001000,
|
||||
BodyStain = 0x00002000,
|
||||
HandsStain = 0x00004000,
|
||||
LegsStain = 0x00008000,
|
||||
FeetStain = 0x00010000,
|
||||
EarsStain = 0x00020000,
|
||||
NeckStain = 0x00040000,
|
||||
WristStain = 0x00080000,
|
||||
RFingerStain = 0x00100000,
|
||||
LFingerStain = 0x00200000,
|
||||
MainhandStain = 0x00400000,
|
||||
OffhandStain = 0x00800000,
|
||||
}
|
||||
|
||||
public static class EquipFlagExtensions
|
||||
{
|
||||
public const EquipFlag All = (EquipFlag)(((uint)EquipFlag.OffhandStain << 1) - 1);
|
||||
|
||||
public static EquipFlag ToFlag(this EquipSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.MainHand => EquipFlag.Mainhand,
|
||||
EquipSlot.OffHand => EquipFlag.Offhand,
|
||||
EquipSlot.Head => EquipFlag.Head,
|
||||
EquipSlot.Body => EquipFlag.Body,
|
||||
EquipSlot.Hands => EquipFlag.Hands,
|
||||
EquipSlot.Legs => EquipFlag.Legs,
|
||||
EquipSlot.Feet => EquipFlag.Feet,
|
||||
EquipSlot.Ears => EquipFlag.Ears,
|
||||
EquipSlot.Neck => EquipFlag.Neck,
|
||||
EquipSlot.Wrists => EquipFlag.Wrist,
|
||||
EquipSlot.RFinger => EquipFlag.RFinger,
|
||||
EquipSlot.LFinger => EquipFlag.LFinger,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
public static EquipFlag ToStainFlag(this EquipSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
EquipSlot.MainHand => EquipFlag.MainhandStain,
|
||||
EquipSlot.OffHand => EquipFlag.OffhandStain,
|
||||
EquipSlot.Head => EquipFlag.HeadStain,
|
||||
EquipSlot.Body => EquipFlag.BodyStain,
|
||||
EquipSlot.Hands => EquipFlag.HandsStain,
|
||||
EquipSlot.Legs => EquipFlag.LegsStain,
|
||||
EquipSlot.Feet => EquipFlag.FeetStain,
|
||||
EquipSlot.Ears => EquipFlag.EarsStain,
|
||||
EquipSlot.Neck => EquipFlag.NeckStain,
|
||||
EquipSlot.Wrists => EquipFlag.WristStain,
|
||||
EquipSlot.RFinger => EquipFlag.RFingerStain,
|
||||
EquipSlot.LFinger => EquipFlag.LFingerStain,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
80
Glamourer/Configuration.cs
Normal file
80
Glamourer/Configuration.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Glamourer.Gui;
|
||||
using Glamourer.Services;
|
||||
using Newtonsoft.Json;
|
||||
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
||||
|
||||
namespace Glamourer;
|
||||
|
||||
public class Configuration : IPluginConfiguration, ISavable
|
||||
{
|
||||
public bool UseRestrictedGearProtection = true;
|
||||
|
||||
public int Version { get; set; } = Constants.CurrentVersion;
|
||||
|
||||
public Dictionary<ColorId, uint> Colors { get; private set; }
|
||||
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
public Configuration(SaveService saveService, ConfigMigrationService migrator)
|
||||
{
|
||||
_saveService = saveService;
|
||||
Load(migrator);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
=> _saveService.QueueSave(this);
|
||||
|
||||
public void Load(ConfigMigrationService migrator)
|
||||
{
|
||||
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
|
||||
{
|
||||
Glamourer.Log.Error(
|
||||
$"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
|
||||
errorArgs.ErrorContext.Handled = true;
|
||||
}
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.ConfigFile))
|
||||
return;
|
||||
|
||||
if (File.Exists(_saveService.FileNames.ConfigFile))
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(_saveService.FileNames.ConfigFile);
|
||||
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
|
||||
{
|
||||
Error = HandleDeserializationError,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.Chat.NotificationMessage(ex,
|
||||
"Error reading Configuration, reverting to default.\nYou may be able to restore your configuration using the rolling backups in the XIVLauncher/backups/Glamourer directory.",
|
||||
"Error reading Configuration", "Error", NotificationType.Error);
|
||||
}
|
||||
|
||||
migrator.Migrate(this);
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.ConfigFile;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
|
||||
serializer.Serialize(jWriter, this);
|
||||
}
|
||||
|
||||
public static class Constants
|
||||
{
|
||||
public const int CurrentVersion = 2;
|
||||
}
|
||||
}
|
||||
549
Glamourer/Designs/Design.cs
Normal file
549
Glamourer/Designs/Design.cs
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Glamourer.Customization;
|
||||
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
|
||||
{
|
||||
internal Design(ItemManager items)
|
||||
{ }
|
||||
|
||||
// Metadata
|
||||
public const int FileVersion = 1;
|
||||
|
||||
public Guid Identifier { get; internal init; }
|
||||
public DateTimeOffset CreationDate { get; internal init; }
|
||||
public DateTimeOffset LastEdit { get; internal set; }
|
||||
public LowerString Name { get; internal set; } = LowerString.Empty;
|
||||
public string Description { get; internal set; } = string.Empty;
|
||||
public string[] Tags { get; internal set; } = Array.Empty<string>();
|
||||
public int Index { get; internal set; }
|
||||
|
||||
internal DesignData DesignData;
|
||||
|
||||
#region Application Data
|
||||
|
||||
[Flags]
|
||||
private enum DesignFlags : byte
|
||||
{
|
||||
ApplyHatVisible = 0x01,
|
||||
ApplyVisorState = 0x02,
|
||||
ApplyWeaponVisible = 0x04,
|
||||
WriteProtected = 0x08,
|
||||
}
|
||||
|
||||
private CustomizeFlag _applyCustomize;
|
||||
private EquipFlag _applyEquip;
|
||||
private DesignFlags _designFlags;
|
||||
|
||||
public bool DoApplyHatVisible()
|
||||
=> _designFlags.HasFlag(DesignFlags.ApplyHatVisible);
|
||||
|
||||
public bool DoApplyVisorToggle()
|
||||
=> _designFlags.HasFlag(DesignFlags.ApplyVisorState);
|
||||
|
||||
public bool DoApplyWeaponVisible()
|
||||
=> _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible);
|
||||
|
||||
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 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 ISavable
|
||||
|
||||
public JObject JsonSerialize()
|
||||
{
|
||||
var ret = new JObject
|
||||
{
|
||||
["FileVersion"] = FileVersion,
|
||||
["Identifier"] = Identifier,
|
||||
["CreationDate"] = CreationDate,
|
||||
["LastEdit"] = LastEdit,
|
||||
["Name"] = Name.Text,
|
||||
["Description"] = Description,
|
||||
["Tags"] = JArray.FromObject(Tags),
|
||||
["WriteProtected"] = WriteProtected(),
|
||||
["Equipment"] = SerializeEquipment(),
|
||||
["Customize"] = SerializeCustomize(),
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public 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["IsWet"] = DesignData.IsWet();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static Design LoadDesign(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes)
|
||||
{
|
||||
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
|
||||
return version switch
|
||||
{
|
||||
1 => LoadDesignV1(customizeManager, items, json, out changes),
|
||||
_ => throw new Exception("The design to be loaded has no valid Version."),
|
||||
};
|
||||
}
|
||||
|
||||
private static Design LoadDesignV1(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes)
|
||||
{
|
||||
static string[] ParseTags(JObject json)
|
||||
{
|
||||
var tags = json["Tags"]?.ToObject<string[]>() ?? Array.Empty<string>();
|
||||
return tags.OrderBy(t => t).Distinct().ToArray();
|
||||
}
|
||||
|
||||
var creationDate = json["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
|
||||
|
||||
var design = new Design(items)
|
||||
{
|
||||
CreationDate = creationDate,
|
||||
Identifier = json["Identifier"]?.ToObject<Guid>() ?? throw new ArgumentNullException("Identifier"),
|
||||
Name = new LowerString(json["Name"]?.ToObject<string>() ?? throw new ArgumentNullException("Name")),
|
||||
Description = json["Description"]?.ToObject<string>() ?? string.Empty,
|
||||
Tags = ParseTags(json),
|
||||
LastEdit = json["LastEdit"]?.ToObject<DateTimeOffset>() ?? creationDate,
|
||||
};
|
||||
|
||||
changes = LoadEquip(items, json["Equipment"], design);
|
||||
changes |= LoadCustomize(customizeManager, json["Customize"], design);
|
||||
return design;
|
||||
}
|
||||
|
||||
private static bool ValidateItem(ItemManager items, EquipSlot slot, uint itemId, out EquipItem item)
|
||||
{
|
||||
item = items.Resolve(slot, itemId);
|
||||
if (item.Valid)
|
||||
return true;
|
||||
|
||||
Glamourer.Chat.NotificationMessage($"The {slot.ToName()} item {itemId} does not exist, reset to Nothing.", "Warning",
|
||||
NotificationType.Warning);
|
||||
item = ItemManager.NothingItem(slot);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ValidateStain(ItemManager items, StainId stain, out StainId ret)
|
||||
{
|
||||
if (stain.Value != 0 && !items.Stains.ContainsKey(stain))
|
||||
{
|
||||
ret = 0;
|
||||
Glamourer.Chat.NotificationMessage($"The Stain {stain} does not exist, reset to unstained.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ret = stain;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ValidateWeapons(ItemManager items, uint mainId, uint offId, out EquipItem main, out EquipItem off)
|
||||
{
|
||||
var ret = true;
|
||||
main = items.Resolve(EquipSlot.MainHand, mainId);
|
||||
if (!main.Valid)
|
||||
{
|
||||
Glamourer.Chat.NotificationMessage($"The mainhand weapon {mainId} does not exist, reset to default sword.", "Warning",
|
||||
NotificationType.Warning);
|
||||
main = items.DefaultSword;
|
||||
ret = false;
|
||||
}
|
||||
|
||||
off = items.Resolve(main.Type.Offhand(), offId);
|
||||
if (off.Valid)
|
||||
return ret;
|
||||
|
||||
ret = false;
|
||||
off = items.Resolve(main.Type.Offhand(), mainId);
|
||||
if (off.Valid)
|
||||
{
|
||||
Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to implied offhand.", "Warning",
|
||||
NotificationType.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
off = ItemManager.NothingItem(FullEquipType.Shield);
|
||||
if (main.Type.Offhand() == FullEquipType.Shield)
|
||||
{
|
||||
Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to no offhand.", "Warning",
|
||||
NotificationType.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
main = items.DefaultSword;
|
||||
Glamourer.Chat.NotificationMessage(
|
||||
$"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing.",
|
||||
"Warning",
|
||||
NotificationType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool LoadEquip(ItemManager items, JToken? equip, Design design)
|
||||
{
|
||||
if (equip == null)
|
||||
return true;
|
||||
|
||||
static (uint, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item)
|
||||
{
|
||||
var id = item?["ItemId"]?.ToObject<uint>() ?? ItemManager.NothingId(slot);
|
||||
var stain = (StainId)(item?["Stain"]?.ToObject<byte>() ?? 0);
|
||||
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
|
||||
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
|
||||
return (id, stain, apply, applyStain);
|
||||
}
|
||||
|
||||
var changes = false;
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
|
||||
changes |= !ValidateItem(items, slot, id, out var item);
|
||||
changes |= !ValidateStain(items, stain, out stain);
|
||||
design.DesignData.SetItem(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);
|
||||
changes |= ValidateWeapons(items, id, idOff, out var main, out var off);
|
||||
changes |= ValidateStain(items, stain, out stain);
|
||||
changes |= ValidateStain(items, stainOff, out stainOff);
|
||||
design.DesignData.SetItem(main);
|
||||
design.DesignData.SetItem(off);
|
||||
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);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static bool ValidateCustomize(CustomizationManager manager, ref Customize customize)
|
||||
{
|
||||
var ret = true;
|
||||
if (!manager.Races.Contains(customize.Race))
|
||||
{
|
||||
ret = false;
|
||||
if (manager.Clans.Contains(customize.Clan))
|
||||
{
|
||||
Glamourer.Chat.NotificationMessage(
|
||||
$"The race {customize.Race.ToName()} is unknown, reset to {customize.Clan.ToRace().ToName()} from Clan.", "Warning",
|
||||
NotificationType.Warning);
|
||||
customize.Race = customize.Clan.ToRace();
|
||||
}
|
||||
else
|
||||
{
|
||||
Glamourer.Chat.NotificationMessage(
|
||||
$"The race {customize.Race.ToName()} is unknown, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}.", "Warning",
|
||||
NotificationType.Warning);
|
||||
customize.Race = Race.Hyur;
|
||||
customize.Clan = SubRace.Midlander;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manager.Clans.Contains(customize.Clan))
|
||||
{
|
||||
ret = false;
|
||||
var oldClan = customize.Clan;
|
||||
customize.Clan = (SubRace)((byte)customize.Race * 2 - 1);
|
||||
if (manager.Clans.Contains(customize.Clan))
|
||||
{
|
||||
Glamourer.Chat.NotificationMessage($"The clan {oldClan.ToName()} is unknown, reset to {customize.Clan.ToName()} from race.",
|
||||
"Warning", NotificationType.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
customize.Race = Race.Hyur;
|
||||
customize.Clan = SubRace.Midlander;
|
||||
Glamourer.Chat.NotificationMessage(
|
||||
$"The clan {oldClan.ToName()} is unknown, reset to {customize.Race.ToName()} {customize.Clan.ToName()}.", "Warning",
|
||||
NotificationType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
if (!manager.Genders.Contains(customize.Gender))
|
||||
{
|
||||
ret = false;
|
||||
Glamourer.Chat.NotificationMessage($"The gender {customize.Gender} is unknown, reset to {Gender.Male.ToName()}.", "Warning",
|
||||
NotificationType.Warning);
|
||||
customize.Gender = Gender.Male;
|
||||
}
|
||||
|
||||
// TODO: Female Hrothgar
|
||||
if (customize.Gender == Gender.Female && customize.Race == Race.Hrothgar)
|
||||
{
|
||||
ret = false;
|
||||
Glamourer.Chat.NotificationMessage($"Hrothgar do not currently support female characters, reset to male.", "Warning",
|
||||
NotificationType.Warning);
|
||||
customize.Gender = Gender.Male;
|
||||
}
|
||||
|
||||
var list = manager.GetList(customize.Clan, customize.Gender);
|
||||
|
||||
// Face is handled first automatically so it should not conflict with other customizations when corrupt.
|
||||
foreach (var index in Enum.GetValues<CustomizeIndex>().Where(list.IsAvailable))
|
||||
{
|
||||
var value = customize.Get(index);
|
||||
var count = list.Count(index, customize.Face);
|
||||
var idx = list.DataByValue(index, value, out var data, customize.Face);
|
||||
if (idx >= 0 && idx < count)
|
||||
continue;
|
||||
|
||||
ret = false;
|
||||
var name = list.Option(index);
|
||||
var newValue = list.Data(index, 0, customize.Face);
|
||||
Glamourer.Chat.NotificationMessage(
|
||||
$"Customization {name} for {customize.Race.ToName()} {customize.Gender.ToName()}s does not support value {value.Value}, reset to {newValue.Value.Value}");
|
||||
customize.Set(index, newValue.Value);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool ValidateModelId(ref uint modelId)
|
||||
{
|
||||
if (modelId != 0)
|
||||
{
|
||||
Glamourer.Chat.NotificationMessage($"Model IDs different from 0 are not currently allowed, reset {modelId} to 0.", "Warning",
|
||||
NotificationType.Warning);
|
||||
modelId = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool LoadCustomize(CustomizationManager manager, JToken? json, Design design)
|
||||
{
|
||||
if (json == null)
|
||||
return true;
|
||||
|
||||
design.DesignData.ModelId = json["ModelId"]?.ToObject<uint>() ?? 0;
|
||||
var ret = !ValidateModelId(ref design.DesignData.ModelId);
|
||||
|
||||
foreach (var idx in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
var tok = json[idx.ToString()];
|
||||
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
|
||||
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
|
||||
design.DesignData.Customize[idx] = data;
|
||||
design.SetApplyCustomize(idx, apply);
|
||||
}
|
||||
|
||||
design.DesignData.SetIsWet(json["IsWet"]?.ToObject<bool>() ?? false);
|
||||
ret |= !ValidateCustomize(manager, ref design.DesignData.Customize);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
//public void MigrateBase64(ItemManager items, string base64)
|
||||
//{
|
||||
// var data = DesignBase64Migration.MigrateBase64(items, base64, out var applyEquip, out var applyCustomize, out var writeProtected, out var wet,
|
||||
// out var hat,
|
||||
// out var visor, out var weapon);
|
||||
// UpdateMainhand(items, data.MainHand);
|
||||
// UpdateOffhand(items, data.OffHand);
|
||||
// foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
// UpdateArmor(items, slot, data.Armor(slot), true);
|
||||
// ModelData.Customize = data.Customize;
|
||||
// _applyEquip = applyEquip;
|
||||
// _applyCustomize = applyCustomize;
|
||||
// WriteProtected = writeProtected;
|
||||
// Wetness = wet;
|
||||
// Hat = hat;
|
||||
// Visor = visor;
|
||||
// Weapon = weapon;
|
||||
//}
|
||||
//
|
||||
//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);
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.DesignFile(this);
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var j = new JsonTextWriter(writer)
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
var obj = JsonSerialize();
|
||||
obj.WriteTo(j);
|
||||
}
|
||||
|
||||
public string LogName(string fileName)
|
||||
=> Path.GetFileNameWithoutExtension(fileName);
|
||||
|
||||
#endregion
|
||||
}
|
||||
150
Glamourer/Designs/DesignBase64Migration.cs
Normal file
150
Glamourer/Designs/DesignBase64Migration.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
using System;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Services;
|
||||
using Glamourer.Structs;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public static class DesignBase64Migration
|
||||
{
|
||||
public const int Base64Size = 91;
|
||||
|
||||
public static DesignData MigrateBase64(ItemManager items, string base64, out EquipFlag equipFlags, out CustomizeFlag customizeFlags,
|
||||
out bool writeProtected, out bool applyHat, out bool applyVisor, out bool applyWeapon)
|
||||
{
|
||||
static void CheckSize(int length, int requiredLength)
|
||||
{
|
||||
if (length != requiredLength)
|
||||
throw new Exception(
|
||||
$"Can not parse Base64 string into CharacterSave:\n\tInvalid size {length} instead of {requiredLength}.");
|
||||
}
|
||||
|
||||
byte applicationFlags;
|
||||
ushort equipFlagsS;
|
||||
var bytes = Convert.FromBase64String(base64);
|
||||
applyHat = false;
|
||||
applyVisor = false;
|
||||
applyWeapon = false;
|
||||
var data = new DesignData();
|
||||
switch (bytes[0])
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
CheckSize(bytes.Length, 86);
|
||||
applicationFlags = bytes[1];
|
||||
equipFlagsS = BitConverter.ToUInt16(bytes, 2);
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
CheckSize(bytes.Length, Base64Size);
|
||||
applicationFlags = bytes[1];
|
||||
equipFlagsS = BitConverter.ToUInt16(bytes, 2);
|
||||
data.SetHatVisible((bytes[90] & 0x01) == 0);
|
||||
data.SetVisor((bytes[90] & 0x10) != 0);
|
||||
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
|
||||
break;
|
||||
}
|
||||
default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}.");
|
||||
}
|
||||
|
||||
customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0;
|
||||
data.SetIsWet((applicationFlags & 0x02) != 0);
|
||||
applyHat = (applicationFlags & 0x04) != 0;
|
||||
applyWeapon = (applicationFlags & 0x08) != 0;
|
||||
applyVisor = (applicationFlags & 0x10) != 0;
|
||||
writeProtected = (applicationFlags & 0x20) != 0;
|
||||
|
||||
equipFlags = 0;
|
||||
equipFlags |= (equipFlagsS & 0x0001) != 0 ? EquipFlag.Mainhand | EquipFlag.MainhandStain : 0;
|
||||
equipFlags |= (equipFlagsS & 0x0002) != 0 ? EquipFlag.Offhand | EquipFlag.OffhandStain : 0;
|
||||
var flag = 0x0002u;
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
flag <<= 1;
|
||||
equipFlags |= (equipFlagsS & flag) != 0 ? slot.ToFlag() | slot.ToStainFlag() : 0;
|
||||
}
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* ptr = bytes)
|
||||
{
|
||||
data.Customize.Load(*(Customize*)(ptr + 4));
|
||||
var cur = (CharacterWeapon*)(ptr + 30);
|
||||
var main = items.Identify(EquipSlot.MainHand, cur[0].Set, cur[0].Type, (byte)cur[0].Variant);
|
||||
if (!main.Valid)
|
||||
throw new Exception($"Base64 string invalid, weapon could not be identified.");
|
||||
|
||||
data.SetItem(main);
|
||||
data.SetStain(EquipSlot.MainHand, cur[0].Stain);
|
||||
var off = items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, (byte)cur[1].Variant, main.Type);
|
||||
if (!off.Valid)
|
||||
throw new Exception($"Base64 string invalid, weapon could not be identified.");
|
||||
|
||||
data.SetItem(off);
|
||||
data.SetStain(EquipSlot.OffHand, cur[1].Stain);
|
||||
|
||||
var eq = (CharacterArmor*)(ptr + 46);
|
||||
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
|
||||
{
|
||||
var mdl = eq[idx];
|
||||
var item = items.Identify(slot, mdl.Set, mdl.Variant);
|
||||
if (!item.Valid)
|
||||
throw new Exception($"Base64 string invalid, item could not be identified.");
|
||||
|
||||
data.SetItem(item);
|
||||
data.SetStain(slot, mdl.Stain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags,
|
||||
bool setHat, bool setVisor, bool setWeapon, bool writeProtected, float alpha = 1.0f)
|
||||
{
|
||||
var data = stackalloc byte[Base64Size];
|
||||
data[0] = 2;
|
||||
data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0)
|
||||
| (save.IsWet() ? 0x02 : 0)
|
||||
| (setHat ? 0x04 : 0)
|
||||
| (setWeapon ? 0x08 : 0)
|
||||
| (setVisor ? 0x10 : 0)
|
||||
| (writeProtected ? 0x20 : 0));
|
||||
data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Head) ? 0x04 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Body) ? 0x08 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Hands) ? 0x10 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Legs) ? 0x20 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Feet) ? 0x40 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Ears) ? 0x80 : 0));
|
||||
data[3] = (byte)((equipFlags.HasFlag(EquipFlag.Neck) ? 0x01 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0)
|
||||
| (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0));
|
||||
save.Customize.Write((nint)data + 4);
|
||||
((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand));
|
||||
((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand));
|
||||
((CharacterArmor*)(data + 46))[0] = save.Item(EquipSlot.Head).Armor(save.Stain(EquipSlot.Head));
|
||||
((CharacterArmor*)(data + 46))[1] = save.Item(EquipSlot.Body).Armor(save.Stain(EquipSlot.Body));
|
||||
((CharacterArmor*)(data + 46))[2] = save.Item(EquipSlot.Hands).Armor(save.Stain(EquipSlot.Hands));
|
||||
((CharacterArmor*)(data + 46))[3] = save.Item(EquipSlot.Legs).Armor(save.Stain(EquipSlot.Legs));
|
||||
((CharacterArmor*)(data + 46))[4] = save.Item(EquipSlot.Feet).Armor(save.Stain(EquipSlot.Feet));
|
||||
((CharacterArmor*)(data + 46))[5] = save.Item(EquipSlot.Ears).Armor(save.Stain(EquipSlot.Ears));
|
||||
((CharacterArmor*)(data + 46))[6] = save.Item(EquipSlot.Neck).Armor(save.Stain(EquipSlot.Neck));
|
||||
((CharacterArmor*)(data + 46))[7] = save.Item(EquipSlot.Wrists).Armor(save.Stain(EquipSlot.Wrists));
|
||||
((CharacterArmor*)(data + 46))[8] = save.Item(EquipSlot.RFinger).Armor(save.Stain(EquipSlot.RFinger));
|
||||
((CharacterArmor*)(data + 46))[9] = save.Item(EquipSlot.LFinger).Armor(save.Stain(EquipSlot.LFinger));
|
||||
*(float*)(data + 86) = 1f;
|
||||
data[90] = (byte)((save.IsHatVisible() ? 0x01 : 0)
|
||||
| (save.IsVisorToggled() ? 0x10 : 0)
|
||||
| (save.IsWeaponVisible() ? 0x02 : 0));
|
||||
|
||||
return Convert.ToBase64String(new Span<byte>(data, Base64Size));
|
||||
}
|
||||
}
|
||||
179
Glamourer/Designs/DesignData.cs
Normal file
179
Glamourer/Designs/DesignData.cs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Glamourer.Customization;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public unsafe struct DesignData
|
||||
{
|
||||
private string _nameHead = string.Empty;
|
||||
private string _nameBody = string.Empty;
|
||||
private string _nameHands = string.Empty;
|
||||
private string _nameLegs = string.Empty;
|
||||
private string _nameFeet = string.Empty;
|
||||
private string _nameEars = string.Empty;
|
||||
private string _nameNeck = string.Empty;
|
||||
private string _nameWrists = string.Empty;
|
||||
private string _nameRFinger = string.Empty;
|
||||
private string _nameLFinger = string.Empty;
|
||||
private string _nameMainhand = string.Empty;
|
||||
private string _nameOffhand = string.Empty;
|
||||
private fixed uint _itemIds[12];
|
||||
private fixed ushort _iconIds[12];
|
||||
private fixed byte _equipmentBytes[48];
|
||||
public Customize Customize = Customize.Default;
|
||||
public uint ModelId;
|
||||
private WeaponType _secondaryMainhand;
|
||||
private WeaponType _secondaryOffhand;
|
||||
private FullEquipType _typeMainhand;
|
||||
private FullEquipType _typeOffhand;
|
||||
private byte _states;
|
||||
|
||||
public DesignData()
|
||||
{}
|
||||
|
||||
public readonly StainId Stain(EquipSlot slot)
|
||||
{
|
||||
var index = slot.ToIndex();
|
||||
return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3];
|
||||
}
|
||||
|
||||
public readonly EquipItem Item(EquipSlot slot)
|
||||
=> slot.ToIndex() switch
|
||||
{
|
||||
// @formatter:off
|
||||
0 => new EquipItem(_nameHead, _itemIds[ 0], _iconIds[ 0], (SetId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (WeaponType)0, _equipmentBytes[ 2], FullEquipType.Head ),
|
||||
1 => new EquipItem(_nameBody, _itemIds[ 1], _iconIds[ 1], (SetId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (WeaponType)0, _equipmentBytes[ 6], FullEquipType.Body ),
|
||||
2 => new EquipItem(_nameHands, _itemIds[ 2], _iconIds[ 2], (SetId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (WeaponType)0, _equipmentBytes[10], FullEquipType.Hands ),
|
||||
3 => new EquipItem(_nameLegs, _itemIds[ 3], _iconIds[ 3], (SetId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (WeaponType)0, _equipmentBytes[14], FullEquipType.Legs ),
|
||||
4 => new EquipItem(_nameFeet, _itemIds[ 4], _iconIds[ 4], (SetId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (WeaponType)0, _equipmentBytes[18], FullEquipType.Feet ),
|
||||
5 => new EquipItem(_nameEars, _itemIds[ 5], _iconIds[ 5], (SetId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (WeaponType)0, _equipmentBytes[22], FullEquipType.Ears ),
|
||||
6 => new EquipItem(_nameNeck, _itemIds[ 6], _iconIds[ 6], (SetId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (WeaponType)0, _equipmentBytes[26], FullEquipType.Neck ),
|
||||
7 => new EquipItem(_nameWrists, _itemIds[ 7], _iconIds[ 7], (SetId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (WeaponType)0, _equipmentBytes[30], FullEquipType.Wrists ),
|
||||
8 => new EquipItem(_nameRFinger, _itemIds[ 8], _iconIds[ 8], (SetId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (WeaponType)0, _equipmentBytes[34], FullEquipType.Finger ),
|
||||
9 => new EquipItem(_nameLFinger, _itemIds[ 9], _iconIds[ 9], (SetId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (WeaponType)0, _equipmentBytes[38], FullEquipType.Finger ),
|
||||
10 => new EquipItem(_nameMainhand, _itemIds[10], _iconIds[10], (SetId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, _equipmentBytes[42], _typeMainhand ),
|
||||
11 => new EquipItem(_nameOffhand, _itemIds[11], _iconIds[11], (SetId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, _equipmentBytes[46], _typeOffhand ),
|
||||
_ => new EquipItem(),
|
||||
// @formatter:on
|
||||
};
|
||||
|
||||
public bool SetItem(EquipItem item)
|
||||
{
|
||||
var index = item.Type.ToSlot().ToIndex();
|
||||
if (index > 11 || _itemIds[index] == item.Id)
|
||||
return false;
|
||||
|
||||
_itemIds[index] = item.Id;
|
||||
_iconIds[index] = item.IconId;
|
||||
_equipmentBytes[4 * index + 0] = (byte)item.ModelId;
|
||||
_equipmentBytes[4 * index + 1] = (byte)(item.ModelId.Value >> 8);
|
||||
_equipmentBytes[4 * index + 2] = item.Variant;
|
||||
switch (index)
|
||||
{
|
||||
// @formatter:off
|
||||
case 0: _nameHead = item.Name; return true;
|
||||
case 1: _nameBody = item.Name; return true;
|
||||
case 2: _nameHands = item.Name; return true;
|
||||
case 3: _nameLegs = item.Name; return true;
|
||||
case 4: _nameFeet = item.Name; return true;
|
||||
case 5: _nameEars = item.Name; return true;
|
||||
case 6: _nameNeck = item.Name; return true;
|
||||
case 7: _nameWrists = item.Name; return true;
|
||||
case 8: _nameRFinger = item.Name; return true;
|
||||
case 9: _nameLFinger = item.Name; return true;
|
||||
// @formatter:on
|
||||
case 10:
|
||||
_nameMainhand = item.Name;
|
||||
_secondaryMainhand = item.WeaponType;
|
||||
_typeMainhand = item.Type;
|
||||
return true;
|
||||
case 11:
|
||||
_nameOffhand = item.Name;
|
||||
_secondaryOffhand = item.WeaponType;
|
||||
_typeOffhand = item.Type;
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetStain(EquipSlot slot, StainId stain)
|
||||
=> slot.ToIndex() switch
|
||||
{
|
||||
0 => SetIfDifferent(ref _equipmentBytes[3], stain.Value),
|
||||
1 => SetIfDifferent(ref _equipmentBytes[7], stain.Value),
|
||||
2 => SetIfDifferent(ref _equipmentBytes[11], stain.Value),
|
||||
3 => SetIfDifferent(ref _equipmentBytes[15], stain.Value),
|
||||
4 => SetIfDifferent(ref _equipmentBytes[19], stain.Value),
|
||||
5 => SetIfDifferent(ref _equipmentBytes[23], stain.Value),
|
||||
6 => SetIfDifferent(ref _equipmentBytes[27], stain.Value),
|
||||
7 => SetIfDifferent(ref _equipmentBytes[31], stain.Value),
|
||||
8 => SetIfDifferent(ref _equipmentBytes[35], stain.Value),
|
||||
9 => SetIfDifferent(ref _equipmentBytes[39], stain.Value),
|
||||
10 => SetIfDifferent(ref _equipmentBytes[43], stain.Value),
|
||||
11 => SetIfDifferent(ref _equipmentBytes[47], stain.Value),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public readonly bool IsWet()
|
||||
=> (_states & 0x01) == 0x01;
|
||||
|
||||
public bool SetIsWet(bool value)
|
||||
{
|
||||
if (value == IsWet())
|
||||
return false;
|
||||
|
||||
_states = (byte)(value ? _states | 0x01 : _states & ~0x01);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public readonly bool IsVisorToggled()
|
||||
=> (_states & 0x02) == 0x02;
|
||||
|
||||
public bool SetVisor(bool value)
|
||||
{
|
||||
if (value == IsVisorToggled())
|
||||
return false;
|
||||
|
||||
_states = (byte)(value ? _states | 0x02 : _states & ~0x02);
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly bool IsHatVisible()
|
||||
=> (_states & 0x04) == 0x04;
|
||||
|
||||
public bool SetHatVisible(bool value)
|
||||
{
|
||||
if (value == IsHatVisible())
|
||||
return false;
|
||||
|
||||
_states = (byte)(value ? _states | 0x04 : _states & ~0x04);
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly bool IsWeaponVisible()
|
||||
=> (_states & 0x08) == 0x09;
|
||||
|
||||
public bool SetWeaponVisible(bool value)
|
||||
{
|
||||
if (value == IsWeaponVisible())
|
||||
return false;
|
||||
|
||||
_states = (byte)(value ? _states | 0x08 : _states & ~0x08);
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool SetIfDifferent<T>(ref T old, T value) where T : IEquatable<T>
|
||||
{
|
||||
if (old.Equals(value))
|
||||
return false;
|
||||
|
||||
old = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
46
Glamourer/Designs/IDesign.cs
Normal file
46
Glamourer/Designs/IDesign.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using Glamourer.Customization;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Glamourer.Designs;
|
||||
|
||||
public interface IDesign
|
||||
{
|
||||
public uint GetModelId();
|
||||
public bool SetModelId(uint modelId);
|
||||
|
||||
public EquipItem GetEquipItem(EquipSlot slot);
|
||||
public bool SetEquipItem(EquipItem item);
|
||||
|
||||
public StainId GetStain(EquipSlot slot);
|
||||
public bool SetStain(EquipSlot slot, StainId stain);
|
||||
|
||||
public CustomizeValue GetCustomizeValue(CustomizeIndex type);
|
||||
public bool SetCustomizeValue(CustomizeIndex type);
|
||||
|
||||
public bool DoApplyEquip(EquipSlot slot);
|
||||
public bool DoApplyStain(EquipSlot slot);
|
||||
public bool DoApplyCustomize(CustomizeIndex index);
|
||||
|
||||
public bool SetApplyEquip(EquipSlot slot, bool value);
|
||||
public bool SetApplyStain(EquipSlot slot, bool value);
|
||||
public bool SetApplyCustomize(CustomizeIndex slot, bool value);
|
||||
|
||||
public bool IsWet();
|
||||
public bool SetIsWet(bool value);
|
||||
|
||||
public bool IsHatVisible();
|
||||
public bool DoApplyHatVisible();
|
||||
public bool SetHatVisible(bool value);
|
||||
public bool SetApplyHatVisible(bool value);
|
||||
|
||||
public bool IsVisorToggled();
|
||||
public bool DoApplyVisorToggle();
|
||||
public bool SetVisorToggle(bool value);
|
||||
public bool SetApplyVisorToggle(bool value);
|
||||
|
||||
public bool IsWeaponVisible();
|
||||
public bool DoApplyWeaponVisible();
|
||||
public bool SetWeaponVisible(bool value);
|
||||
public bool SetApplyWeaponVisible(bool value);
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ using OtterGui.Log;
|
|||
|
||||
namespace Glamourer;
|
||||
|
||||
public class Item : IDalamudPlugin
|
||||
public class Glamourer : IDalamudPlugin
|
||||
{
|
||||
public string Name
|
||||
=> "Glamourer";
|
||||
|
|
@ -26,7 +26,7 @@ public class Item : IDalamudPlugin
|
|||
|
||||
private readonly ServiceProvider _services;
|
||||
|
||||
public Item(DalamudPluginInterface pluginInterface)
|
||||
public Glamourer(DalamudPluginInterface pluginInterface)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -120,10 +120,6 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Designs\" />
|
||||
</ItemGroup>
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll, $(TargetDir)Penumbra.Api.dll, $(TargetDir)Penumbra.String.dll $(SolutionDir)$(SolutionName).zip" />
|
||||
<Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
|
||||
|
|
|
|||
34
Glamourer/Gui/Colors.cs
Normal file
34
Glamourer/Gui/Colors.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Glamourer.Gui;
|
||||
|
||||
public enum ColorId
|
||||
{
|
||||
CustomizationDesign,
|
||||
StateDesign,
|
||||
EquipmentDesign,
|
||||
}
|
||||
|
||||
public static class Colors
|
||||
{
|
||||
public static (uint DefaultColor, string Name, string Description) Data(this ColorId color)
|
||||
=> color switch
|
||||
{
|
||||
// @formatter:off
|
||||
ColorId.CustomizationDesign => (0xFFC000C0, "Customization Design", "A design that only changes customizations on a character." ),
|
||||
ColorId.StateDesign => (0xFF00C0C0, "State Design", "A design that only changes meta state on a character." ),
|
||||
ColorId.EquipmentDesign => (0xFF00C000, "Equipment Design", "A design that only changes equipment on a character." ),
|
||||
_ => (0x00000000, string.Empty, string.Empty ),
|
||||
// @formatter:on
|
||||
};
|
||||
|
||||
private static IReadOnlyDictionary<ColorId, uint> _colors = new Dictionary<ColorId, uint>();
|
||||
|
||||
/// <summary> Obtain the configured value for a color. </summary>
|
||||
public static uint Value(this ColorId color)
|
||||
=> _colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor;
|
||||
|
||||
/// <summary> Set the configurable colors dictionary to a value. </summary>
|
||||
public static void SetColors(Configuration config)
|
||||
=> _colors = config.Colors;
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ public class MainWindow : Window
|
|||
}
|
||||
|
||||
private static string GetLabel()
|
||||
=> Item.Version.Length == 0
|
||||
=> Glamourer.Version.Length == 0
|
||||
? "Glamourer###GlamourerMainWindow"
|
||||
: $"Glamourer v{Item.Version}###GlamourerMainWindow";
|
||||
: $"Glamourer v{Glamourer.Version}###GlamourerMainWindow";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Interface;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using Glamourer.Customization;
|
||||
using Glamourer.Designs;
|
||||
using Glamourer.Interop;
|
||||
using Glamourer.Interop.Penumbra;
|
||||
using Glamourer.Interop.Structs;
|
||||
|
|
@ -17,6 +19,7 @@ using OtterGui.Widgets;
|
|||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using static OtterGui.Raii.ImRaii;
|
||||
|
||||
namespace Glamourer.Gui.Tabs;
|
||||
|
||||
|
|
@ -28,6 +31,7 @@ public unsafe class DebugTab : ITab
|
|||
private readonly WeaponService _weaponService;
|
||||
private readonly PenumbraService _penumbra;
|
||||
private readonly ObjectTable _objects;
|
||||
private readonly ObjectManager _objectManager;
|
||||
|
||||
private readonly ItemManager _items;
|
||||
private readonly ActorService _actors;
|
||||
|
|
@ -37,7 +41,7 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, ObjectTable objects,
|
||||
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, IdentifierService identifier,
|
||||
ActorService actors, ItemManager items, CustomizationService customization)
|
||||
ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager)
|
||||
{
|
||||
_changeCustomizeService = changeCustomizeService;
|
||||
_visorService = visorService;
|
||||
|
|
@ -48,6 +52,7 @@ public unsafe class DebugTab : ITab
|
|||
_actors = actors;
|
||||
_items = items;
|
||||
_customization = customization;
|
||||
_objectManager = objectManager;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
|
|
@ -58,6 +63,7 @@ public unsafe class DebugTab : ITab
|
|||
DrawInteropHeader();
|
||||
DrawGameDataHeader();
|
||||
DrawPenumbraHeader();
|
||||
DrawDesignManager();
|
||||
}
|
||||
|
||||
#region Interop
|
||||
|
|
@ -67,10 +73,20 @@ public unsafe class DebugTab : ITab
|
|||
if (!ImGui.CollapsingHeader("Interop"))
|
||||
return;
|
||||
|
||||
DrawModelEvaluation();
|
||||
DrawObjectManager();
|
||||
}
|
||||
|
||||
private void DrawModelEvaluation()
|
||||
{
|
||||
using var tree = TreeNode("Model Evaluation");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0);
|
||||
var actor = (Actor)_objects.GetObjectAddress(_gameObjectIndex);
|
||||
var model = actor.Model;
|
||||
using var table = ImRaii.Table("##interopTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||
using var table = Table("##evaluationTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader("Actor");
|
||||
|
|
@ -107,9 +123,79 @@ public unsafe class DebugTab : ITab
|
|||
DrawCustomize(actor, model);
|
||||
}
|
||||
|
||||
private string _objectFilter = string.Empty;
|
||||
|
||||
private void DrawObjectManager()
|
||||
{
|
||||
using var tree = TreeNode("Object Manager");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
_objectManager.Update();
|
||||
|
||||
using (var table = Table("##data", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
|
||||
{
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
ImGuiUtil.DrawTableColumn("Last Update");
|
||||
ImGuiUtil.DrawTableColumn(_objectManager.LastUpdate.ToString(CultureInfo.InvariantCulture));
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
ImGuiUtil.DrawTableColumn("World");
|
||||
ImGuiUtil.DrawTableColumn(_actors.Valid ? _actors.AwaitedService.Data.ToWorldName(_objectManager.World) : "Service Missing");
|
||||
ImGuiUtil.DrawTableColumn(_objectManager.World.ToString());
|
||||
|
||||
ImGuiUtil.DrawTableColumn("Player Character");
|
||||
ImGuiUtil.DrawTableColumn($"{_objectManager.Player.Utf8Name} ({_objectManager.Player.Index})");
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiUtil.CopyOnClickSelectable(_objectManager.Player.ToString());
|
||||
|
||||
ImGuiUtil.DrawTableColumn("In GPose");
|
||||
ImGuiUtil.DrawTableColumn(_objectManager.IsInGPose.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
if (_objectManager.IsInGPose)
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn("GPose Player");
|
||||
ImGuiUtil.DrawTableColumn($"{_objectManager.GPosePlayer.Utf8Name} ({_objectManager.GPosePlayer.Index})");
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiUtil.CopyOnClickSelectable(_objectManager.GPosePlayer.ToString());
|
||||
}
|
||||
|
||||
ImGuiUtil.DrawTableColumn("Number of Players");
|
||||
ImGuiUtil.DrawTableColumn(_objectManager.Count.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
|
||||
var filterChanged = ImGui.InputTextWithHint("##Filter", "Filter...", ref _objectFilter, 64);
|
||||
using var table2 = Table("##data2", 3,
|
||||
ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.ScrollY,
|
||||
new Vector2(-1, 20 * ImGui.GetTextLineHeightWithSpacing()));
|
||||
if (!table2)
|
||||
return;
|
||||
|
||||
if (filterChanged)
|
||||
ImGui.SetScrollY(0);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing());
|
||||
ImGui.TableNextRow();
|
||||
|
||||
var remainder = ImGuiClip.FilteredClippedDraw(_objectManager, skips,
|
||||
p => p.Value.Label.Contains(_objectFilter, StringComparison.OrdinalIgnoreCase), p
|
||||
=>
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn(p.Key.ToString());
|
||||
ImGuiUtil.DrawTableColumn(p.Value.Label);
|
||||
ImGuiUtil.DrawTableColumn(string.Join(", ", p.Value.Objects.OrderBy(a => a.Index).Select(a => a.Index.ToString())));
|
||||
});
|
||||
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeightWithSpacing());
|
||||
}
|
||||
|
||||
private void DrawVisor(Actor actor, Model model)
|
||||
{
|
||||
using var id = ImRaii.PushId("Visor");
|
||||
using var id = PushId("Visor");
|
||||
ImGuiUtil.DrawTableColumn("Visor State");
|
||||
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->DrawData.IsVisorToggled.ToString() : "No Character");
|
||||
ImGuiUtil.DrawTableColumn(model.IsHuman ? _visorService.GetVisorState(model).ToString() : "No Human");
|
||||
|
|
@ -129,7 +215,7 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawHatState(Actor actor, Model model)
|
||||
{
|
||||
using var id = ImRaii.PushId("HatState");
|
||||
using var id = PushId("HatState");
|
||||
ImGuiUtil.DrawTableColumn("Hat State");
|
||||
ImGuiUtil.DrawTableColumn(actor.IsCharacter
|
||||
? actor.AsCharacter->DrawData.IsHatHidden ? "Hidden" : actor.GetArmor(EquipSlot.Head).ToString()
|
||||
|
|
@ -154,7 +240,7 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawWeaponState(Actor actor, Model model)
|
||||
{
|
||||
using var id = ImRaii.PushId("WeaponState");
|
||||
using var id = PushId("WeaponState");
|
||||
ImGuiUtil.DrawTableColumn("Weapon State");
|
||||
ImGuiUtil.DrawTableColumn(actor.IsCharacter
|
||||
? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible"
|
||||
|
|
@ -186,7 +272,7 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawWetness(Actor actor, Model model)
|
||||
{
|
||||
using var id = ImRaii.PushId("Wetness");
|
||||
using var id = PushId("Wetness");
|
||||
ImGuiUtil.DrawTableColumn("Wetness");
|
||||
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.AsCharacter->IsGPoseWet ? "GPose" : "None" : "No Character");
|
||||
var modelString = model.IsCharacterBase
|
||||
|
|
@ -212,10 +298,10 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawEquip(Actor actor, Model model)
|
||||
{
|
||||
using var id = ImRaii.PushId("Equipment");
|
||||
using var id = PushId("Equipment");
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||
{
|
||||
using var id2 = ImRaii.PushId((int)slot);
|
||||
using var id2 = PushId((int)slot);
|
||||
ImGuiUtil.DrawTableColumn(slot.ToName());
|
||||
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetArmor(slot).ToString() : "No Character");
|
||||
ImGuiUtil.DrawTableColumn(model.IsHuman ? model.GetArmor(slot).ToString() : "No Human");
|
||||
|
|
@ -237,7 +323,7 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawCustomize(Actor actor, Model model)
|
||||
{
|
||||
using var id = ImRaii.PushId("Customize");
|
||||
using var id = PushId("Customize");
|
||||
var actorCustomize = new Customize(actor.IsCharacter
|
||||
? *(Penumbra.GameData.Structs.CustomizeData*)&actor.AsCharacter->DrawData.CustomizeData
|
||||
: new Penumbra.GameData.Structs.CustomizeData());
|
||||
|
|
@ -246,7 +332,7 @@ public unsafe class DebugTab : ITab
|
|||
: new Penumbra.GameData.Structs.CustomizeData());
|
||||
foreach (var type in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
using var id2 = ImRaii.PushId((int)type);
|
||||
using var id2 = PushId((int)type);
|
||||
ImGuiUtil.DrawTableColumn(type.ToDefaultName());
|
||||
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actorCustomize[type].Value.ToString("X2") : "No Character");
|
||||
ImGuiUtil.DrawTableColumn(model.IsHuman ? modelCustomize[type].Value.ToString("X2") : "No Human");
|
||||
|
|
@ -287,7 +373,7 @@ public unsafe class DebugTab : ITab
|
|||
if (!ImGui.CollapsingHeader("Penumbra"))
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||
using var table = Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
|
|
@ -324,7 +410,7 @@ public unsafe class DebugTab : ITab
|
|||
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0);
|
||||
ImGui.TableNextColumn();
|
||||
using (var disabled = ImRaii.Disabled(!_penumbra.Available))
|
||||
using (var disabled = Disabled(!_penumbra.Available))
|
||||
{
|
||||
if (ImGui.SmallButton("Redraw"))
|
||||
_penumbra.RedrawObject(_objects.GetObjectAddress(_gameObjectIndex), RedrawType.Redraw);
|
||||
|
|
@ -355,8 +441,8 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawIdentifierService()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled(!_items.IdentifierService.Valid);
|
||||
using var tree = ImRaii.TreeNode("Identifier Service");
|
||||
using var disabled = Disabled(!_items.IdentifierService.Valid);
|
||||
using var tree = TreeNode("Identifier Service");
|
||||
if (!tree || !_items.IdentifierService.Valid)
|
||||
return;
|
||||
|
||||
|
|
@ -400,7 +486,7 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawRestrictedGear()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode("Restricted Gear Service");
|
||||
using var tree = TreeNode("Restricted Gear Service");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
|
|
@ -451,8 +537,8 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawActorService()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled(!_actors.Valid);
|
||||
using var tree = ImRaii.TreeNode("Actor Service");
|
||||
using var disabled = Disabled(!_actors.Valid);
|
||||
using var tree = TreeNode("Actor Service");
|
||||
if (!tree || !_actors.Valid)
|
||||
return;
|
||||
|
||||
|
|
@ -468,14 +554,14 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private static void DrawNameTable(string label, ref string filter, IEnumerable<(uint, string)> names)
|
||||
{
|
||||
using var _ = ImRaii.PushId(label);
|
||||
using var tree = ImRaii.TreeNode(label);
|
||||
using var _ = PushId(label);
|
||||
using var tree = TreeNode(label);
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref filter, 256);
|
||||
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
|
||||
using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter,
|
||||
using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter,
|
||||
new Vector2(-1, 10 * height));
|
||||
if (!table)
|
||||
return;
|
||||
|
|
@ -502,13 +588,13 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawItemService()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled(!_items.ItemService.Valid);
|
||||
using var tree = ImRaii.TreeNode("Item Manager");
|
||||
using var disabled = Disabled(!_items.ItemService.Valid);
|
||||
using var tree = TreeNode("Item Manager");
|
||||
if (!tree || !_items.ItemService.Valid)
|
||||
return;
|
||||
|
||||
disabled.Dispose();
|
||||
ImRaii.TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})",
|
||||
TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.Id}) ({_items.DefaultSword.Weapon()})",
|
||||
ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
DrawNameTable("All Items (Main)", ref _itemFilter,
|
||||
_items.ItemService.AwaitedService.AllItems(true).Select(p => (p.Item1,
|
||||
|
|
@ -530,13 +616,13 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawStainService()
|
||||
{
|
||||
using var tree = ImRaii.TreeNode("Stain Service");
|
||||
using var tree = TreeNode("Stain Service");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _stainFilter, 256);
|
||||
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
|
||||
using var table = ImRaii.Table("##table", 4,
|
||||
using var table = Table("##table", 4,
|
||||
ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit,
|
||||
new Vector2(-1, 10 * height));
|
||||
if (!table)
|
||||
|
|
@ -566,8 +652,8 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawCustomizationService()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled(!_customization.Valid);
|
||||
using var tree = ImRaii.TreeNode("Customization Service");
|
||||
using var disabled = Disabled(!_customization.Valid);
|
||||
using var tree = TreeNode("Customization Service");
|
||||
if (!tree || !_customization.Valid)
|
||||
return;
|
||||
|
||||
|
|
@ -582,11 +668,11 @@ public unsafe class DebugTab : ITab
|
|||
|
||||
private void DrawCustomizationInfo(CustomizationSet set)
|
||||
{
|
||||
using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}");
|
||||
using var tree = TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}");
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||
using var table = Table("data", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
|
|
@ -601,4 +687,140 @@ public unsafe class DebugTab : ITab
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Designs
|
||||
|
||||
private string _base64 = string.Empty;
|
||||
private string _restore = string.Empty;
|
||||
private byte[] _base64Bytes = Array.Empty<byte>();
|
||||
private byte[] _restoreBytes = Array.Empty<byte>();
|
||||
private DesignData _parse64 = new();
|
||||
private Exception? _parse64Failure;
|
||||
|
||||
private void DrawDesignManager()
|
||||
{
|
||||
if (!ImGui.CollapsingHeader("Designs"))
|
||||
return;
|
||||
|
||||
ImGui.InputTextWithHint("##base64", "Base 64 input...", ref _base64, 2048);
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
try
|
||||
{
|
||||
_base64Bytes = Convert.FromBase64String(_base64);
|
||||
_parse64Failure = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_base64Bytes = Array.Empty<byte>();
|
||||
_parse64Failure = ex;
|
||||
}
|
||||
|
||||
if (_parse64Failure == null)
|
||||
try
|
||||
{
|
||||
_parse64 = DesignBase64Migration.MigrateBase64(_items, _base64, out var ef, out var cf, out var wp, out var ah, out var av,
|
||||
out var aw);
|
||||
_restore = DesignBase64Migration.CreateOldBase64(in _parse64, ef, cf, ah, av, wp, aw);
|
||||
_restoreBytes = Convert.FromBase64String(_restore);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_parse64Failure = ex;
|
||||
_restore = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
if (_parse64Failure != null)
|
||||
{
|
||||
ImGuiUtil.TextWrapped(_parse64Failure.ToString());
|
||||
}
|
||||
else if (_restore.Length > 0)
|
||||
{
|
||||
DrawDesignData(_parse64);
|
||||
using var font = PushFont(UiBuilder.MonoFont);
|
||||
ImGui.TextUnformatted(_base64);
|
||||
using (var style = PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 }))
|
||||
{
|
||||
foreach (var (c1, c2) in _restore.Zip(_base64))
|
||||
{
|
||||
using var color = PushColor(ImGuiCol.Text, 0xFF4040D0, c1 != c2);
|
||||
ImGui.TextUnformatted(c1.ToString());
|
||||
ImGui.SameLine();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
|
||||
foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex())
|
||||
{
|
||||
using (var group = Group())
|
||||
{
|
||||
ImGui.TextUnformatted(idx.ToString("D2"));
|
||||
ImGui.TextUnformatted(b1.ToString("X2"));
|
||||
using var color = PushColor(ImGuiCol.Text, 0xFF4040D0, b1 != b2);
|
||||
ImGui.TextUnformatted(b2.ToString("X2"));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
}
|
||||
|
||||
if (_parse64Failure != null && _base64Bytes.Length > 0)
|
||||
{
|
||||
using var font = PushFont(UiBuilder.MonoFont);
|
||||
foreach (var (b, idx) in _base64Bytes.WithIndex())
|
||||
{
|
||||
using (var group = Group())
|
||||
{
|
||||
ImGui.TextUnformatted(idx.ToString("D2"));
|
||||
ImGui.TextUnformatted(b.ToString("X2"));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawDesignData(in DesignData data)
|
||||
{
|
||||
using var table = Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
|
||||
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
|
||||
{
|
||||
var item = data.Item(slot);
|
||||
var stain = data.Stain(slot);
|
||||
ImGuiUtil.DrawTableColumn(slot.ToName());
|
||||
ImGuiUtil.DrawTableColumn(item.Name);
|
||||
ImGuiUtil.DrawTableColumn(item.Id.ToString());
|
||||
ImGuiUtil.DrawTableColumn(stain.ToString());
|
||||
}
|
||||
|
||||
ImGuiUtil.DrawTableColumn("Hat Visible");
|
||||
ImGuiUtil.DrawTableColumn(data.IsHatVisible().ToString());
|
||||
ImGui.TableNextRow();
|
||||
ImGuiUtil.DrawTableColumn("Visor Toggled");
|
||||
ImGuiUtil.DrawTableColumn(data.IsVisorToggled().ToString());
|
||||
ImGui.TableNextRow();
|
||||
ImGuiUtil.DrawTableColumn("Weapon Visible");
|
||||
ImGuiUtil.DrawTableColumn(data.IsWeaponVisible().ToString());
|
||||
ImGui.TableNextRow();
|
||||
|
||||
ImGuiUtil.DrawTableColumn("Model ID");
|
||||
ImGuiUtil.DrawTableColumn(data.ModelId.ToString());
|
||||
ImGui.TableNextRow();
|
||||
|
||||
foreach (var index in Enum.GetValues<CustomizeIndex>())
|
||||
{
|
||||
var value = data.Customize[index];
|
||||
ImGuiUtil.DrawTableColumn(index.ToDefaultName());
|
||||
ImGuiUtil.DrawTableColumn(value.Value.ToString());
|
||||
ImGui.TableNextRow();
|
||||
}
|
||||
|
||||
ImGuiUtil.DrawTableColumn("Is Wet");
|
||||
ImGuiUtil.DrawTableColumn(data.IsWet().ToString());
|
||||
ImGui.TableNextRow();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public unsafe class ChangeCustomizeService
|
|||
if (!model.IsHuman)
|
||||
return false;
|
||||
|
||||
Item.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}.");
|
||||
Glamourer.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}.");
|
||||
return _changeCustomize(model.AsHuman, customize.Data, 1);
|
||||
}
|
||||
|
||||
|
|
|
|||
139
Glamourer/Interop/ObjectManager.cs
Normal file
139
Glamourer/Interop/ObjectManager.cs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Glamourer.Interop.Structs;
|
||||
using Glamourer.Services;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
||||
namespace Glamourer.Interop;
|
||||
|
||||
public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
|
||||
{
|
||||
private readonly Framework _framework;
|
||||
private readonly ClientState _clientState;
|
||||
private readonly ObjectTable _objects;
|
||||
private readonly ActorService _actors;
|
||||
|
||||
public ObjectManager(Framework framework, ClientState clientState, ObjectTable objects, ActorService actors)
|
||||
{
|
||||
_framework = framework;
|
||||
_clientState = clientState;
|
||||
_objects = objects;
|
||||
_actors = actors;
|
||||
}
|
||||
|
||||
public DateTime LastUpdate { get; private set; }
|
||||
|
||||
public bool IsInGPose { get; private set; }
|
||||
public ushort World { get; private set; }
|
||||
|
||||
private readonly Dictionary<ActorIdentifier, ActorData> _identifiers = new(200);
|
||||
|
||||
public IReadOnlyDictionary<ActorIdentifier, ActorData> Identifiers
|
||||
=> _identifiers;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
var lastUpdate = _framework.LastUpdate;
|
||||
if (lastUpdate <= LastUpdate)
|
||||
return;
|
||||
|
||||
LastUpdate = lastUpdate;
|
||||
World = (ushort)(_clientState.LocalPlayer?.CurrentWorld.Id ?? 0u);
|
||||
_identifiers.Clear();
|
||||
|
||||
for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i)
|
||||
{
|
||||
Actor character = _objects.GetObjectAddress(i);
|
||||
if (character.Identifier(_actors.AwaitedService, out var identifier))
|
||||
HandleIdentifier(identifier, character);
|
||||
}
|
||||
|
||||
for (var i = (int)ScreenActor.CutsceneStart; i < (int)ScreenActor.CutsceneEnd; ++i)
|
||||
{
|
||||
Actor character = _objects.GetObjectAddress(i);
|
||||
if (!character.Valid)
|
||||
break;
|
||||
|
||||
HandleIdentifier(character.GetIdentifier(_actors.AwaitedService), character);
|
||||
}
|
||||
|
||||
void AddSpecial(ScreenActor idx, string label)
|
||||
{
|
||||
Actor actor = _objects.GetObjectAddress((int)idx);
|
||||
if (actor.Identifier(_actors.AwaitedService, out var ident))
|
||||
{
|
||||
var data = new ActorData(actor, label);
|
||||
_identifiers.Add(ident, data);
|
||||
}
|
||||
}
|
||||
|
||||
AddSpecial(ScreenActor.CharacterScreen, "Character Screen Actor");
|
||||
AddSpecial(ScreenActor.ExamineScreen, "Examine Screen Actor");
|
||||
AddSpecial(ScreenActor.FittingRoom, "Fitting Room Actor");
|
||||
AddSpecial(ScreenActor.DyePreview, "Dye Preview Actor");
|
||||
AddSpecial(ScreenActor.Portrait, "Portrait Actor");
|
||||
AddSpecial(ScreenActor.Card6, "Card Actor 6");
|
||||
AddSpecial(ScreenActor.Card7, "Card Actor 7");
|
||||
AddSpecial(ScreenActor.Card8, "Card Actor 8");
|
||||
|
||||
for (var i = (int)ScreenActor.ScreenEnd; i < _objects.Length; ++i)
|
||||
{
|
||||
Actor character = _objects.GetObjectAddress(i);
|
||||
if (character.Identifier(_actors.AwaitedService, out var identifier))
|
||||
HandleIdentifier(identifier, character);
|
||||
}
|
||||
|
||||
var gPose = GPosePlayer;
|
||||
IsInGPose = gPose.Utf8Name.Length > 0;
|
||||
}
|
||||
|
||||
private void HandleIdentifier(ActorIdentifier identifier, Actor character)
|
||||
{
|
||||
if (!character.Model || !identifier.IsValid)
|
||||
return;
|
||||
|
||||
if (!_identifiers.TryGetValue(identifier, out var data))
|
||||
{
|
||||
data = new ActorData(character, identifier.ToString());
|
||||
_identifiers[identifier] = data;
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Objects.Add(character);
|
||||
}
|
||||
}
|
||||
|
||||
public Actor GPosePlayer
|
||||
=> _objects.GetObjectAddress((int)ScreenActor.GPosePlayer);
|
||||
|
||||
public Actor Player
|
||||
=> _objects.GetObjectAddress(0);
|
||||
|
||||
public IEnumerator<KeyValuePair<ActorIdentifier, ActorData>> GetEnumerator()
|
||||
=> Identifiers.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count
|
||||
=> Identifiers.Count;
|
||||
|
||||
public bool ContainsKey(ActorIdentifier key)
|
||||
=> Identifiers.ContainsKey(key);
|
||||
|
||||
public bool TryGetValue(ActorIdentifier key, out ActorData value)
|
||||
=> Identifiers.TryGetValue(key, out value);
|
||||
|
||||
public ActorData this[ActorIdentifier key]
|
||||
=> Identifiers[key];
|
||||
|
||||
public IEnumerable<ActorIdentifier> Keys
|
||||
=> Identifiers.Keys;
|
||||
|
||||
public IEnumerable<ActorData> Values
|
||||
=> Identifiers.Values;
|
||||
}
|
||||
|
|
@ -107,11 +107,11 @@ public unsafe class PenumbraService : IDisposable
|
|||
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
|
||||
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
|
||||
Available = true;
|
||||
Item.Log.Debug("Glamourer attached to Penumbra.");
|
||||
Glamourer.Log.Debug("Glamourer attached to Penumbra.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Item.Log.Debug($"Could not attach to Penumbra:\n{e}");
|
||||
Glamourer.Log.Debug($"Could not attach to Penumbra:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ public unsafe class PenumbraService : IDisposable
|
|||
if (Available)
|
||||
{
|
||||
Available = false;
|
||||
Item.Log.Debug("Glamourer detached from Penumbra.");
|
||||
Glamourer.Log.Debug("Glamourer detached from Penumbra.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
using System;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Glamourer.Interop.Structs;
|
||||
|
||||
|
|
@ -43,6 +45,9 @@ public readonly unsafe struct Actor : IEquatable<Actor>
|
|||
public ActorIdentifier GetIdentifier(ActorManager actors)
|
||||
=> actors.FromObject(AsObject, out _, true, true, false);
|
||||
|
||||
public ByteString Utf8Name
|
||||
=> Valid ? new ByteString(AsObject->Name) : ByteString.Empty;
|
||||
|
||||
public bool Identifier(ActorManager actors, out ActorIdentifier ident)
|
||||
{
|
||||
if (Valid)
|
||||
|
|
@ -55,6 +60,9 @@ public readonly unsafe struct Actor : IEquatable<Actor>
|
|||
return false;
|
||||
}
|
||||
|
||||
public int Index
|
||||
=> Valid ? AsObject->ObjectIndex : -1;
|
||||
|
||||
public Model Model
|
||||
=> Valid ? AsObject->DrawObject : null;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
namespace Glamourer.Interop.Structs;
|
||||
|
||||
/// <summary>
|
||||
/// A single actor with its label and the list of associated game objects.
|
||||
/// </summary>
|
||||
public readonly struct ActorData
|
||||
{
|
||||
public readonly List<Actor> Objects;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public class VisorService : IDisposable
|
|||
return false;
|
||||
|
||||
var oldState = GetVisorState(human);
|
||||
Item.Log.Verbose($"[SetVisorState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}.");
|
||||
Glamourer.Log.Verbose($"[SetVisorState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}.");
|
||||
if (oldState == on)
|
||||
return false;
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ public class VisorService : IDisposable
|
|||
// and also control whether the function should be called at all.
|
||||
Event.Invoke(human, ref on, ref callOriginal);
|
||||
|
||||
Item.Log.Excessive(
|
||||
Glamourer.Log.Excessive(
|
||||
$"[SetVisorState] Invoked from game on 0x{human:X} switching to {on} (original {originalOn}, call original {callOriginal}).");
|
||||
|
||||
if (callOriginal)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public unsafe class WeaponService : IDisposable
|
|||
|
||||
// First call the regular function.
|
||||
_loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4);
|
||||
Item.Log.Information(
|
||||
Glamourer.Log.Excessive(
|
||||
$"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
|
||||
}
|
||||
|
||||
|
|
|
|||
56
Glamourer/Services/ConfigMigrationService.cs
Normal file
56
Glamourer/Services/ConfigMigrationService.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Glamourer.Gui;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Glamourer.Services;
|
||||
|
||||
public class ConfigMigrationService
|
||||
{
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
private Configuration _config = null!;
|
||||
private JObject _data = null!;
|
||||
|
||||
public ConfigMigrationService(SaveService saveService)
|
||||
=> _saveService = saveService;
|
||||
|
||||
public void Migrate(Configuration config)
|
||||
{
|
||||
_config = config;
|
||||
if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_saveService.FileNames.ConfigFile))
|
||||
{
|
||||
AddColors(config, false);
|
||||
return;
|
||||
}
|
||||
|
||||
_data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile));
|
||||
MigrateV1To2();
|
||||
AddColors(config, true);
|
||||
}
|
||||
|
||||
private void MigrateV1To2()
|
||||
{
|
||||
if (_config.Version > 1)
|
||||
return;
|
||||
|
||||
_config.Version = 2;
|
||||
var customizationColor = _data["CustomizationColor"]?.ToObject<uint>() ?? ColorId.CustomizationDesign.Data().DefaultColor;
|
||||
_config.Colors[ColorId.CustomizationDesign] = customizationColor;
|
||||
var stateColor = _data["StateColor"]?.ToObject<uint>() ?? ColorId.StateDesign.Data().DefaultColor;
|
||||
_config.Colors[ColorId.StateDesign] = stateColor;
|
||||
var equipmentColor = _data["EquipmentColor"]?.ToObject<uint>() ?? ColorId.EquipmentDesign.Data().DefaultColor;
|
||||
_config.Colors[ColorId.EquipmentDesign] = equipmentColor;
|
||||
}
|
||||
|
||||
private static void AddColors(Configuration config, bool forceSave)
|
||||
{
|
||||
var save = false;
|
||||
foreach (var color in Enum.GetValues<ColorId>())
|
||||
save |= config.Colors.TryAdd(color, color.Data().DefaultColor);
|
||||
|
||||
if (save || forceSave)
|
||||
config.Save();
|
||||
Colors.SetColors(config);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Dalamud.Plugin;
|
||||
using Glamourer.Designs;
|
||||
|
||||
namespace Glamourer.Services;
|
||||
|
||||
|
|
@ -32,4 +33,7 @@ public class FilenameService
|
|||
|
||||
public string DesignFile(string identifier)
|
||||
=> Path.Combine(DesignDirectory, $"{identifier}.json");
|
||||
|
||||
public string DesignFile(Design design)
|
||||
=> DesignFile(design.Identifier.ToString());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ public class ItemManager : IDisposable
|
|||
public const string SmallClothesNpc = "Smallclothes (NPC)";
|
||||
public const ushort SmallClothesNpcModel = 9903;
|
||||
|
||||
private readonly Configuration _config;
|
||||
|
||||
public readonly IdentifierService IdentifierService;
|
||||
public readonly ExcelSheet<Lumina.Excel.GeneratedSheets.Item> ItemSheet;
|
||||
public readonly StainData Stains;
|
||||
|
|
@ -24,8 +26,10 @@ public class ItemManager : IDisposable
|
|||
|
||||
public readonly EquipItem DefaultSword;
|
||||
|
||||
public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService)
|
||||
public ItemManager(Configuration config, DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService,
|
||||
ItemService itemService)
|
||||
{
|
||||
_config = config;
|
||||
ItemSheet = gameData.GetExcelSheet<Lumina.Excel.GeneratedSheets.Item>()!;
|
||||
IdentifierService = identifierService;
|
||||
Stains = new StainData(pi, gameData, gameData.Language);
|
||||
|
|
@ -42,10 +46,8 @@ public class ItemManager : IDisposable
|
|||
|
||||
|
||||
public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender)
|
||||
// TODO
|
||||
//if (_config.UseRestrictedGearProtection)
|
||||
=> RestrictedGear.ResolveRestricted(armor, slot, race, gender);
|
||||
//return (false, armor);
|
||||
=> _config.UseRestrictedGearProtection ? RestrictedGear.ResolveRestricted(armor, slot, race, gender) : (false, armor);
|
||||
|
||||
|
||||
public static uint NothingId(EquipSlot slot)
|
||||
=> uint.MaxValue - 128 - (uint)slot.ToSlot();
|
||||
|
|
@ -82,6 +84,20 @@ public class ItemManager : IDisposable
|
|||
return item;
|
||||
}
|
||||
|
||||
public EquipItem Resolve(FullEquipType type, uint itemId)
|
||||
{
|
||||
if (itemId == NothingId(type))
|
||||
return NothingItem(type);
|
||||
|
||||
if (!ItemService.AwaitedService.TryGetValue(itemId, false, out var item))
|
||||
return new EquipItem(string.Intern($"Unknown #{itemId}"), itemId, 0, 0, 0, 0, 0);
|
||||
|
||||
if (item.Type != type)
|
||||
return new EquipItem(string.Intern($"Invalid #{itemId}"), itemId, item.IconId, item.ModelId, item.WeaponType, item.Variant, 0);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public EquipItem Identify(EquipSlot slot, SetId id, byte variant)
|
||||
{
|
||||
slot = slot.ToSlot();
|
||||
|
|
|
|||
17
Glamourer/Services/SaveService.cs
Normal file
17
Glamourer/Services/SaveService.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
|
||||
namespace Glamourer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Any file type that we want to save via SaveService.
|
||||
/// </summary>
|
||||
public interface ISavable : ISavable<FilenameService>
|
||||
{ }
|
||||
|
||||
public sealed class SaveService : SaveServiceBase<FilenameService>
|
||||
{
|
||||
public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames)
|
||||
: base(log, framework, fileNames)
|
||||
{ }
|
||||
}
|
||||
|
|
@ -36,7 +36,11 @@ public static class ServiceManager
|
|||
private static IServiceCollection AddMeta(this IServiceCollection services)
|
||||
=> services.AddSingleton<ChatService>()
|
||||
.AddSingleton<FilenameService>()
|
||||
.AddSingleton<BackupService>();
|
||||
.AddSingleton<BackupService>()
|
||||
.AddSingleton<FrameworkManager>()
|
||||
.AddSingleton<SaveService>()
|
||||
.AddSingleton<ConfigMigrationService>()
|
||||
.AddSingleton<Configuration>();
|
||||
|
||||
private static IServiceCollection AddEvents(this IServiceCollection services)
|
||||
=> services.AddSingleton<VisorStateChanged>()
|
||||
|
|
@ -54,11 +58,11 @@ public static class ServiceManager
|
|||
.AddSingleton<ChangeCustomizeService>()
|
||||
.AddSingleton<UpdateSlotService>()
|
||||
.AddSingleton<WeaponService>()
|
||||
.AddSingleton<PenumbraService>();
|
||||
.AddSingleton<PenumbraService>()
|
||||
.AddSingleton<ObjectManager>();
|
||||
|
||||
private static IServiceCollection AddUi(this IServiceCollection services)
|
||||
=> services
|
||||
.AddSingleton<DebugTab>()
|
||||
=> services.AddSingleton<DebugTab>()
|
||||
.AddSingleton<MainWindow>()
|
||||
.AddSingleton<GlamourerWindowSystem>();
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public abstract class AsyncServiceWrapper<T> : IDisposable
|
|||
else
|
||||
{
|
||||
Service = service;
|
||||
Item.Log.Verbose($"[{Name}] Created.");
|
||||
Glamourer.Log.Verbose($"[{Name}] Created.");
|
||||
_task = null;
|
||||
}
|
||||
});
|
||||
|
|
@ -71,7 +71,7 @@ public abstract class AsyncServiceWrapper<T> : IDisposable
|
|||
_task = null;
|
||||
if (Service is IDisposable d)
|
||||
d.Dispose();
|
||||
Item.Log.Verbose($"[{Name}] Disposed.");
|
||||
Glamourer.Log.Verbose($"[{Name}] Disposed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,106 @@
|
|||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Glamourer.Services;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
||||
|
||||
namespace Glamourer;
|
||||
|
||||
public class Configuration : IPluginConfiguration, ISavable
|
||||
{
|
||||
public bool UseRestrictedGearProtection = true;
|
||||
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
public Dictionary<ColorId, uint> Colors { get; set; }
|
||||
= Enum.GetValues<ColorId>().ToDictionary(c => c, c => c.Data().DefaultColor);
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
public Configuration(SaveService saveService, ConfigMigrationService migrator)
|
||||
{
|
||||
_saveService = saveService;
|
||||
Load(migrator);
|
||||
}
|
||||
|
||||
public void Save()
|
||||
=> _saveService.QueueSave(this);
|
||||
|
||||
public void Load(ConfigMigrationService migrator)
|
||||
{
|
||||
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
|
||||
{
|
||||
Glamourer.Log.Error(
|
||||
$"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
|
||||
errorArgs.ErrorContext.Handled = true;
|
||||
}
|
||||
|
||||
if (!File.Exists(_saveService.FileNames.ConfigFile))
|
||||
return;
|
||||
|
||||
if (File.Exists(_saveService.FileNames.ConfigFile))
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(_saveService.FileNames.ConfigFile);
|
||||
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
|
||||
{
|
||||
Error = HandleDeserializationError,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Glamourer.ChatService.NotificationMessage(ex,
|
||||
"Error reading Configuration, reverting to default.\nYou may be able to restore your configuration using the rolling backups in the XIVLauncher/backups/Glamourer directory.",
|
||||
"Error reading Configuration", "Error", NotificationType.Error);
|
||||
}
|
||||
|
||||
migrator.Migrate(this);
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.ConfigFile;
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
|
||||
serializer.Serialize(jWriter, this);
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfigMigrationService
|
||||
{
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
private Configuration _config = null!;
|
||||
private JObject _data = null!;
|
||||
|
||||
public ConfigMigrationService(SaveService saveService)
|
||||
=> _saveService = saveService;
|
||||
|
||||
public void Migrate(Configuration config)
|
||||
{
|
||||
_config = config;
|
||||
_data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile));
|
||||
MigrateV1To2();
|
||||
}
|
||||
|
||||
private void MigrateV1To2()
|
||||
{
|
||||
if (_config.Version > 1)
|
||||
return;
|
||||
|
||||
_config.Version = 2;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfigurationOld : IPluginConfiguration, ISavable
|
||||
{
|
||||
[JsonIgnore]
|
||||
private readonly SaveService _saveService;
|
||||
|
|
@ -42,7 +135,7 @@ public class Configuration : IPluginConfiguration, ISavable
|
|||
public void Save()
|
||||
=> _saveService.QueueSave(this);
|
||||
|
||||
public Configuration(SaveService saveService)
|
||||
public ConfigurationOld(SaveService saveService)
|
||||
{
|
||||
_saveService = saveService;
|
||||
Load();
|
||||
|
|
|
|||
|
|
@ -75,10 +75,10 @@ public partial class Interface
|
|||
ImGui.Dummy(_spacing);
|
||||
|
||||
DrawColorPicker("Customization Color", "The color for designs that only apply their character customization.",
|
||||
_config.CustomizationColor, Configuration.DefaultCustomizationColor, c => _config.CustomizationColor = c);
|
||||
_config.CustomizationColor, ConfigurationOld.DefaultCustomizationColor, c => _config.CustomizationColor = c);
|
||||
DrawColorPicker("Equipment Color", "The color for designs that only apply some or all of their equipment slots and stains.",
|
||||
_config.EquipmentColor, Configuration.DefaultEquipmentColor, c => _config.EquipmentColor = c);
|
||||
_config.EquipmentColor, ConfigurationOld.DefaultEquipmentColor, c => _config.EquipmentColor = c);
|
||||
DrawColorPicker("State Color", "The color for designs that only apply some state modification.",
|
||||
_config.StateColor, Configuration.DefaultStateColor, c => _config.StateColor = c);
|
||||
_config.StateColor, ConfigurationOld.DefaultStateColor, c => _config.StateColor = c);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ public partial class Interface : Window, IDisposable
|
|||
|
||||
private readonly EquipmentDrawer _equipmentDrawer;
|
||||
private readonly CustomizationDrawer _customizationDrawer;
|
||||
private readonly Configuration _config;
|
||||
private readonly ConfigurationOld _config;
|
||||
private readonly ActorTab _actorTab;
|
||||
private readonly DesignTab _designTab;
|
||||
private readonly DebugStateTab _debugStateTab;
|
||||
private readonly DebugDataTab _debugDataTab;
|
||||
|
||||
public Interface(DalamudPluginInterface pi, ItemManager items, ActiveDesign.Manager activeDesigns, DesignManager designManager,
|
||||
DesignFileSystem fileSystem, ObjectManager objects, CustomizationService customization, Configuration config, DataManager gameData, TargetManager targets, ActorService actors, KeyState keyState)
|
||||
DesignFileSystem fileSystem, ObjectManager objects, CustomizationService customization, ConfigurationOld config, DataManager gameData, TargetManager targets, ActorService actors, KeyState keyState)
|
||||
: base(GetLabel())
|
||||
{
|
||||
_pi = pi;
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public unsafe partial struct Actor : IEquatable<Actor>, IDesignable
|
|||
=> Pointer != null;
|
||||
|
||||
public int Index
|
||||
=> Pointer->GameObject.ObjectIndex;
|
||||
=> Valid ? Pointer->GameObject.ObjectIndex : -1;
|
||||
|
||||
public uint ModelId
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ public class ItemManager : IDisposable
|
|||
public const string SmallClothesNpc = "Smallclothes (NPC)";
|
||||
public const ushort SmallClothesNpcModel = 9903;
|
||||
|
||||
private readonly Configuration _config;
|
||||
private readonly ConfigurationOld _config;
|
||||
public readonly IdentifierService IdentifierService;
|
||||
public readonly ExcelSheet<Item> ItemSheet;
|
||||
public readonly StainData Stains;
|
||||
public readonly ItemService ItemService;
|
||||
public readonly RestrictedGear RestrictedGear;
|
||||
|
||||
public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config)
|
||||
public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, ConfigurationOld config)
|
||||
{
|
||||
_config = config;
|
||||
ItemSheet = gameData.GetExcelSheet<Item>()!;
|
||||
|
|
|
|||
|
|
@ -9,89 +9,12 @@ namespace Glamourer.Services;
|
|||
/// <summary>
|
||||
/// Any file type that we want to save via SaveService.
|
||||
/// </summary>
|
||||
public interface ISavable
|
||||
public interface ISavable : ISavable<FilenameService>
|
||||
{ }
|
||||
|
||||
public sealed class SaveService : SaveServiceBase<FilenameService>
|
||||
{
|
||||
/// <summary> The full file name of a given object. </summary>
|
||||
public string ToFilename(FilenameService fileNames);
|
||||
|
||||
/// <summary> Write the objects data to the given stream writer. </summary>
|
||||
public void Save(StreamWriter writer);
|
||||
|
||||
/// <summary> An arbitrary message printed to Debug before saving. </summary>
|
||||
public string LogName(string fileName)
|
||||
=> fileName;
|
||||
|
||||
public string TypeName
|
||||
=> GetType().Name;
|
||||
}
|
||||
|
||||
public class SaveService
|
||||
{
|
||||
private readonly Logger _log;
|
||||
private readonly FrameworkManager _framework;
|
||||
|
||||
public readonly FilenameService FileNames;
|
||||
|
||||
public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames)
|
||||
{
|
||||
_log = log;
|
||||
_framework = framework;
|
||||
FileNames = fileNames;
|
||||
}
|
||||
|
||||
/// <summary> Queue a save for the next framework tick. </summary>
|
||||
public void QueueSave(ISavable value)
|
||||
{
|
||||
var file = value.ToFilename(FileNames);
|
||||
_framework.RegisterOnTick(value.GetType().Name + file, () =>
|
||||
{
|
||||
ImmediateSave(value);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary> Immediately trigger a save. </summary>
|
||||
public void ImmediateSave(ISavable value)
|
||||
{
|
||||
var name = value.ToFilename(FileNames);
|
||||
try
|
||||
{
|
||||
if (name.Length == 0)
|
||||
{
|
||||
throw new Exception("Invalid object returned empty filename.");
|
||||
}
|
||||
|
||||
_log.Debug($"Saving {value.TypeName} {value.LogName(name)}...");
|
||||
var file = new FileInfo(name);
|
||||
file.Directory?.Create();
|
||||
using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew);
|
||||
using var w = new StreamWriter(s, Encoding.UTF8);
|
||||
value.Save(w);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error($"Could not save {value.GetType().Name} {value.LogName(name)}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public void ImmediateDelete(ISavable value)
|
||||
{
|
||||
var name = value.ToFilename(FileNames);
|
||||
try
|
||||
{
|
||||
if (name.Length == 0)
|
||||
{
|
||||
throw new Exception("Invalid object returned empty filename.");
|
||||
}
|
||||
|
||||
if (!File.Exists(name))
|
||||
return;
|
||||
|
||||
_log.Information($"Deleting {value.GetType().Name} {value.LogName(name)}...");
|
||||
File.Delete(name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}");
|
||||
}
|
||||
}
|
||||
: base(log, framework, fileNames)
|
||||
{ }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public static class ServiceManager
|
|||
.AddSingleton<ChatService>();
|
||||
|
||||
private static IServiceCollection AddConfig(this IServiceCollection services)
|
||||
=> services.AddSingleton<Configuration>()
|
||||
=> services.AddSingleton<ConfigurationOld>()
|
||||
.AddSingleton<BackupService>();
|
||||
|
||||
private static IServiceCollection AddPenumbra(this IServiceCollection services)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue