This commit is contained in:
Ottermandias 2023-06-16 16:13:26 +02:00
parent 7463aafa13
commit 27f151c55a
32 changed files with 1744 additions and 151 deletions

View file

@ -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)

View 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,
};
}

View 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
View 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
}

View 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));
}
}

View 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;
}
}

View 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);
}

View file

@ -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
{

View file

@ -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
View 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;
}

View file

@ -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";
}

View file

@ -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
}

View file

@ -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);
}

View 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;
}

View file

@ -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.");
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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)

View file

@ -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}");
}

View 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);
}
}

View file

@ -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());
}

View file

@ -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();

View 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)
{ }
}

View file

@ -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>();

View file

@ -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.");
}
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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
{

View file

@ -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>()!;

View file

@ -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)
{ }
}

View file

@ -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)