This commit is contained in:
Ottermandias 2023-06-09 17:57:40 +02:00
parent 7710cfadfa
commit 2d6fd6015d
88 changed files with 2304 additions and 383 deletions

View file

@ -0,0 +1,318 @@
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using System;
using Glamourer.Api;
using Glamourer.Designs;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.Api.Enums;
namespace Glamourer;
public partial class Glamourer
{
public class GlamourerIpc : IDisposable
{
public const int CurrentApiVersion = 0;
public const string LabelProviderApiVersion = "Glamourer.ApiVersion";
public const string LabelProviderGetAllCustomization = "Glamourer.GetAllCustomization";
public const string LabelProviderGetAllCustomizationFromCharacter = "Glamourer.GetAllCustomizationFromCharacter";
public const string LabelProviderApplyAll = "Glamourer.ApplyAll";
public const string LabelProviderApplyAllToCharacter = "Glamourer.ApplyAllToCharacter";
public const string LabelProviderApplyOnlyEquipment = "Glamourer.ApplyOnlyEquipment";
public const string LabelProviderApplyOnlyEquipmentToCharacter = "Glamourer.ApplyOnlyEquipmentToCharacter";
public const string LabelProviderApplyOnlyCustomization = "Glamourer.ApplyOnlyCustomization";
public const string LabelProviderApplyOnlyCustomizationToCharacter = "Glamourer.ApplyOnlyCustomizationToCharacter";
public const string LabelProviderRevert = "Glamourer.Revert";
public const string LabelProviderRevertCharacter = "Glamourer.RevertCharacter";
private readonly ObjectTable _objectTable;
private readonly DalamudPluginInterface _pluginInterface;
private readonly ActiveDesign.Manager _stateManager;
private readonly ItemManager _items;
private readonly PenumbraAttach _penumbra;
private readonly ActorService _actors;
internal ICallGateProvider<string, string?>? ProviderGetAllCustomization;
internal ICallGateProvider<Character?, string?>? ProviderGetAllCustomizationFromCharacter;
internal ICallGateProvider<string, string, object>? ProviderApplyAll;
internal ICallGateProvider<string, Character?, object>? ProviderApplyAllToCharacter;
internal ICallGateProvider<string, string, object>? ProviderApplyOnlyCustomization;
internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyCustomizationToCharacter;
internal ICallGateProvider<string, string, object>? ProviderApplyOnlyEquipment;
internal ICallGateProvider<string, Character?, object>? ProviderApplyOnlyEquipmentToCharacter;
internal ICallGateProvider<string, object>? ProviderRevert;
internal ICallGateProvider<Character?, object>? ProviderRevertCharacter;
internal ICallGateProvider<int>? ProviderGetApiVersion;
public GlamourerIpc(ObjectTable objectTable, DalamudPluginInterface pluginInterface, ActiveDesign.Manager stateManager,
ItemManager items, PenumbraAttach penumbra, ActorService actors)
{
_objectTable = objectTable;
_pluginInterface = pluginInterface;
_stateManager = stateManager;
_items = items;
_penumbra = penumbra;
_actors = actors;
InitializeProviders();
}
public void Dispose()
=> DisposeProviders();
private void DisposeProviders()
{
ProviderGetAllCustomization?.UnregisterFunc();
ProviderGetAllCustomizationFromCharacter?.UnregisterFunc();
ProviderApplyAll?.UnregisterAction();
ProviderApplyAllToCharacter?.UnregisterAction();
ProviderApplyOnlyCustomization?.UnregisterAction();
ProviderApplyOnlyCustomizationToCharacter?.UnregisterAction();
ProviderApplyOnlyEquipment?.UnregisterAction();
ProviderApplyOnlyEquipmentToCharacter?.UnregisterAction();
ProviderRevert?.UnregisterAction();
ProviderRevertCharacter?.UnregisterAction();
ProviderGetApiVersion?.UnregisterFunc();
}
private void InitializeProviders()
{
try
{
ProviderGetApiVersion = _pluginInterface.GetIpcProvider<int>(LabelProviderApiVersion);
ProviderGetApiVersion.RegisterFunc(GetApiVersion);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApiVersion}.");
}
try
{
ProviderGetAllCustomization = _pluginInterface.GetIpcProvider<string, string?>(LabelProviderGetAllCustomization);
ProviderGetAllCustomization.RegisterFunc(GetAllCustomization);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
}
try
{
ProviderGetAllCustomizationFromCharacter =
_pluginInterface.GetIpcProvider<Character?, string?>(LabelProviderGetAllCustomizationFromCharacter);
ProviderGetAllCustomizationFromCharacter.RegisterFunc(GetAllCustomization);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderGetAllCustomizationFromCharacter}.");
}
try
{
ProviderApplyAll =
_pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyAll);
ProviderApplyAll.RegisterAction(ApplyAll);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyAll}.");
}
try
{
ProviderApplyAllToCharacter =
_pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyAllToCharacter);
ProviderApplyAllToCharacter.RegisterAction(ApplyAll);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyAll}.");
}
try
{
ProviderApplyOnlyCustomization =
_pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyOnlyCustomization);
ProviderApplyOnlyCustomization.RegisterAction(ApplyOnlyCustomization);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyCustomization}.");
}
try
{
ProviderApplyOnlyCustomizationToCharacter =
_pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyOnlyCustomizationToCharacter);
ProviderApplyOnlyCustomizationToCharacter.RegisterAction(ApplyOnlyCustomization);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyCustomization}.");
}
try
{
ProviderApplyOnlyEquipment =
_pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyOnlyEquipment);
ProviderApplyOnlyEquipment.RegisterAction(ApplyOnlyEquipment);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
}
try
{
ProviderApplyOnlyEquipmentToCharacter =
_pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyOnlyEquipmentToCharacter);
ProviderApplyOnlyEquipmentToCharacter.RegisterAction(ApplyOnlyEquipment);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
}
try
{
ProviderRevert =
_pluginInterface.GetIpcProvider<string, object>(LabelProviderRevert);
ProviderRevert.RegisterAction(Revert);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderRevert}.");
}
try
{
ProviderRevertCharacter =
_pluginInterface.GetIpcProvider<Character?, object>(LabelProviderRevertCharacter);
ProviderRevertCharacter.RegisterAction(Revert);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderRevert}.");
}
}
private static int GetApiVersion()
=> CurrentApiVersion;
private void ApplyAll(string customization, string characterName)
{
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() == characterName)
{
ApplyAll(customization, gameObject as Character);
return;
}
}
}
private void ApplyAll(string customization, Character? character)
{
if (character == null)
return;
var design = Design.CreateTemporaryFromBase64(_items, customization, true, true);
var active = _stateManager.GetOrCreateSave(character.Address);
_stateManager.ApplyDesign(active, design, false);
}
private void ApplyOnlyCustomization(string customization, string characterName)
{
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() == characterName)
{
ApplyOnlyCustomization(customization, gameObject as Character);
return;
}
}
}
private void ApplyOnlyCustomization(string customization, Character? character)
{
if (character == null)
return;
var design = Design.CreateTemporaryFromBase64(_items, customization, true, false);
var active = _stateManager.GetOrCreateSave(character.Address);
_stateManager.ApplyDesign(active, design, false);
}
private void ApplyOnlyEquipment(string customization, string characterName)
{
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() != characterName)
continue;
ApplyOnlyEquipment(customization, gameObject as Character);
return;
}
}
private void ApplyOnlyEquipment(string customization, Character? character)
{
if (character == null)
return;
var design = Design.CreateTemporaryFromBase64(_items, customization, false, true);
var active = _stateManager.GetOrCreateSave(character.Address);
_stateManager.ApplyDesign(active, design, false);
}
private void Revert(string characterName)
{
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() != characterName)
continue;
Revert(gameObject as Character);
}
}
private void Revert(Character? character)
{
if (character == null)
return;
var ident = _actors.AwaitedService.FromObject(character, true, false, false);
_stateManager.DeleteSave(ident);
_penumbra.RedrawObject(character.Address, RedrawType.Redraw);
}
private string? GetAllCustomization(Character? character)
{
if (character == null)
return null;
var ident = _actors.AwaitedService.FromObject(character, true, false, false);
if (!_stateManager.TryGetValue(ident, out var design))
design = new ActiveDesign(_items, ident, character.Address);
return design.CreateOldBase64();
}
private string? GetAllCustomization(string characterName)
{
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() == characterName)
return GetAllCustomization(gameObject as Character);
}
return null;
}
}
}

View file

@ -0,0 +1,207 @@
using System;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
using Dalamud.Plugin;
using Glamourer.Interop;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
namespace Glamourer.Api;
public unsafe class PenumbraAttach : IDisposable
{
public const int RequiredPenumbraBreakingVersion = 4;
public const int RequiredPenumbraFeatureVersion = 15;
private readonly DalamudPluginInterface _pluginInterface;
private readonly EventSubscriber<ChangedItemType, uint> _tooltipSubscriber;
private readonly EventSubscriber<MouseButton, ChangedItemType, uint> _clickSubscriber;
private readonly EventSubscriber<nint, string, nint, nint, nint> _creatingCharacterBase;
private readonly EventSubscriber<nint, string, nint> _createdCharacterBase;
private ActionSubscriber<int, RedrawType> _redrawSubscriber;
private FuncSubscriber<nint, (nint, string)> _drawObjectInfo;
private FuncSubscriber<int, int> _cutsceneParent;
private readonly EventSubscriber _initializedEvent;
private readonly EventSubscriber _disposedEvent;
public bool Available { get; private set; }
public PenumbraAttach(DalamudPluginInterface pi)
{
_pluginInterface = pi;
_initializedEvent = Ipc.Initialized.Subscriber(pi, Reattach);
_disposedEvent = Ipc.Disposed.Subscriber(pi, Unattach);
_tooltipSubscriber = Ipc.ChangedItemTooltip.Subscriber(pi);
_clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi);
_createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi);
_creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi);
Reattach();
}
public event Action<MouseButton, ChangedItemType, uint> Click
{
add => _clickSubscriber.Event += value;
remove => _clickSubscriber.Event -= value;
}
public event Action<ChangedItemType, uint> Tooltip
{
add => _tooltipSubscriber.Event += value;
remove => _tooltipSubscriber.Event -= value;
}
public event Action<nint, string, nint, nint, nint> CreatingCharacterBase
{
add => _creatingCharacterBase.Event += value;
remove => _creatingCharacterBase.Event -= value;
}
public event Action<nint, string, nint> CreatedCharacterBase
{
add => _createdCharacterBase.Event += value;
remove => _createdCharacterBase.Event -= value;
}
public Actor GameObjectFromDrawObject(IntPtr drawObject)
=> Available ? _drawObjectInfo.Invoke(drawObject).Item1 : Actor.Null;
public int CutsceneParent(int idx)
=> Available ? _cutsceneParent.Invoke(idx) : -1;
public void RedrawObject(Actor actor, RedrawType settings)
{
if (!actor || !Available)
return;
try
{
_redrawSubscriber.Invoke(actor.Index, settings);
}
catch (Exception e)
{
PluginLog.Debug($"Failure redrawing object:\n{e}");
}
}
public void Reattach()
{
try
{
Unattach();
var (breaking, feature) = Ipc.ApiVersions.Subscriber(_pluginInterface).Invoke();
if (breaking != RequiredPenumbraBreakingVersion || feature < RequiredPenumbraFeatureVersion)
throw new Exception(
$"Invalid Version {breaking}.{feature:D4}, required major Version {RequiredPenumbraBreakingVersion} with feature greater or equal to {RequiredPenumbraFeatureVersion}.");
_tooltipSubscriber.Enable();
_clickSubscriber.Enable();
_creatingCharacterBase.Enable();
_createdCharacterBase.Enable();
_drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface);
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
Available = true;
Glamourer.Log.Debug("Glamourer attached to Penumbra.");
}
catch (Exception e)
{
Glamourer.Log.Debug($"Could not attach to Penumbra:\n{e}");
}
}
public void Unattach()
{
_tooltipSubscriber.Disable();
_clickSubscriber.Disable();
_creatingCharacterBase.Disable();
_createdCharacterBase.Disable();
if (Available)
{
Available = false;
Glamourer.Log.Debug("Glamourer detached from Penumbra.");
}
}
public void Dispose()
{
Unattach();
_tooltipSubscriber.Dispose();
_clickSubscriber.Dispose();
_creatingCharacterBase.Dispose();
_createdCharacterBase.Dispose();
_initializedEvent.Dispose();
_disposedEvent.Dispose();
}
//private static void PenumbraTooltip(ChangedItemType type, uint _)
//{
// if (type == ChangedItemType.Item)
// ImGui.Text("Right click to apply to current Glamourer Set. [Glamourer]");
//}
//
//private void PenumbraRightClick(MouseButton button, ChangedItemType type, uint id)
//{
// if (button != MouseButton.Right || type != ChangedItemType.Item)
// return;
//
// var item = (Lumina.Excel.GeneratedSheets.Item)type.GetObject(Dalamud.GameData, id)!;
// var writeItem = new Item2(item, string.Empty);
//
// UpdateItem(_objects.GPosePlayer, writeItem);
// UpdateItem(_objects.Player, writeItem);
//}
//private static void UpdateItem(Actor actor, Item2 item2)
//{
// if (!actor || !actor.DrawObject)
// return;
//
// switch (item2.EquippableTo)
// {
// case EquipSlot.MainHand:
// {
// var off = item2.HasSubModel
// ? new CharacterWeapon(item2.SubModel.id, item2.SubModel.type, item2.SubModel.variant, actor.DrawObject.OffHand.Stain)
// : item2.IsBothHand
// ? CharacterWeapon.Empty
// : actor.OffHand;
// var main = new CharacterWeapon(item2.MainModel.id, item2.MainModel.type, item2.MainModel.variant,
// actor.DrawObject.MainHand.Stain);
// Glamourer.RedrawManager.LoadWeapon(actor, main, off);
// return;
// }
// case EquipSlot.OffHand:
// {
// var off = new CharacterWeapon(item2.MainModel.id, item2.MainModel.type, item2.MainModel.variant,
// actor.DrawObject.OffHand.Stain);
// var main = actor.MainHand;
// Glamourer.RedrawManager.LoadWeapon(actor, main, off);
// return;
// }
// default:
// {
// var current = actor.DrawObject.Equip[item2.EquippableTo];
// var armor = new CharacterArmor(item2.MainModel.id, (byte)item2.MainModel.variant, current.Stain);
// Glamourer.RedrawManager.UpdateSlot(actor.DrawObject, item2.EquippableTo, armor);
// return;
// }
// }
//}
// Update objects without triggering PlayerWatcher Events,
// then manually redraw using Penumbra.
public void UpdateCharacters(Character character, Character? gPoseOriginalCharacter = null)
{
//RedrawObject(character, RedrawType.Redraw, true);
//
//// Special case for carrying over changes to the gPose player to the regular player, too.
//if (gPoseOriginalCharacter == null)
// return;
//
//newEquip.Write(gPoseOriginalCharacter.Address);
//RedrawObject(gPoseOriginalCharacter, RedrawType.AfterGPose, false);
}
}

View file

@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.IO;
using Dalamud.Configuration;
using Glamourer.Services;
using Newtonsoft.Json;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Glamourer;
public class Configuration : IPluginConfiguration, ISavable
{
[JsonIgnore]
private readonly SaveService _saveService;
public class FixedDesign
{
public string Name = string.Empty;
public string Path = string.Empty;
public uint JobGroups;
public bool Enabled;
}
public int Version { get; set; } = 1;
public const uint DefaultCustomizationColor = 0xFFC000C0;
public const uint DefaultStateColor = 0xFF00C0C0;
public const uint DefaultEquipmentColor = 0xFF00C000;
public bool UseRestrictedGearProtection { get; set; } = true;
public bool FoldersFirst { get; set; } = false;
public bool ColorDesigns { get; set; } = true;
public bool ShowLocks { get; set; } = true;
public bool ApplyFixedDesigns { get; set; } = true;
public uint CustomizationColor { get; set; } = DefaultCustomizationColor;
public uint StateColor { get; set; } = DefaultStateColor;
public uint EquipmentColor { get; set; } = DefaultEquipmentColor;
public List<FixedDesign> FixedDesigns { get; set; } = new();
public void Save()
=> _saveService.QueueSave(this);
public Configuration(SaveService saveService)
{
_saveService = saveService;
Load();
}
public void Load()
{
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;
var text = File.ReadAllText(_saveService.FileNames.ConfigFile);
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
{
Error = HandleDeserializationError,
});
}
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);
}
}

49
GlamourerOld/Dalamud.cs Normal file
View file

@ -0,0 +1,49 @@
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.IoC;
using Dalamud.Plugin;
using Microsoft.Extensions.DependencyInjection;
namespace Glamourer;
public class DalamudServices
{
public DalamudServices(DalamudPluginInterface pi)
{
pi.Inject(this);
}
public void AddServices(IServiceCollection services)
{
services.AddSingleton(PluginInterface);
services.AddSingleton(Commands);
services.AddSingleton(GameData);
services.AddSingleton(ClientState);
services.AddSingleton(GameGui);
services.AddSingleton(Chat);
services.AddSingleton(Framework);
services.AddSingleton(Targets);
services.AddSingleton(Objects);
services.AddSingleton(KeyState);
services.AddSingleton(this);
services.AddSingleton(PluginInterface.UiBuilder);
}
// @formatter:off
[PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!;
// @formatter:on
}

View file

@ -0,0 +1,320 @@
using System;
using System.IO;
using System.Linq;
using Glamourer.Customization;
using Glamourer.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public partial class Design : DesignData, ISavable
{
public const int FileVersion = 1;
public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; internal init; }
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; }
public EquipFlag ApplyEquip { get; internal set; }
public CustomizeFlag ApplyCustomize { get; internal set; }
public QuadBool Wetness { get; internal set; } = QuadBool.NullFalse;
public QuadBool Visor { get; internal set; } = QuadBool.NullFalse;
public QuadBool Hat { get; internal set; } = QuadBool.NullFalse;
public QuadBool Weapon { get; internal set; } = QuadBool.NullFalse;
public bool WriteProtected { get; internal set; }
public bool DoApplyEquip(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToFlag());
public bool DoApplyStain(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToStainFlag());
public bool DoApplyCustomize(CustomizeIndex idx)
=> ApplyCustomize.HasFlag(idx.ToFlag());
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;
}
internal Design(ItemManager items)
: base(items)
{ }
public JObject JsonSerialize()
{
var ret = new JObject
{
[nameof(FileVersion)] = FileVersion,
[nameof(Identifier)] = Identifier,
[nameof(CreationDate)] = CreationDate,
[nameof(Name)] = Name.Text,
[nameof(Description)] = Description,
[nameof(Tags)] = JArray.FromObject(Tags),
[nameof(WriteProtected)] = WriteProtected,
["Equipment"] = SerializeEquipment(),
[nameof(ModelData.Customize)] = SerializeCustomize(),
};
return ret;
}
public JObject SerializeEquipment()
{
static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain)
=> new()
{
[nameof(Item.ItemId)] = itemId,
[nameof(Item.Stain)] = stain.Value,
["Apply"] = apply,
["ApplyStain"] = applyStain,
};
var ret = new JObject()
{
[nameof(MainHandId)] =
Serialize(MainHandId, ModelData.MainHand.Stain, DoApplyEquip(EquipSlot.MainHand), DoApplyStain(EquipSlot.MainHand)),
[nameof(OffHandId)] = Serialize(OffHandId, ModelData.OffHand.Stain, DoApplyEquip(EquipSlot.OffHand),
DoApplyStain(EquipSlot.OffHand)),
};
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var armor = Armor(slot);
ret[slot.ToString()] = Serialize(armor.ItemId, armor.Stain, DoApplyEquip(slot), DoApplyStain(slot));
}
ret[nameof(Hat)] = Hat.ToJObject("Show", "Apply");
ret[nameof(Weapon)] = Weapon.ToJObject("Show", "Apply");
ret[nameof(Visor)] = Visor.ToJObject("IsToggled", "Apply");
return ret;
}
public JObject SerializeCustomize()
{
var ret = new JObject()
{
[nameof(ModelId)] = ModelId,
};
var customize = ModelData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
var data = customize[idx];
ret[idx.ToString()] = new JObject()
{
["Value"] = data.Value,
["Apply"] = true,
};
}
ret[nameof(Wetness)] = Wetness.ToJObject("IsWet", "Apply");
return ret;
}
public static Design LoadDesign(ItemManager items, JObject json, out bool changes)
{
var version = json[nameof(FileVersion)]?.ToObject<int>() ?? 0;
return version switch
{
1 => LoadDesignV1(items, json, out changes),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
}
internal static Design LoadDesignV1(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 design = new Design(items)
{
CreationDate = json["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate"),
Identifier = json["Identifier"]?.ToObject<Guid>() ?? throw new ArgumentNullException("Identifier"),
Name = new LowerString(json["Name"]?.ToObject<string>() ?? throw new ArgumentNullException("Name")),
Description = json["Description"]?.ToObject<string>() ?? string.Empty,
Tags = ParseTags(json),
};
changes = LoadEquip(items, json["Equipment"], design);
changes |= LoadCustomize(json["Customize"], design);
return design;
}
internal 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 |= !design.SetArmor(items, slot, id);
changes |= !design.SetStain(slot, stain);
design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain);
}
var main = equip["MainHand"];
if (main == null)
{
changes = true;
}
else
{
var id = main["ItemId"]?.ToObject<uint>() ?? items.DefaultSword.RowId;
var stain = (StainId)(main["Stain"]?.ToObject<byte>() ?? 0);
var apply = main["Apply"]?.ToObject<bool>() ?? false;
var applyStain = main["ApplyStain"]?.ToObject<bool>() ?? false;
changes |= !design.SetMainhand(items, id);
changes |= !design.SetStain(EquipSlot.MainHand, stain);
design.SetApplyEquip(EquipSlot.MainHand, apply);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
}
var off = equip["OffHand"];
if (off == null)
{
changes = true;
}
else
{
var id = off["ItemId"]?.ToObject<uint>() ?? ItemManager.NothingId(design.MainhandType.Offhand());
var stain = (StainId)(off["Stain"]?.ToObject<byte>() ?? 0);
var apply = off["Apply"]?.ToObject<bool>() ?? false;
var applyStain = off["ApplyStain"]?.ToObject<bool>() ?? false;
changes |= !design.SetOffhand(items, id);
changes |= !design.SetStain(EquipSlot.OffHand, stain);
design.SetApplyEquip(EquipSlot.OffHand, apply);
design.SetApplyStain(EquipSlot.OffHand, applyStain);
}
design.Hat = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.Weapon = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
design.Visor = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
return changes;
}
internal static bool LoadCustomize(JToken? json, Design design)
{
if (json == null)
return true;
ref var customize = ref design.ModelData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
customize[idx] = data;
design.SetApplyCustomize(idx, apply);
}
design.Wetness = QuadBool.FromJObject(json["Wetness"], "IsWet", "Apply", QuadBool.NullFalse);
return false;
}
public void MigrateBase64(ItemManager items, string base64)
{
var data = DesignBase64Migration.MigrateBase64(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);
}

View file

@ -0,0 +1,430 @@
using System;
using Glamourer.Customization;
using Glamourer.Services;
using OtterGui.Classes;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignData
{
internal ModelData ModelData;
public FullEquipType MainhandType { get; internal set; }
public uint Head = ItemManager.NothingId(EquipSlot.Head);
public uint Body = ItemManager.NothingId(EquipSlot.Body);
public uint Hands = ItemManager.NothingId(EquipSlot.Hands);
public uint Legs = ItemManager.NothingId(EquipSlot.Legs);
public uint Feet = ItemManager.NothingId(EquipSlot.Feet);
public uint Ears = ItemManager.NothingId(EquipSlot.Ears);
public uint Neck = ItemManager.NothingId(EquipSlot.Neck);
public uint Wrists = ItemManager.NothingId(EquipSlot.Wrists);
public uint RFinger = ItemManager.NothingId(EquipSlot.RFinger);
public uint LFinger = ItemManager.NothingId(EquipSlot.RFinger);
public uint MainHandId;
public uint OffHandId;
public string HeadName = ItemManager.Nothing;
public string BodyName = ItemManager.Nothing;
public string HandsName = ItemManager.Nothing;
public string LegsName = ItemManager.Nothing;
public string FeetName = ItemManager.Nothing;
public string EarsName = ItemManager.Nothing;
public string NeckName = ItemManager.Nothing;
public string WristsName = ItemManager.Nothing;
public string RFingerName = ItemManager.Nothing;
public string LFingerName = ItemManager.Nothing;
public string MainhandName;
public string OffhandName;
public DesignData(ItemManager items)
{
MainHandId = items.DefaultSword.RowId;
(_, var set, var type, var variant, MainhandName, MainhandType) = items.Resolve(MainHandId, items.DefaultSword);
ModelData = new ModelData(new CharacterWeapon(set, type, variant, 0));
OffHandId = ItemManager.NothingId(MainhandType.Offhand());
(_, ModelData.OffHand.Set, ModelData.OffHand.Type, ModelData.OffHand.Variant, OffhandName, _) =
items.Resolve(OffHandId, MainhandType);
}
public uint ModelId
=> ModelData.ModelId;
public Item Armor(EquipSlot slot)
{
return slot switch
{
EquipSlot.Head => new Item(HeadName, Head, ModelData.Head),
EquipSlot.Body => new Item(BodyName, Body, ModelData.Body),
EquipSlot.Hands => new Item(HandsName, Hands, ModelData.Hands),
EquipSlot.Legs => new Item(LegsName, Legs, ModelData.Legs),
EquipSlot.Feet => new Item(FeetName, Feet, ModelData.Feet),
EquipSlot.Ears => new Item(EarsName, Ears, ModelData.Ears),
EquipSlot.Neck => new Item(NeckName, Neck, ModelData.Neck),
EquipSlot.Wrists => new Item(WristsName, Wrists, ModelData.Wrists),
EquipSlot.RFinger => new Item(RFingerName, RFinger, ModelData.RFinger),
EquipSlot.LFinger => new Item(LFingerName, LFinger, ModelData.LFinger),
_ => throw new Exception("Invalid equip slot for item."),
};
}
public Weapon WeaponMain
=> new(MainhandName, MainHandId, ModelData.MainHand, MainhandType);
public Weapon WeaponOff
=> Weapon.Offhand(OffhandName, OffHandId, ModelData.OffHand, MainhandType);
public CustomizeValue GetCustomize(CustomizeIndex idx)
=> ModelData.Customize[idx];
internal bool SetCustomize(CustomizeIndex idx, CustomizeValue value)
=> ModelData.Customize.Set(idx, value);
internal bool SetArmor(ItemManager items, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var (valid, set, variant, name) = items.Resolve(slot, itemId, item);
if (!valid)
return false;
return SetArmor(slot, set, variant, name, itemId);
}
internal bool SetArmor(EquipSlot slot, Item item)
=> SetArmor(slot, item.ModelBase, item.Variant, item.Name, item.ItemId);
internal bool UpdateArmor(ItemManager items, EquipSlot slot, CharacterArmor armor, bool force)
{
var (valid, id, name) = items.Identify(slot, armor.Set, armor.Variant);
if (!valid)
return false;
return SetArmor(slot, armor.Set, armor.Variant, name, id) | SetStain(slot, armor.Stain);
}
internal bool SetMainhand(ItemManager items, uint mainId, Lumina.Excel.GeneratedSheets.Item? main = null)
{
if (mainId == MainHandId)
return false;
var (valid, set, weapon, variant, name, type) = items.Resolve(mainId, main);
if (!valid)
return false;
var fixOffhand = type.Offhand() != MainhandType.Offhand();
MainHandId = mainId;
MainhandName = name;
MainhandType = type;
ModelData.MainHand.Set = set;
ModelData.MainHand.Type = weapon;
ModelData.MainHand.Variant = variant;
if (fixOffhand)
SetOffhand(items, ItemManager.NothingId(type.Offhand()));
return true;
}
internal bool SetOffhand(ItemManager items, uint offId, Lumina.Excel.GeneratedSheets.Item? off = null)
{
if (offId == OffHandId)
return false;
var (valid, set, weapon, variant, name, type) = items.Resolve(offId, MainhandType, off);
if (!valid)
return false;
OffHandId = offId;
OffhandName = name;
ModelData.OffHand.Set = set;
ModelData.OffHand.Type = weapon;
ModelData.OffHand.Variant = variant;
return true;
}
internal bool UpdateMainhand(ItemManager items, CharacterWeapon weapon)
{
if (weapon.Value == ModelData.MainHand.Value)
return false;
var (valid, id, name, type) = items.Identify(EquipSlot.MainHand, weapon.Set, weapon.Type, (byte)weapon.Variant);
if (!valid || id == MainHandId)
return false;
var fixOffhand = type.Offhand() != MainhandType.Offhand();
MainHandId = id;
MainhandName = name;
MainhandType = type;
ModelData.MainHand.Set = weapon.Set;
ModelData.MainHand.Type = weapon.Type;
ModelData.MainHand.Variant = weapon.Variant;
ModelData.MainHand.Stain = weapon.Stain;
if (fixOffhand)
SetOffhand(items, ItemManager.NothingId(type.Offhand()));
return true;
}
internal bool UpdateOffhand(ItemManager items, CharacterWeapon weapon)
{
if (weapon.Value == ModelData.OffHand.Value)
return false;
var (valid, id, name, _) = items.Identify(EquipSlot.OffHand, weapon.Set, weapon.Type, (byte)weapon.Variant, MainhandType);
if (!valid || id == OffHandId)
return false;
OffHandId = id;
OffhandName = name;
ModelData.OffHand.Set = weapon.Set;
ModelData.OffHand.Type = weapon.Type;
ModelData.OffHand.Variant = weapon.Variant;
ModelData.OffHand.Stain = weapon.Stain;
return true;
}
internal bool SetStain(EquipSlot slot, StainId id)
{
return slot switch
{
EquipSlot.MainHand => SetIfDifferent(ref ModelData.MainHand.Stain, id),
EquipSlot.OffHand => SetIfDifferent(ref ModelData.OffHand.Stain, id),
EquipSlot.Head => SetIfDifferent(ref ModelData.Head.Stain, id),
EquipSlot.Body => SetIfDifferent(ref ModelData.Body.Stain, id),
EquipSlot.Hands => SetIfDifferent(ref ModelData.Hands.Stain, id),
EquipSlot.Legs => SetIfDifferent(ref ModelData.Legs.Stain, id),
EquipSlot.Feet => SetIfDifferent(ref ModelData.Feet.Stain, id),
EquipSlot.Ears => SetIfDifferent(ref ModelData.Ears.Stain, id),
EquipSlot.Neck => SetIfDifferent(ref ModelData.Neck.Stain, id),
EquipSlot.Wrists => SetIfDifferent(ref ModelData.Wrists.Stain, id),
EquipSlot.RFinger => SetIfDifferent(ref ModelData.RFinger.Stain, id),
EquipSlot.LFinger => SetIfDifferent(ref ModelData.LFinger.Stain, id),
_ => false,
};
}
internal static bool SetIfDifferent<T>(ref T old, T value) where T : IEquatable<T>
{
if (old.Equals(value))
return false;
old = value;
return true;
}
private bool SetArmor(EquipSlot slot, SetId set, byte variant, string name, uint id)
{
var changes = false;
switch (slot)
{
case EquipSlot.Head:
changes |= SetIfDifferent(ref ModelData.Head.Set, set);
changes |= SetIfDifferent(ref ModelData.Head.Variant, variant);
changes |= HeadName != name;
HeadName = name;
changes |= Head != id;
Head = id;
return changes;
case EquipSlot.Body:
changes |= SetIfDifferent(ref ModelData.Body.Set, set);
changes |= SetIfDifferent(ref ModelData.Body.Variant, variant);
changes |= BodyName != name;
BodyName = name;
changes |= Body != id;
Body = id;
return changes;
case EquipSlot.Hands:
changes |= SetIfDifferent(ref ModelData.Hands.Set, set);
changes |= SetIfDifferent(ref ModelData.Hands.Variant, variant);
changes |= HandsName != name;
HandsName = name;
changes |= Hands != id;
Hands = id;
return changes;
case EquipSlot.Legs:
changes |= SetIfDifferent(ref ModelData.Legs.Set, set);
changes |= SetIfDifferent(ref ModelData.Legs.Variant, variant);
changes |= LegsName != name;
LegsName = name;
changes |= Legs != id;
Legs = id;
return changes;
case EquipSlot.Feet:
changes |= SetIfDifferent(ref ModelData.Feet.Set, set);
changes |= SetIfDifferent(ref ModelData.Feet.Variant, variant);
changes |= FeetName != name;
FeetName = name;
changes |= Feet != id;
Feet = id;
return changes;
case EquipSlot.Ears:
changes |= SetIfDifferent(ref ModelData.Ears.Set, set);
changes |= SetIfDifferent(ref ModelData.Ears.Variant, variant);
changes |= EarsName != name;
EarsName = name;
changes |= Ears != id;
Ears = id;
return changes;
case EquipSlot.Neck:
changes |= SetIfDifferent(ref ModelData.Neck.Set, set);
changes |= SetIfDifferent(ref ModelData.Neck.Variant, variant);
changes |= NeckName != name;
NeckName = name;
changes |= Neck != id;
Neck = id;
return changes;
case EquipSlot.Wrists:
changes |= SetIfDifferent(ref ModelData.Wrists.Set, set);
changes |= SetIfDifferent(ref ModelData.Wrists.Variant, variant);
changes |= WristsName != name;
WristsName = name;
changes |= Wrists != id;
Wrists = id;
return changes;
case EquipSlot.RFinger:
changes |= SetIfDifferent(ref ModelData.RFinger.Set, set);
changes |= SetIfDifferent(ref ModelData.RFinger.Variant, variant);
changes |= RFingerName != name;
RFingerName = name;
changes |= RFinger != id;
RFinger = id;
return changes;
case EquipSlot.LFinger:
changes |= SetIfDifferent(ref ModelData.LFinger.Set, set);
changes |= SetIfDifferent(ref ModelData.LFinger.Variant, variant);
changes |= LFingerName != name;
LFingerName = name;
changes |= LFinger != id;
LFinger = id;
return changes;
default: return false;
}
}
}
public static class DesignBase64Migration
{
public const int Base64Size = 91;
public static ModelData MigrateBase64(string base64, out EquipFlag equipFlags, out CustomizeFlag customizeFlags,
out bool writeinternal, out QuadBool wet, out QuadBool hat, out QuadBool visor, out QuadBool weapon)
{
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);
hat = QuadBool.Null;
visor = QuadBool.Null;
weapon = QuadBool.Null;
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);
hat = hat.SetValue((bytes[90] & 0x01) == 0);
visor = visor.SetValue((bytes[90] & 0x10) != 0);
weapon = weapon.SetValue((bytes[90] & 0x02) == 0);
break;
}
default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}.");
}
customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0;
wet = (applicationFlags & 0x02) != 0 ? QuadBool.True : QuadBool.NullFalse;
hat = hat.SetEnabled((applicationFlags & 0x04) != 0);
weapon = weapon.SetEnabled((applicationFlags & 0x08) != 0);
visor = visor.SetEnabled((applicationFlags & 0x10) != 0);
writeinternal = (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;
}
var data = new ModelData();
unsafe
{
fixed (byte* ptr = bytes)
{
data.Customize.Load(*(Customize*) (ptr + 4));
var cur = (CharacterWeapon*)(ptr + 30);
data.MainHand = cur[0];
data.OffHand = cur[1];
var eq = (CharacterArmor*)(cur + 2);
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{
var mdl = eq[idx];
data.SetPiece(slot, mdl.Set, mdl.Variant, mdl.Stain, out _);
}
}
}
return data;
}
public static unsafe string CreateOldBase64(in ModelData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, bool wet, bool hat,
bool setHat, bool visor, bool setVisor, bool weapon, bool setWeapon, bool writeinternal, float alpha)
{
var data = stackalloc byte[Base64Size];
data[0] = 2;
data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0)
| (wet ? 0x02 : 0)
| (setHat ? 0x04 : 0)
| (setWeapon ? 0x08 : 0)
| (setVisor ? 0x10 : 0)
| (writeinternal ? 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.Load(*(Customize*) (data + 4));
((CharacterWeapon*)(data + 30))[0] = save.MainHand;
((CharacterWeapon*)(data + 30))[1] = save.OffHand;
((CharacterArmor*)(data + 44))[0] = save.Head;
((CharacterArmor*)(data + 44))[1] = save.Body;
((CharacterArmor*)(data + 44))[2] = save.Hands;
((CharacterArmor*)(data + 44))[3] = save.Legs;
((CharacterArmor*)(data + 44))[4] = save.Feet;
((CharacterArmor*)(data + 44))[5] = save.Ears;
((CharacterArmor*)(data + 44))[6] = save.Neck;
((CharacterArmor*)(data + 44))[7] = save.Wrists;
((CharacterArmor*)(data + 44))[8] = save.RFinger;
((CharacterArmor*)(data + 44))[9] = save.LFinger;
*(float*)(data + 84) = 1f;
data[88] = (byte)((hat ? 0x01 : 0)
| (visor ? 0x10 : 0)
| (weapon ? 0x02 : 0));
return Convert.ToBase64String(new Span<byte>(data, Base64Size));
}
}

View file

@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Plugin;
using Glamourer.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
namespace Glamourer.Designs;
public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
{
public static string GetDesignFileSystemFile(DalamudPluginInterface pi)
=> Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json");
public readonly string DesignFileSystemFile;
private readonly SaveService _saveService;
private readonly DesignManager _designManager;
public DesignFileSystem(DesignManager designManager, DalamudPluginInterface pi, SaveService saveService)
{
DesignFileSystemFile = GetDesignFileSystemFile(pi);
_designManager = designManager;
_saveService = saveService;
_designManager.DesignChange += OnDataChange;
Changed += OnChange;
Reload();
}
private void Reload()
{
if (Load(new FileInfo(DesignFileSystemFile), _designManager.Designs, DesignToIdentifier, DesignToName))
_saveService.ImmediateSave(this);
Glamourer.Log.Debug("Reloaded design filesystem.");
}
public void Dispose()
{
_designManager.DesignChange -= OnDataChange;
}
public struct CreationDate : ISortMode<Design>
{
public string Name
=> "Creation Date (Older First)";
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their creation date.";
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.CreationDate));
}
public struct InverseCreationDate : ISortMode<Design>
{
public string Name
=> "Creation Date (Newer First)";
public string Description
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse creation date.";
public IEnumerable<IPath> GetChildren(Folder f)
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.CreationDate));
}
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
{
if (type != FileSystemChangeType.Reload)
_saveService.QueueSave(this);
}
private void OnDataChange(DesignManager.DesignChangeType type, Design design, object? data)
{
switch (type)
{
case DesignManager.DesignChangeType.Created:
var originalName = design.Name.Text.FixName();
var name = originalName;
var counter = 1;
while (Find(name, out _))
name = $"{originalName} ({++counter})";
CreateLeaf(Root, name, design);
break;
case DesignManager.DesignChangeType.Deleted:
if (FindLeaf(design, out var leaf))
Delete(leaf);
break;
case DesignManager.DesignChangeType.ReloadedAll:
Reload();
break;
case DesignManager.DesignChangeType.Renamed when data is string oldName:
var old = oldName.FixName();
if (Find(old, out var child) && child is not Folder)
Rename(child, design.Name);
break;
}
}
// Used for saving and loading.
private static string DesignToIdentifier(Design design)
=> design.Identifier.ToString();
private static string DesignToName(Design design)
=> design.Name.Text.FixName();
private static bool DesignHasDefaultPath(Design design, string fullPath)
{
var regex = new Regex($@"^{Regex.Escape(DesignToName(design))}( \(\d+\))?$");
return regex.IsMatch(fullPath);
}
private static (string, bool) SaveDesign(Design design, string fullPath)
// Only save pairs with non-default paths.
=> DesignHasDefaultPath(design, fullPath)
? (string.Empty, false)
: (DesignToIdentifier(design), true);
// Search the entire filesystem for the leaf corresponding to a design.
public bool FindLeaf(Design design, [NotNullWhen(true)] out Leaf? leaf)
{
leaf = Root.GetAllDescendants(ISortMode<Design>.Lexicographical)
.OfType<Leaf>()
.FirstOrDefault(l => l.Value == design);
return leaf != null;
}
internal static void MigrateOldPaths(DalamudPluginInterface pi, Dictionary<string, string> oldPaths)
{
if (oldPaths.Count == 0)
return;
var file = GetDesignFileSystemFile(pi);
try
{
JObject jObject;
if (File.Exists(file))
{
var text = File.ReadAllText(file);
jObject = JObject.Parse(text);
var dict = jObject["Data"]?.ToObject<Dictionary<string, string>>();
if (dict != null)
foreach (var (key, value) in dict)
oldPaths.TryAdd(key, value);
jObject["Data"] = JToken.FromObject(oldPaths);
}
else
{
jObject = new JObject
{
["Data"] = JToken.FromObject(oldPaths),
["EmptyFolders"] = JToken.FromObject(Array.Empty<string>()),
};
}
var data = jObject.ToString(Formatting.Indented);
File.WriteAllText(file, data);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not migrate old folder paths to new version:\n{ex}");
}
}
public string ToFilename(FilenameService fileNames)
=> fileNames.DesignFileSystem;
public void Save(StreamWriter writer)
{
SaveToFile(writer, SaveDesign, true);
}
}

View file

@ -0,0 +1,371 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Dalamud.Utility;
using Glamourer.Customization;
using Glamourer.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignManager
{
public const string DesignFolderName = "designs";
public readonly string DesignFolder;
private readonly ItemManager _items;
private readonly SaveService _saveService;
private readonly List<Design> _designs = new();
public enum DesignChangeType
{
Created,
Deleted,
ReloadedAll,
Renamed,
ChangedDescription,
AddedTag,
RemovedTag,
ChangedTag,
Customize,
Equip,
Weapon,
Stain,
ApplyCustomize,
ApplyEquip,
Other,
}
public delegate void DesignChangeDelegate(DesignChangeType type, Design design, object? changeData = null);
public event DesignChangeDelegate? DesignChange;
public IReadOnlyList<Design> Designs
=> _designs;
public DesignManager(DalamudPluginInterface pi, SaveService saveService, ItemManager items)
{
_saveService = saveService;
_items = items;
DesignFolder = SetDesignFolder(pi);
LoadDesigns();
MigrateOldDesigns(pi, Path.Combine(new DirectoryInfo(DesignFolder).Parent!.FullName, "Designs.json"));
}
private static string SetDesignFolder(DalamudPluginInterface pi)
{
var ret = Path.Combine(pi.GetPluginConfigDirectory(), DesignFolderName);
if (Directory.Exists(ret))
return ret;
try
{
Directory.CreateDirectory(ret);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not create design folder directory at {ret}:\n{ex}");
}
return ret;
}
public void LoadDesigns()
{
_designs.Clear();
List<(Design, string)> invalidNames = new();
var skipped = 0;
foreach (var file in new DirectoryInfo(DesignFolder).EnumerateFiles("*.json", SearchOption.TopDirectoryOnly))
{
try
{
var text = File.ReadAllText(file.FullName);
var data = JObject.Parse(text);
var design = Design.LoadDesign(_items, data, out var changes);
if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(file.Name))
invalidNames.Add((design, file.FullName));
if (_designs.Any(f => f.Identifier == design.Identifier))
throw new Exception($"Identifier {design.Identifier} was not unique.");
// TODO something when changed?
design.Index = _designs.Count;
_designs.Add(design);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not load design, skipped:\n{ex}");
++skipped;
}
}
var failed = 0;
foreach (var (design, name) in invalidNames)
{
try
{
var correctName = _saveService.FileNames.DesignFile(design);
File.Move(name, correctName, false);
Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
}
catch (Exception ex)
{
++failed;
Glamourer.Log.Error($"Failed to move invalid design file from {Path.GetFileName(name)}:\n{ex}");
}
}
if (invalidNames.Count > 0)
Glamourer.Log.Information(
$"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}");
Glamourer.Log.Information(
$"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
DesignChange?.Invoke(DesignChangeType.ReloadedAll, null!);
}
public Design Create(string name)
{
var design = new Design(_items)
{
CreationDate = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Index = _designs.Count,
Name = name,
};
_designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
_saveService.ImmediateSave(design);
DesignChange?.Invoke(DesignChangeType.Created, design);
return design;
}
public void Delete(Design design)
{
_designs.RemoveAt(design.Index);
foreach (var d in _designs.Skip(design.Index + 1))
--d.Index;
_saveService.ImmediateDelete(design);
}
public void Rename(Design design, string newName)
{
var oldName = design.Name.Text;
_saveService.ImmediateDelete(design);
design.Name = newName;
Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
_saveService.ImmediateSave(design);
DesignChange?.Invoke(DesignChangeType.Renamed, design, oldName);
}
public void ChangeDescription(Design design, string description)
{
design.Description = description;
Glamourer.Log.Debug($"Changed description of design {design.Identifier}.");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.ChangedDescription, design);
}
public void AddTag(Design design, string tag)
{
if (design.Tags.Contains(tag))
return;
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
var idx = design.Tags.IndexOf(tag);
Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}.");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.AddedTag, design);
}
public void RemoveTag(Design design, string tag)
{
var idx = design.Tags.IndexOf(tag);
if (idx >= 0)
RemoveTag(design, idx);
}
public void RemoveTag(Design design, int tagIdx)
{
var oldTag = design.Tags[tagIdx];
design.Tags = design.Tags.Take(tagIdx).Concat(design.Tags.Skip(tagIdx + 1)).ToArray();
Glamourer.Log.Debug($"Removed tag {oldTag} at {tagIdx} from design {design.Identifier}.");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.RemovedTag, design);
}
public void RenameTag(Design design, int tagIdx, string newTag)
{
var oldTag = design.Tags[tagIdx];
if (oldTag == newTag)
return;
design.Tags[tagIdx] = newTag;
Array.Sort(design.Tags);
Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.ChangedTag, design);
}
public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value)
{
var old = design.GetCustomize(idx);
if (!design.SetCustomize(idx, value))
return;
Glamourer.Log.Debug($"Changed customize {idx} in design {design.Identifier} from {old.Value} to {value.Value}");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.Customize, design, idx);
}
public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
{
if (!design.SetApplyCustomize(idx, value))
return;
Glamourer.Log.Debug($"Set applying of customization {idx} to {value}.");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.ApplyCustomize, design, idx);
}
public void ChangeEquip(Design design, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var old = design.Armor(slot);
if (!design.SetArmor(_items, slot, itemId, item))
return;
var n = design.Armor(slot);
Glamourer.Log.Debug(
$"Set {slot} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId}).");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.Equip, design, slot);
}
public void ChangeWeapon(Design design, uint itemId, EquipSlot offhand, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var (old, change, n) = offhand == EquipSlot.OffHand
? (design.WeaponOff, design.SetOffhand(_items, itemId, item), design.WeaponOff)
: (design.WeaponMain, design.SetMainhand(_items, itemId, item), design.WeaponMain);
if (!change)
return;
Glamourer.Log.Debug(
$"Set {offhand} weapon in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId}).");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.Weapon, design, offhand);
}
public void ChangeApplyEquip(Design design, EquipSlot slot, bool value)
{
if (!design.SetApplyEquip(slot, value))
return;
Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}.");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.ApplyEquip, design, slot);
}
public void ChangeStain(Design design, EquipSlot slot, StainId stain)
{
if (!design.SetStain(slot, stain))
return;
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}.");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.Stain, design, slot);
}
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
{
if (!design.SetApplyStain(slot, value))
return;
Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}.");
_saveService.QueueSave(design);
DesignChange?.Invoke(DesignChangeType.Stain, design, slot);
}
private Guid CreateNewGuid()
{
while (true)
{
var guid = Guid.NewGuid();
if (_designs.All(d => d.Identifier != guid))
return guid;
}
}
private bool Add(Design design, string? message)
{
if (_designs.Any(d => d == design || d.Identifier == design.Identifier))
return false;
design.Index = _designs.Count;
_designs.Add(design);
if (!message.IsNullOrEmpty())
Glamourer.Log.Debug(message);
_saveService.ImmediateSave(design);
DesignChange?.Invoke(DesignChangeType.Created, design);
return true;
}
private void MigrateOldDesigns(DalamudPluginInterface pi, string filePath)
{
if (!File.Exists(filePath))
return;
var errors = 0;
var successes = 0;
try
{
var text = File.ReadAllText(filePath);
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(text) ?? new Dictionary<string, string>();
var migratedFileSystemPaths = new Dictionary<string, string>(dict.Count);
foreach (var (name, base64) in dict)
{
try
{
var actualName = Path.GetFileName(name);
var design = new Design(_items)
{
CreationDate = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Name = actualName,
};
design.MigrateBase64(_items, base64);
Add(design, $"Migrated old design to {design.Identifier}.");
migratedFileSystemPaths.Add(design.Identifier.ToString(), name);
++successes;
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}");
++errors;
}
}
DesignFileSystem.MigrateOldPaths(pi, migratedFileSystemPaths);
Glamourer.Log.Information($"Successfully migrated {successes} old designs. Failed to migrate {errors} designs.");
}
catch (Exception e)
{
Glamourer.Log.Error($"Could not migrate old design file {filePath}:\n{e}");
}
try
{
File.Move(filePath, Path.ChangeExtension(filePath, ".json.bak"));
Glamourer.Log.Information($"Moved migrated design file {filePath} to backup file.");
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not move migrated design file {filePath} to backup file:\n{ex}");
}
}
}

View file

@ -0,0 +1,74 @@
using System;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
[Flags]
public enum EquipFlag : uint
{
Head = 0x00000001,
Body = 0x00000002,
Hands = 0x00000004,
Legs = 0x00000008,
Feet = 0x00000010,
Ears = 0x00000020,
Neck = 0x00000040,
Wrist = 0x00000080,
RFinger = 0x00000100,
LFinger = 0x00000200,
Mainhand = 0x00000400,
Offhand = 0x00000800,
HeadStain = 0x00001000,
BodyStain = 0x00002000,
HandsStain = 0x00004000,
LegsStain = 0x00008000,
FeetStain = 0x00010000,
EarsStain = 0x00020000,
NeckStain = 0x00040000,
WristStain = 0x00080000,
RFingerStain = 0x00100000,
LFingerStain = 0x00200000,
MainhandStain = 0x00400000,
OffhandStain = 0x00800000,
}
public static class EquipFlagExtensions
{
public 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,496 @@
using System;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.String.Functions;
namespace Glamourer.Designs;
public struct ItemPiece
{
public string Name;
public uint ItemId;
public SetId ModelId;
}
[Flags]
public enum ModelFlags : ushort
{
HatVisible = 0x01,
WeaponVisible = 0x02,
VisorToggled = 0x04,
}
public struct ModelData
{
public readonly unsafe ModelData Clone()
{
var data = new ModelData(MainHand);
fixed (void* ptr = &this)
{
MemoryUtility.MemCpyUnchecked(&data, ptr, sizeof(ModelData));
}
return data;
}
public Customize Customize = Customize.Default;
public ModelFlags Flags = ModelFlags.HatVisible | ModelFlags.WeaponVisible;
public CharacterWeapon MainHand;
public CharacterWeapon OffHand = CharacterWeapon.Empty;
public uint ModelId = 0;
public CharacterArmor Head = CharacterArmor.Empty;
public CharacterArmor Body = CharacterArmor.Empty;
public CharacterArmor Hands = CharacterArmor.Empty;
public CharacterArmor Legs = CharacterArmor.Empty;
public CharacterArmor Feet = CharacterArmor.Empty;
public CharacterArmor Ears = CharacterArmor.Empty;
public CharacterArmor Neck = CharacterArmor.Empty;
public CharacterArmor Wrists = CharacterArmor.Empty;
public CharacterArmor RFinger = CharacterArmor.Empty;
public CharacterArmor LFinger = CharacterArmor.Empty;
public ModelData(CharacterWeapon mainHand)
=> MainHand = mainHand;
public readonly CharacterArmor Armor(EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => MainHand.ToArmor(),
EquipSlot.OffHand => OffHand.ToArmor(),
EquipSlot.Head => Head,
EquipSlot.Body => Body,
EquipSlot.Hands => Hands,
EquipSlot.Legs => Legs,
EquipSlot.Feet => Feet,
EquipSlot.Ears => Ears,
EquipSlot.Neck => Neck,
EquipSlot.Wrists => Wrists,
EquipSlot.RFinger => RFinger,
EquipSlot.LFinger => LFinger,
_ => CharacterArmor.Empty,
};
public readonly CharacterWeapon Piece(EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => MainHand,
EquipSlot.OffHand => OffHand,
EquipSlot.Head => Head.ToWeapon(),
EquipSlot.Body => Body.ToWeapon(),
EquipSlot.Hands => Hands.ToWeapon(),
EquipSlot.Legs => Legs.ToWeapon(),
EquipSlot.Feet => Feet.ToWeapon(),
EquipSlot.Ears => Ears.ToWeapon(),
EquipSlot.Neck => Neck.ToWeapon(),
EquipSlot.Wrists => Wrists.ToWeapon(),
EquipSlot.RFinger => RFinger.ToWeapon(),
EquipSlot.LFinger => LFinger.ToWeapon(),
_ => CharacterWeapon.Empty,
};
public bool SetPiece(EquipSlot slot, SetId model, byte variant, out CharacterWeapon ret)
{
var changes = false;
switch (slot)
{
case EquipSlot.MainHand:
changes |= SetIfDifferent(ref MainHand.Set, model);
changes |= SetIfDifferent(ref MainHand.Variant, variant);
ret = MainHand;
return changes;
case EquipSlot.OffHand:
changes |= SetIfDifferent(ref OffHand.Set, model);
changes |= SetIfDifferent(ref OffHand.Variant, variant);
ret = OffHand;
return changes;
case EquipSlot.Head:
changes |= SetIfDifferent(ref Head.Set, model);
changes |= SetIfDifferent(ref Head.Variant, variant);
ret = Head.ToWeapon();
return changes;
case EquipSlot.Body:
changes |= SetIfDifferent(ref Body.Set, model);
changes |= SetIfDifferent(ref Body.Variant, variant);
ret = Body.ToWeapon();
return changes;
case EquipSlot.Hands:
changes |= SetIfDifferent(ref Hands.Set, model);
changes |= SetIfDifferent(ref Hands.Variant, variant);
ret = Hands.ToWeapon();
return changes;
case EquipSlot.Legs:
changes |= SetIfDifferent(ref Legs.Set, model);
changes |= SetIfDifferent(ref Legs.Variant, variant);
ret = Legs.ToWeapon();
return changes;
case EquipSlot.Feet:
changes |= SetIfDifferent(ref Feet.Set, model);
changes |= SetIfDifferent(ref Feet.Variant, variant);
ret = Feet.ToWeapon();
return changes;
case EquipSlot.Ears:
changes |= SetIfDifferent(ref Ears.Set, model);
changes |= SetIfDifferent(ref Ears.Variant, variant);
ret = Ears.ToWeapon();
return changes;
case EquipSlot.Neck:
changes |= SetIfDifferent(ref Neck.Set, model);
changes |= SetIfDifferent(ref Neck.Variant, variant);
ret = Neck.ToWeapon();
return changes;
case EquipSlot.Wrists:
changes |= SetIfDifferent(ref Wrists.Set, model);
changes |= SetIfDifferent(ref Wrists.Variant, variant);
ret = Wrists.ToWeapon();
return changes;
case EquipSlot.RFinger:
changes |= SetIfDifferent(ref RFinger.Set, model);
changes |= SetIfDifferent(ref RFinger.Variant, variant);
ret = RFinger.ToWeapon();
return changes;
case EquipSlot.LFinger:
changes |= SetIfDifferent(ref LFinger.Set, model);
changes |= SetIfDifferent(ref LFinger.Variant, variant);
ret = LFinger.ToWeapon();
return changes;
default:
ret = CharacterWeapon.Empty;
return changes;
}
}
public bool SetPiece(EquipSlot slot, SetId model, WeaponType type, byte variant, out CharacterWeapon ret)
{
var changes = false;
switch (slot)
{
case EquipSlot.MainHand:
changes |= SetIfDifferent(ref MainHand.Set, model);
changes |= SetIfDifferent(ref MainHand.Type, type);
changes |= SetIfDifferent(ref MainHand.Variant, variant);
ret = MainHand;
return changes;
case EquipSlot.OffHand:
changes |= SetIfDifferent(ref OffHand.Set, model);
changes |= SetIfDifferent(ref OffHand.Type, type);
changes |= SetIfDifferent(ref OffHand.Variant, variant);
ret = OffHand;
return changes;
case EquipSlot.Head:
changes |= SetIfDifferent(ref Head.Set, model);
changes |= SetIfDifferent(ref Head.Variant, variant);
ret = Head.ToWeapon();
return changes;
case EquipSlot.Body:
changes |= SetIfDifferent(ref Body.Set, model);
changes |= SetIfDifferent(ref Body.Variant, variant);
ret = Body.ToWeapon();
return changes;
case EquipSlot.Hands:
changes |= SetIfDifferent(ref Hands.Set, model);
changes |= SetIfDifferent(ref Hands.Variant, variant);
ret = Hands.ToWeapon();
return changes;
case EquipSlot.Legs:
changes |= SetIfDifferent(ref Legs.Set, model);
changes |= SetIfDifferent(ref Legs.Variant, variant);
ret = Legs.ToWeapon();
return changes;
case EquipSlot.Feet:
changes |= SetIfDifferent(ref Feet.Set, model);
changes |= SetIfDifferent(ref Feet.Variant, variant);
ret = Feet.ToWeapon();
return changes;
case EquipSlot.Ears:
changes |= SetIfDifferent(ref Ears.Set, model);
changes |= SetIfDifferent(ref Ears.Variant, variant);
ret = Ears.ToWeapon();
return changes;
case EquipSlot.Neck:
changes |= SetIfDifferent(ref Neck.Set, model);
changes |= SetIfDifferent(ref Neck.Variant, variant);
ret = Neck.ToWeapon();
return changes;
case EquipSlot.Wrists:
changes |= SetIfDifferent(ref Wrists.Set, model);
changes |= SetIfDifferent(ref Wrists.Variant, variant);
ret = Wrists.ToWeapon();
return changes;
case EquipSlot.RFinger:
changes |= SetIfDifferent(ref RFinger.Set, model);
changes |= SetIfDifferent(ref RFinger.Variant, variant);
ret = RFinger.ToWeapon();
return changes;
case EquipSlot.LFinger:
changes |= SetIfDifferent(ref LFinger.Set, model);
changes |= SetIfDifferent(ref LFinger.Variant, variant);
ret = LFinger.ToWeapon();
return changes;
default:
ret = CharacterWeapon.Empty;
return changes;
}
}
public bool SetPiece(EquipSlot slot, SetId model, byte variant, StainId stain, out CharacterWeapon ret)
{
var changes = false;
switch (slot)
{
case EquipSlot.MainHand:
changes |= SetIfDifferent(ref MainHand.Set, model);
changes |= SetIfDifferent(ref MainHand.Variant, variant);
changes |= SetIfDifferent(ref MainHand.Stain, stain);
ret = MainHand;
return changes;
case EquipSlot.OffHand:
changes |= SetIfDifferent(ref OffHand.Set, model);
changes |= SetIfDifferent(ref OffHand.Variant, variant);
changes |= SetIfDifferent(ref OffHand.Stain, stain);
ret = OffHand;
return changes;
case EquipSlot.Head:
changes |= SetIfDifferent(ref Head.Set, model);
changes |= SetIfDifferent(ref Head.Variant, variant);
changes |= SetIfDifferent(ref Head.Stain, stain);
ret = Head.ToWeapon();
return changes;
case EquipSlot.Body:
changes |= SetIfDifferent(ref Body.Set, model);
changes |= SetIfDifferent(ref Body.Variant, variant);
changes |= SetIfDifferent(ref Body.Stain, stain);
ret = Body.ToWeapon();
return changes;
case EquipSlot.Hands:
changes |= SetIfDifferent(ref Hands.Set, model);
changes |= SetIfDifferent(ref Hands.Variant, variant);
changes |= SetIfDifferent(ref Hands.Stain, stain);
ret = Hands.ToWeapon();
return changes;
case EquipSlot.Legs:
changes |= SetIfDifferent(ref Legs.Set, model);
changes |= SetIfDifferent(ref Legs.Variant, variant);
changes |= SetIfDifferent(ref Legs.Stain, stain);
ret = Legs.ToWeapon();
return changes;
case EquipSlot.Feet:
changes |= SetIfDifferent(ref Feet.Set, model);
changes |= SetIfDifferent(ref Feet.Variant, variant);
changes |= SetIfDifferent(ref Feet.Stain, stain);
ret = Feet.ToWeapon();
return changes;
case EquipSlot.Ears:
changes |= SetIfDifferent(ref Ears.Set, model);
changes |= SetIfDifferent(ref Ears.Variant, variant);
changes |= SetIfDifferent(ref Ears.Stain, stain);
ret = Ears.ToWeapon();
return changes;
case EquipSlot.Neck:
changes |= SetIfDifferent(ref Neck.Set, model);
changes |= SetIfDifferent(ref Neck.Variant, variant);
changes |= SetIfDifferent(ref Neck.Stain, stain);
ret = Neck.ToWeapon();
return changes;
case EquipSlot.Wrists:
changes |= SetIfDifferent(ref Wrists.Set, model);
changes |= SetIfDifferent(ref Wrists.Variant, variant);
changes |= SetIfDifferent(ref Wrists.Stain, stain);
ret = Wrists.ToWeapon();
return changes;
case EquipSlot.RFinger:
changes |= SetIfDifferent(ref RFinger.Set, model);
changes |= SetIfDifferent(ref RFinger.Variant, variant);
changes |= SetIfDifferent(ref RFinger.Stain, stain);
ret = RFinger.ToWeapon();
return changes;
case EquipSlot.LFinger:
changes |= SetIfDifferent(ref LFinger.Set, model);
changes |= SetIfDifferent(ref LFinger.Variant, variant);
changes |= SetIfDifferent(ref LFinger.Stain, stain);
ret = LFinger.ToWeapon();
return changes;
default:
ret = CharacterWeapon.Empty;
return changes;
}
}
public bool SetPiece(EquipSlot slot, CharacterWeapon weapon, out CharacterWeapon ret)
=> SetPiece(slot, weapon.Set, weapon.Type, (byte)weapon.Variant, weapon.Stain, out ret);
public bool SetPiece(EquipSlot slot, CharacterArmor armor, out CharacterWeapon ret)
=> SetPiece(slot, armor.Set, armor.Variant, armor.Stain, out ret);
public bool SetPiece(EquipSlot slot, SetId model, WeaponType type, byte variant, StainId stain, out CharacterWeapon ret)
{
var changes = false;
switch (slot)
{
case EquipSlot.MainHand:
changes |= SetIfDifferent(ref MainHand.Set, model);
changes |= SetIfDifferent(ref MainHand.Type, type);
changes |= SetIfDifferent(ref MainHand.Variant, variant);
changes |= SetIfDifferent(ref MainHand.Stain, stain);
ret = MainHand;
return changes;
case EquipSlot.OffHand:
changes |= SetIfDifferent(ref OffHand.Set, model);
changes |= SetIfDifferent(ref OffHand.Type, type);
changes |= SetIfDifferent(ref OffHand.Variant, variant);
changes |= SetIfDifferent(ref OffHand.Stain, stain);
ret = OffHand;
return changes;
case EquipSlot.Head:
changes |= SetIfDifferent(ref Head.Set, model);
changes |= SetIfDifferent(ref Head.Variant, variant);
changes |= SetIfDifferent(ref Head.Stain, stain);
ret = Head.ToWeapon();
return changes;
case EquipSlot.Body:
changes |= SetIfDifferent(ref Body.Set, model);
changes |= SetIfDifferent(ref Body.Variant, variant);
changes |= SetIfDifferent(ref Body.Stain, stain);
ret = Body.ToWeapon();
return changes;
case EquipSlot.Hands:
changes |= SetIfDifferent(ref Hands.Set, model);
changes |= SetIfDifferent(ref Hands.Variant, variant);
changes |= SetIfDifferent(ref Hands.Stain, stain);
ret = Hands.ToWeapon();
return changes;
case EquipSlot.Legs:
changes |= SetIfDifferent(ref Legs.Set, model);
changes |= SetIfDifferent(ref Legs.Variant, variant);
changes |= SetIfDifferent(ref Legs.Stain, stain);
ret = Legs.ToWeapon();
return changes;
case EquipSlot.Feet:
changes |= SetIfDifferent(ref Feet.Set, model);
changes |= SetIfDifferent(ref Feet.Variant, variant);
changes |= SetIfDifferent(ref Feet.Stain, stain);
ret = Feet.ToWeapon();
return changes;
case EquipSlot.Ears:
changes |= SetIfDifferent(ref Ears.Set, model);
changes |= SetIfDifferent(ref Ears.Variant, variant);
changes |= SetIfDifferent(ref Ears.Stain, stain);
ret = Ears.ToWeapon();
return changes;
case EquipSlot.Neck:
changes |= SetIfDifferent(ref Neck.Set, model);
changes |= SetIfDifferent(ref Neck.Variant, variant);
changes |= SetIfDifferent(ref Neck.Stain, stain);
ret = Neck.ToWeapon();
return changes;
case EquipSlot.Wrists:
changes |= SetIfDifferent(ref Wrists.Set, model);
changes |= SetIfDifferent(ref Wrists.Variant, variant);
changes |= SetIfDifferent(ref Wrists.Stain, stain);
ret = Wrists.ToWeapon();
return changes;
case EquipSlot.RFinger:
changes |= SetIfDifferent(ref RFinger.Set, model);
changes |= SetIfDifferent(ref RFinger.Variant, variant);
changes |= SetIfDifferent(ref RFinger.Stain, stain);
ret = RFinger.ToWeapon();
return changes;
case EquipSlot.LFinger:
changes |= SetIfDifferent(ref LFinger.Set, model);
changes |= SetIfDifferent(ref LFinger.Variant, variant);
changes |= SetIfDifferent(ref LFinger.Stain, stain);
ret = LFinger.ToWeapon();
return changes;
default:
ret = CharacterWeapon.Empty;
return changes;
}
}
public StainId Stain(EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => MainHand.Stain,
EquipSlot.OffHand => OffHand.Stain,
EquipSlot.Head => Head.Stain,
EquipSlot.Body => Body.Stain,
EquipSlot.Hands => Hands.Stain,
EquipSlot.Legs => Legs.Stain,
EquipSlot.Feet => Feet.Stain,
EquipSlot.Ears => Ears.Stain,
EquipSlot.Neck => Neck.Stain,
EquipSlot.Wrists => Wrists.Stain,
EquipSlot.RFinger => RFinger.Stain,
EquipSlot.LFinger => LFinger.Stain,
_ => 0,
};
public bool SetStain(EquipSlot slot, StainId stain, out CharacterWeapon ret)
{
var changes = false;
switch (slot)
{
case EquipSlot.MainHand:
changes = SetIfDifferent(ref MainHand.Stain, stain);
ret = MainHand;
return changes;
case EquipSlot.OffHand:
changes = SetIfDifferent(ref OffHand.Stain, stain);
ret = OffHand;
return changes;
case EquipSlot.Head:
changes = SetIfDifferent(ref Head.Stain, stain);
ret = Head.ToWeapon();
return changes;
case EquipSlot.Body:
changes = SetIfDifferent(ref Body.Stain, stain);
ret = Body.ToWeapon();
return changes;
case EquipSlot.Hands:
changes = SetIfDifferent(ref Hands.Stain, stain);
ret = Hands.ToWeapon();
return changes;
case EquipSlot.Legs:
changes = SetIfDifferent(ref Legs.Stain, stain);
ret = Legs.ToWeapon();
return changes;
case EquipSlot.Feet:
changes = SetIfDifferent(ref Feet.Stain, stain);
ret = Feet.ToWeapon();
return changes;
case EquipSlot.Ears:
changes = SetIfDifferent(ref Ears.Stain, stain);
ret = Ears.ToWeapon();
return changes;
case EquipSlot.Neck:
changes = SetIfDifferent(ref Neck.Stain, stain);
ret = Neck.ToWeapon();
return changes;
case EquipSlot.Wrists:
changes = SetIfDifferent(ref Wrists.Stain, stain);
ret = Wrists.ToWeapon();
return changes;
case EquipSlot.RFinger:
changes = SetIfDifferent(ref RFinger.Stain, stain);
ret = RFinger.ToWeapon();
return changes;
case EquipSlot.LFinger:
changes = SetIfDifferent(ref LFinger.Stain, stain);
ret = LFinger.ToWeapon();
return changes;
default:
ret = CharacterWeapon.Empty;
return false;
}
}
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,105 @@
using Dalamud.Utility;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public readonly struct Item
{
public readonly string Name;
public readonly uint ItemId;
public readonly CharacterArmor Model;
public SetId ModelBase
=> Model.Set;
public byte Variant
=> Model.Variant;
public StainId Stain
=> Model.Stain;
public Item WithStain(StainId id)
=> new(Name, ItemId, Model with { Stain = id });
public Item(string name, uint itemId, CharacterArmor armor)
{
Name = name;
ItemId = itemId;
Model.Set = armor.Set;
Model.Variant = armor.Variant;
Model.Stain = armor.Stain;
}
public Item(Lumina.Excel.GeneratedSheets.Item item)
{
Name = string.Intern(item.Name.ToDalamudString().TextValue);
ItemId = item.RowId;
Model.Set = (SetId)item.ModelMain;
Model.Variant = (byte)(item.ModelMain >> 16);
}
}
public readonly struct Weapon
{
public readonly string Name = string.Empty;
public readonly uint ItemId;
public readonly FullEquipType Type;
public readonly bool Valid;
public readonly CharacterWeapon Model;
public SetId ModelBase
=> Model.Set;
public WeaponType WeaponBase
=> Model.Type;
public byte Variant
=> (byte)Model.Variant;
public StainId Stain
=> Model.Stain;
public Weapon WithStain(StainId id)
=> new(Name, ItemId, Model with { Stain = id }, Type);
public Weapon(string name, uint itemId, CharacterWeapon weapon, FullEquipType type)
{
Name = name;
ItemId = itemId;
Type = type;
Valid = true;
Model.Set = weapon.Set;
Model.Type = weapon.Type;
Model.Variant = (byte)weapon.Variant;
Model.Stain = weapon.Stain;
}
public static Weapon Offhand(string name, uint itemId, CharacterWeapon weapon, FullEquipType type)
{
var offType = type.Offhand();
return offType is FullEquipType.Unknown
? new Weapon()
: new Weapon(name, itemId, weapon, offType);
}
public Weapon(Lumina.Excel.GeneratedSheets.Item item, bool offhand)
{
Name = string.Intern(item.Name.ToDalamudString().TextValue);
ItemId = item.RowId;
Type = item.ToEquipType();
var offType = Type.Offhand();
var model = offhand && offType == Type ? item.ModelSub : item.ModelMain;
Valid = Type.ToSlot() switch
{
EquipSlot.MainHand when !offhand => true,
EquipSlot.MainHand when offhand && offType == Type => true,
EquipSlot.OffHand when offhand => true,
_ => false,
};
Model.Set = (SetId)model;
Model.Type = (WeaponType)(model >> 16);
Model.Variant = (byte)(model >> 32);
}
}

View file

@ -0,0 +1,84 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Api;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer;
public class DrawObjectManager : IDisposable
{
private readonly ItemManager _items;
private readonly ActorService _actors;
private readonly ActiveDesign.Manager _manager;
private readonly PenumbraAttach _penumbra;
public DrawObjectManager(ItemManager items, ActorService actors, ActiveDesign.Manager manager,
PenumbraAttach penumbra)
{
_items = items;
_actors = actors;
_manager = manager;
_penumbra = penumbra;
//_interop.EquipUpdate += FixEquipment;
_penumbra.CreatingCharacterBase += ApplyActiveDesign;
}
private void FixEquipment(DrawObject drawObject, EquipSlot slot, ref CharacterArmor item)
{
var customize = drawObject.Customize;
var (changed, newArmor) = _items.ResolveRestrictedGear(item, slot, customize.Race, customize.Gender);
if (!changed)
return;
Glamourer.Log.Verbose(
$"Invalid armor {item.Set.Value}-{item.Variant} for {customize.Gender} {customize.Race} changed to {newArmor.Set.Value}-{newArmor.Variant}.");
item = newArmor;
}
private unsafe void ApplyActiveDesign(nint gameObjectPtr, string collectionName, nint modelIdPtr, nint customizePtr, nint equipDataPtr)
{
var gameObject = (Actor)gameObjectPtr;
ref var modelId = ref *(uint*)modelIdPtr;
// Do not apply anything if the game object model id does not correspond to the draw object model id.
// This is the case if the actor is transformed to a different creature.
if (gameObject.ModelId != modelId)
return;
var identifier = _actors.AwaitedService.FromObject((GameObject*)gameObjectPtr, out _, true, true, false);
if (!identifier.IsValid || !_manager.TryGetValue(identifier, out var design))
return;
// Compare game object customize data against draw object customize data for transformations.
// Apply customization if they correspond and there is customization to apply.
var gameObjectCustomize = gameObject.Customize;
var customize = new Customize(*(CustomizeData*)customizePtr);
if (gameObjectCustomize.Equals(customize))
customize.Load(design.ModelData.Customize);
// Compare game object equip data against draw object equip data for transformations.
// Apply each piece of equip that should be applied if they correspond.
var gameObjectEquip = gameObject.Equip;
var equip = new CharacterEquip((CharacterArmor*)equipDataPtr);
if (gameObjectEquip.Equals(equip))
{
foreach (var slot in EquipSlotExtensions.EquipmentSlots)
{
(_, equip[slot]) =
_items.ResolveRestrictedGear(design.ModelData.Armor(slot), slot, customize.Race, customize.Gender);
}
}
}
public void Dispose()
{
_penumbra.CreatingCharacterBase -= ApplyActiveDesign;
//_interop.EquipUpdate -= FixEquipment;
}
}

View file

@ -0,0 +1,34 @@
using Glamourer.Interop;
using Glamourer.Structs;
namespace Glamourer.Fixed;
public struct FixedCondition
{
private const ulong _territoryFlag = 1ul << 32;
private const ulong _jobFlag = 1ul << 33;
private ulong _data;
public static FixedCondition TerritoryCondition(ushort territoryType)
=> new() { _data = territoryType | _territoryFlag };
public static FixedCondition JobCondition(JobGroup group)
=> new() { _data = group.Id | _jobFlag };
public bool Check(Actor actor)
{
//if ((_data & (_territoryFlag | _jobFlag)) == 0)
// return true;
//
//if ((_data & _territoryFlag) != 0)
// return Dalamud.ClientState.TerritoryType == (ushort)_data;
//
//if (actor && GameData.JobGroups(Dalamud.GameData).TryGetValue((ushort)_data, out var group) && group.Fits(actor.Job))
// return true;
//
return true;
}
public override string ToString()
=> _data.ToString();
}

View file

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

190
GlamourerOld/Glamourer.cs Normal file
View file

@ -0,0 +1,190 @@
using System.Reflection;
using Dalamud.Plugin;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.Services;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer;
public partial class Glamourer : IDalamudPlugin
{
public string Name
=> "Glamourer";
public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
public static readonly string CommitHash =
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "Unknown";
public static readonly Logger Log = new();
public static ChatService ChatService { get; private set; } = null!;
private readonly ServiceProvider _services;
public Glamourer(DalamudPluginInterface pluginInterface)
{
try
{
EventWrapper.ChangeLogger(Log);
_services = ServiceManager.CreateProvider(pluginInterface, Log);
ChatService = _services.GetRequiredService<ChatService>();
_services.GetRequiredService<BackupService>();
_services.GetRequiredService<GlamourerWindowSystem>();
_services.GetRequiredService<CommandService>();
_services.GetRequiredService<GlamourerIpc>();
_services.GetRequiredService<ChangeCustomizeService>();
_services.GetRequiredService<JobService>();
_services.GetRequiredService<UpdateSlotService>();
_services.GetRequiredService<VisorService>();
_services.GetRequiredService<WeaponService>();
_services.GetRequiredService<RedrawManager>();
}
catch
{
Dispose();
throw;
}
}
public void Dispose()
{
_services?.Dispose();
}
//private static GameObject? GetPlayer(string name)
//{
// var lowerName = name.ToLowerInvariant();
// return lowerName switch
// {
// "" => null,
// "<me>" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer,
// "self" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer,
// "<t>" => Dalamud.Targets.Target,
// "target" => Dalamud.Targets.Target,
// "<f>" => Dalamud.Targets.FocusTarget,
// "focus" => Dalamud.Targets.FocusTarget,
// "<mo>" => Dalamud.Targets.MouseOverTarget,
// "mouseover" => Dalamud.Targets.MouseOverTarget,
// _ => Dalamud.Objects.LastOrDefault(
// a => string.Equals(a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase)),
// };
//}
//
//public void CopyToClipboard(Character player)
//{
// var save = new CharacterSave();
// save.LoadCharacter(player);
// ImGui.SetClipboardText(save.ToBase64());
//}
//
//public void ApplyCommand(Character player, string target)
//{
// CharacterSave? save = null;
// if (target.ToLowerInvariant() == "clipboard")
// try
// {
// save = CharacterSave.FromString(ImGui.GetClipboardText());
// }
// catch (Exception)
// {
// Dalamud.Chat.PrintError("Clipboard does not contain a valid customization string.");
// }
// else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d)
// Dalamud.Chat.PrintError("The given path to a saved design does not exist or does not point to a design.");
// else
// save = d.Data;
//
// save?.Apply(player);
// Penumbra.UpdateCharacters(player);
//}
//
//public void SaveCommand(Character player, string path)
//{
// var save = new CharacterSave();
// save.LoadCharacter(player);
// try
// {
// var (folder, name) = Designs.FileSystem.CreateAllFolders(path);
// var design = new Design(folder, name) { Data = save };
// folder.FindOrAddChild(design);
// Designs.Designs.Add(design.FullName(), design.Data);
// Designs.SaveToFile();
// }
// catch (Exception e)
// {
// Dalamud.Chat.PrintError("Could not save file:");
// Dalamud.Chat.PrintError($" {e.Message}");
// }
//}
//
public void OnGlamour(string command, string arguments)
{
//static void PrintHelp()
//{
// Dalamud.Chat.Print("Usage:");
// Dalamud.Chat.Print($" {HelpString}");
//}
//arguments = arguments.Trim();
//if (!arguments.Any())
//{
// PrintHelp();
// return;
//}
//
//var split = arguments.Split(new[]
//{
// ',',
//}, 3, StringSplitOptions.RemoveEmptyEntries);
//
//if (split.Length < 2)
//{
// PrintHelp();
// return;
//}
//
//var player = GetPlayer(split[1]) as Character;
//if (player == null)
//{
// Dalamud.Chat.Print($"Could not find object for {split[1]} or it was not a Character.");
// return;
//}
//
//switch (split[0].ToLowerInvariant())
//{
// case "copy":
// CopyToClipboard(player);
// return;
// case "apply":
// {
// if (split.Length < 3)
// {
// Dalamud.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'.");
// return;
// }
//
// ApplyCommand(player, split[2]);
//
// return;
// }
// case "save":
// {
// if (split.Length < 3)
// {
// Dalamud.Chat.Print("Saving requires a name for the save.");
// return;
// }
//
// SaveCommand(player, split[2]);
// return;
// }
// default:
// PrintHelp();
// return;
//}
}
}

View file

@ -0,0 +1,126 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>GlamourerOld</AssemblyName>
<FileVersion>0.2.0.0</FileVersion>
<AssemblyVersion>0.2.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
</PropertyGroup>
<ItemGroup>
<None Remove="LegacyTattoo.raw" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="LegacyTattoo.raw" />
</ItemGroup>
<PropertyGroup>
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" />
</Exec>
<PropertyGroup>
<InformationalVersion>$(GitCommitHash)</InformationalVersion>
</PropertyGroup>
</Target>
<ItemGroup>
<None Update="Glamourer.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</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)" />
</Target>
</Project>

View file

@ -0,0 +1,57 @@
using Glamourer.Designs;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui;
public static class ActorDebug
{
/// <summary> Draw the model data values as straight table data without evaluation. </summary>
public static unsafe void Draw(in ModelData model)
{
using var table = ImRaii.Table("##drawObjectData", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
ImGuiUtil.DrawTableColumn("Model ID");
ImGuiUtil.DrawTableColumn(model.ModelId.ToString());
ImGuiUtil.DrawTableColumn(model.ModelId.ToString("X8"));
for (var i = 0; i < Penumbra.GameData.Structs.CustomizeData.Size; ++i)
{
ImGuiUtil.DrawTableColumn($"Customize[{i:D2}]");
ImGuiUtil.DrawTableColumn(model.Customize.Data.Data[i].ToString());
ImGuiUtil.DrawTableColumn(model.Customize.Data.Data[i].ToString("X2"));
}
ImGuiUtil.DrawTableColumn("Race");
ImGuiUtil.DrawTableColumn(model.Customize.Race.ToString());
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Clan");
ImGuiUtil.DrawTableColumn(model.Customize.Clan.ToString());
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Gender");
ImGuiUtil.DrawTableColumn(model.Customize.Gender.ToString());
ImGui.TableNextColumn();
for (var i = 0; i < 10; ++i)
{
var slot = EquipSlotExtensions.EqdpSlots[i];
ImGuiUtil.DrawTableColumn($"Equipment[{i}] ({slot})");
var armor = model.Armor(slot);
ImGuiUtil.DrawTableColumn($"{armor.Set.Value}, {armor.Variant}, {armor.Stain.Value}");
ImGuiUtil.DrawTableColumn(armor.Value.ToString("X8"));
}
ImGuiUtil.DrawTableColumn("Mainhand");
ImGuiUtil.DrawTableColumn($"{model.MainHand.Set.Value}, {model.MainHand.Type.Value}, {model.MainHand.Variant}, {model.MainHand.Stain.Value}");
ImGuiUtil.DrawTableColumn(model.MainHand.Value.ToString("X16"));
ImGuiUtil.DrawTableColumn("Offhand");
ImGuiUtil.DrawTableColumn($"{model.OffHand.Set.Value}, {model.OffHand.Type.Value}, {model.OffHand.Variant}, {model.OffHand.Stain.Value}");
ImGuiUtil.DrawTableColumn(model.OffHand.Value.ToString("X16"));
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Numerics;
using Glamourer.Customization;
using ImGuiNET;
using OtterGui.Raii;
namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer
{
private const string ColorPickerPopupName = "ColorPicker";
private void DrawColorPicker(CustomizeIndex index)
{
using var _ = SetId(index);
var (current, custom) = GetCurrentCustomization(index);
var color = ImGui.ColorConvertU32ToFloat4(custom.Color);
// Print 1-based index instead of 0.
if (ImGui.ColorButton($"{current + 1}##color", color, ImGuiColorEditFlags.None, _framedIconSize))
ImGui.OpenPopup(ColorPickerPopupName);
ImGui.SameLine();
using (var group = ImRaii.Group())
{
DataInputInt(current);
ImGui.TextUnformatted(_currentOption);
}
DrawColorPickerPopup();
}
private void DrawColorPickerPopup()
{
using var popup = ImRaii.Popup(ColorPickerPopupName, ImGuiWindowFlags.AlwaysAutoResize);
if (!popup)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
for (var i = 0; i < _currentCount; ++i)
{
var custom = _set.Data(_currentIndex, i, _customize[CustomizeIndex.Face]);
if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)))
{
UpdateValue(custom.Value);
ImGui.CloseCurrentPopup();
}
if (i % 8 != 7)
ImGui.SameLine();
}
}
// Obtain the current customization and print a warning if it is not known.
private (int, CustomizeData) GetCurrentCustomization(CustomizeIndex index)
{
var current = _set.DataByValue(index, _customize[index], out var custom, _customize.Face);
if (_set.IsAvailable(index) && current < 0)
throw new Exception($"Read invalid customization value {_customize[index]} for {index}.");
return (current, custom!.Value);
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Linq;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.Util;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer
{
private void DrawRaceGenderSelector()
{
DrawGenderSelector();
ImGui.SameLine();
using var group = ImRaii.Group();
DrawRaceCombo();
var gender = _service.AwaitedService.GetName(CustomName.Gender);
var clan = _service.AwaitedService.GetName(CustomName.Clan);
ImGui.TextUnformatted($"{gender} & {clan}");
}
private void DrawGenderSelector()
{
using var font = ImRaii.PushFont(UiBuilder.IconFont);
var icon = _customize.Gender switch
{
Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble,
Gender.Male => FontAwesomeIcon.Mars,
Gender.Female => FontAwesomeIcon.Venus,
_ => throw new Exception($"Gender value {_customize.Gender} is not a valid gender for a design."),
};
if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, icon == FontAwesomeIcon.MarsDouble, true))
return;
Changed |= _customize.ChangeGender(CharacterEquip.Null, _customize.Gender is Gender.Male ? Gender.Female : Gender.Male, _items, _service.AwaitedService);
}
private void DrawRaceCombo()
{
ImGui.SetNextItemWidth(_raceSelectorWidth);
using var combo = ImRaii.Combo("##subRaceCombo", _customize.ClanName(_service.AwaitedService));
if (!combo)
return;
foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown
{
if (ImGui.Selectable(CustomizeExtensions.ClanName(_service.AwaitedService, subRace, _customize.Gender), subRace == _customize.Clan))
Changed |= _customize.ChangeRace(CharacterEquip.Null, subRace, _items, _service.AwaitedService);
}
}
}

View file

@ -0,0 +1,150 @@
using System;
using System.Numerics;
using Glamourer.Customization;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer
{
private const string IconSelectorPopup = "Style Picker";
private void DrawIconSelector(CustomizeIndex index)
{
using var _ = SetId(index);
using var bigGroup = ImRaii.Group();
var label = _currentOption;
var current = _set.DataByValue(index, _currentByte, out var custom, _customize.Face);
if (current < 0)
{
label = $"{_currentOption} (Custom #{_customize[index]})";
current = 0;
custom = _set.Data(index, 0);
}
var icon = _service.AwaitedService.GetIcon(custom!.Value.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
ImGui.OpenPopup(IconSelectorPopup);
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
ImGui.SameLine();
using (var group = ImRaii.Group())
{
if (_currentIndex == CustomizeIndex.Face)
FaceInputInt(current);
else
DataInputInt(current);
ImGui.TextUnformatted($"{label} ({custom.Value.Value})");
}
DrawIconPickerPopup();
}
private bool UpdateFace(CustomizeData data)
{
// Hrothgar Hack
var value = _set.Race == Race.Hrothgar ? data.Value + 4 : data.Value;
if (_customize.Face == value)
return false;
_customize.Face = value;
Changed |= CustomizeFlag.Face;
return true;
}
private void FaceInputInt(int currentIndex)
{
++currentIndex;
ImGui.SetNextItemWidth(_inputIntSize);
if (ImGui.InputInt("##text", ref currentIndex, 1, 1))
{
currentIndex = Math.Clamp(currentIndex - 1, 0, _currentCount - 1);
var data = _set.Data(_currentIndex, currentIndex, _customize.Face);
UpdateFace(data);
}
ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]");
}
private void DrawIconPickerPopup()
{
using var popup = ImRaii.Popup(IconSelectorPopup, ImGuiWindowFlags.AlwaysAutoResize);
if (!popup)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
for (var i = 0; i < _currentCount; ++i)
{
var custom = _set.Data(_currentIndex, i, _customize.Face);
var icon = _service.AwaitedService.GetIcon(custom.IconId);
using (var _ = ImRaii.Group())
{
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
{
if (_currentIndex == CustomizeIndex.Face)
UpdateFace(custom);
else
UpdateValue(custom.Value);
ImGui.CloseCurrentPopup();
}
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
var text = custom.Value.ToString();
var textWidth = ImGui.CalcTextSize(text).X;
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (_iconSize.X - textWidth + 2 * ImGui.GetStyle().FramePadding.X) / 2);
ImGui.TextUnformatted(text);
}
if (i % 8 != 7)
ImGui.SameLine();
}
}
// Only used for facial features, so fixed ID.
private void DrawMultiIconSelector()
{
using var bigGroup = ImRaii.Group();
DrawMultiIcons();
ImGui.SameLine();
using var group = ImRaii.Group();
ImGui.Dummy(new Vector2(0, ImGui.GetTextLineHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y / 2));
_currentCount = 256;
PercentageInputInt();
ImGui.TextUnformatted(_set.Option(CustomizeIndex.LegacyTattoo));
}
private void DrawMultiIcons()
{
var options = _set.Order[CharaMakeParams.MenuType.IconCheckmark];
using var _ = ImRaii.Group();
foreach (var (featureIdx, idx) in options.WithIndex())
{
using var id = SetId(featureIdx);
var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero;
var feature = _set.Data(featureIdx, 0, _customize.Face);
var icon = featureIdx == CustomizeIndex.LegacyTattoo
? _legacyTattoo ?? _service.AwaitedService.GetIcon(feature.IconId)
: _service.AwaitedService.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X,
Vector4.Zero, enabled ? Vector4.One : _redTint))
{
_customize.Set(featureIdx, enabled ? CustomizeValue.Zero : CustomizeValue.Max);
Changed |= _currentFlag;
}
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
if (idx % 4 != 3)
ImGui.SameLine();
}
}
}

View file

@ -0,0 +1,102 @@
using System;
using Glamourer.Customization;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer
{
private void PercentageSelector(CustomizeIndex index)
{
using var _ = SetId(index);
using var bigGroup = ImRaii.Group();
DrawPercentageSlider();
ImGui.SameLine();
PercentageInputInt();
ImGui.SameLine();
ImGui.TextUnformatted(_currentOption);
}
private void DrawPercentageSlider()
{
var tmp = (int)_currentByte.Value;
ImGui.SetNextItemWidth(_comboSelectorSize);
if (ImGui.SliderInt("##slider", ref tmp, 0, _currentCount - 1, "%i", ImGuiSliderFlags.AlwaysClamp))
UpdateValue((CustomizeValue)tmp);
}
private void PercentageInputInt()
{
var tmp = (int)_currentByte.Value;
ImGui.SetNextItemWidth(_inputIntSize);
if (ImGui.InputInt("##text", ref tmp, 1, 1))
UpdateValue((CustomizeValue)Math.Clamp(tmp, 0, _currentCount - 1));
ImGuiUtil.HoverTooltip($"Input Range: [0, {_currentCount - 1}]");
}
// Integral input for an icon- or color based item.
private void DataInputInt(int currentIndex)
{
++currentIndex;
ImGui.SetNextItemWidth(_inputIntSize);
if (ImGui.InputInt("##text", ref currentIndex, 1, 1))
{
currentIndex = Math.Clamp(currentIndex - 1, 0, _currentCount - 1);
var data = _set.Data(_currentIndex, currentIndex, _customize.Face);
UpdateValue(data.Value);
}
ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]");
}
private void DrawListSelector(CustomizeIndex index)
{
using var _ = SetId(index);
using var bigGroup = ImRaii.Group();
ListCombo();
ImGui.SameLine();
ListInputInt();
ImGui.SameLine();
ImGui.TextUnformatted(_currentOption);
}
private void ListCombo()
{
ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale);
using var combo = ImRaii.Combo("##combo", $"{_currentOption} #{_currentByte.Value + 1}");
if (!combo)
return;
for (var i = 0; i < _currentCount; ++i)
{
if (ImGui.Selectable($"{_currentOption} #{i + 1}##combo", i == _currentByte.Value))
UpdateValue((CustomizeValue)i);
}
}
private void ListInputInt()
{
var tmp = _currentByte.Value + 1;
ImGui.SetNextItemWidth(_inputIntSize);
if (ImGui.InputInt("##text", ref tmp, 1, 1) && tmp > 0 && tmp <= _currentCount)
UpdateValue((CustomizeValue)Math.Clamp(tmp - 1, 0, _currentCount - 1));
ImGuiUtil.HoverTooltip($"Input Range: [1, {_currentCount}]");
}
// Draw a customize checkbox.
private void DrawCheckbox(CustomizeIndex idx)
{
using var id = SetId(idx);
var tmp = _currentByte != CustomizeValue.Zero;
if (ImGui.Checkbox(_currentOption, ref tmp))
{
_customize.Set(idx, tmp ? CustomizeValue.Max : CustomizeValue.Zero);
Changed |= _currentFlag;
}
}
}

View file

@ -0,0 +1,164 @@
using System;
using System.Numerics;
using System.Reflection;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer : IDisposable
{
private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f);
private readonly ImGuiScene.TextureWrap? _legacyTattoo;
private bool _withFlags = false;
private Exception? _terminate = null;
private Customize _customize;
private CustomizationSet _set = null!;
public Customize Customize;
public CustomizeFlag CurrentFlag { get; private set; }
public CustomizeFlag Changed { get; private set; }
public bool RequiresRedraw
=> Changed.RequiresRedraw();
private bool _locked = false;
private Vector2 _iconSize;
private Vector2 _framedIconSize;
private float _inputIntSize;
private float _comboSelectorSize;
private float _raceSelectorWidth;
private readonly CustomizationService _service;
private readonly ItemManager _items;
public CustomizationDrawer(DalamudPluginInterface pi, CustomizationService service, ItemManager items)
{
_service = service;
_items = items;
_legacyTattoo = GetLegacyTattooIcon(pi);
Customize = Customize.Default;
}
public void Dispose()
{
_legacyTattoo?.Dispose();
}
public bool Draw(Customize current, bool locked)
{
_withFlags = false;
CurrentFlag = CustomizeFlagExtensions.All;
Init(current, locked);
return DrawInternal();
}
public bool Draw(Customize current, CustomizeFlag currentFlags, bool locked)
{
_withFlags = true;
CurrentFlag = currentFlags;
Init(current, locked);
return DrawInternal();
}
private void Init(Customize current, bool locked)
{
UpdateSizes();
_terminate = null;
Changed = 0;
_customize.Load(current);
_locked = locked;
}
// Set state for drawing of current customization.
private CustomizeIndex _currentIndex;
private CustomizeFlag _currentFlag;
private CustomizeValue _currentByte = CustomizeValue.Zero;
private int _currentCount;
private string _currentOption = string.Empty;
// Prepare a new customization option.
private ImRaii.Id SetId(CustomizeIndex index)
{
_currentIndex = index;
_currentFlag = index.ToFlag();
_currentByte = _customize[index];
_currentCount = _set.Count(index, _customize.Face);
_currentOption = _set.Option(index);
return ImRaii.PushId((int)index);
}
// Update the current id with a new value.
private void UpdateValue(CustomizeValue value)
{
if (_currentByte == value)
return;
_customize[_currentIndex] = value;
Changed |= _currentFlag;
}
private bool DrawInternal()
{
using var disabled = ImRaii.Disabled(_locked);
try
{
DrawRaceGenderSelector();
_set = _service.AwaitedService.GetList(_customize.Clan, _customize.Gender);
foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage])
PercentageSelector(id);
Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.IconSelector], DrawIconSelector, ImGui.SameLine);
DrawMultiIconSelector();
foreach (var id in _set.Order[CharaMakeParams.MenuType.ListSelector])
DrawListSelector(id);
Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.ColorPicker], DrawColorPicker, ImGui.SameLine);
Functions.IteratePairwise(_set.Order[CharaMakeParams.MenuType.Checkmark], DrawCheckbox,
() => ImGui.SameLine(_inputIntSize + _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X));
return Changed != 0;
}
catch (Exception ex)
{
_terminate = ex;
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF4040FF);
ImGui.NewLine();
ImGuiUtil.TextWrapped(_terminate.ToString());
return false;
}
}
private void UpdateSizes()
{
_iconSize = new Vector2(ImGui.GetTextLineHeightWithSpacing() * 2);
_framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding;
_inputIntSize = 2 * _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X;
_comboSelectorSize = 4 * _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
_raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X;
}
private static ImGuiScene.TextureWrap? GetLegacyTattooIcon(DalamudPluginInterface pi)
{
using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw");
if (resource == null)
return null;
var rawImage = new byte[resource.Length];
var length = resource.Read(rawImage, 0, (int)resource.Length);
return length == resource.Length
? pi.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4)
: null;
}
}

View file

@ -0,0 +1,59 @@
using System.Numerics;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface;
using Glamourer.Designs;
using OtterGui;
using OtterGui.FileSystem.Selector;
namespace Glamourer.Gui.Designs;
public sealed class DesignFileSystemSelector : FileSystemSelector<Design, DesignFileSystemSelector.DesignState>
{
private readonly DesignManager _designManager;
public struct DesignState
{ }
public DesignFileSystemSelector(DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState)
: base(fileSystem, keyState)
{
_designManager = designManager;
_designManager.DesignChange += OnDesignChange;
AddButton(DeleteButton, 1000);
}
public override void Dispose()
{
base.Dispose();
_designManager.DesignChange -= OnDesignChange;
}
private void OnDesignChange(DesignManager.DesignChangeType type, Design design, object? oldData)
{
switch (type)
{
case DesignManager.DesignChangeType.ReloadedAll:
case DesignManager.DesignChangeType.Renamed:
case DesignManager.DesignChangeType.AddedTag:
case DesignManager.DesignChangeType.ChangedTag:
case DesignManager.DesignChangeType.RemovedTag:
SetFilterDirty();
break;
}
}
private void DeleteButton(Vector2 size)
{
var keys = true;
var tt = SelectedLeaf == null
? "No design selected."
: "Delete the currently selected design entirely from your drive.\n"
+ "This can not be undone.";
//if (!keys)
// tt += $"\nHold {_config.DeleteModModifier} while clicking to delete the mod.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true)
&& Selected != null)
_designManager.Delete(Selected);
}
}

View file

@ -0,0 +1,368 @@
using Dalamud.Game.ClientState.Keys;
using Glamourer.Designs;
using OtterGui.Filesystem;
using OtterGui.FileSystem.Selector;
namespace Glamourer.Gui.Designs;
//internal partial class Interface
//{
// private int _totalObject;
//
// private bool _inDesignMode;
// private Design? _selection;
// private string _newChildName = string.Empty;
//
// private void DrawDesignSelector()
// {
// _totalObject = 0;
// ImGui.BeginGroup();
// if (ImGui.BeginChild("##selector", new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true))
// {
// DrawFolderContent(_designs.FileSystem.Root, Glamourer.Config.FoldersFirst ? SortMode.FoldersFirst : SortMode.Lexicographical);
// ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
// ImGui.EndChild();
// ImGui.PopStyleVar();
// }
//
// DrawDesignSelectorButtons();
// ImGui.EndGroup();
// }
//
// private void DrawPasteClipboardButton()
// {
// if (_selection!.Data.WriteProtected)
// ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
//
// ImGui.PushFont(UiBuilder.IconFont);
// var applyButton = ImGui.Button(FontAwesomeIcon.Paste.ToIconString());
// ImGui.PopFont();
// if (_selection!.Data.WriteProtected)
// ImGui.PopStyleVar();
//
// ImGuiUtil.HoverTooltip("Overwrite with customization code from clipboard.");
//
// if (_selection!.Data.WriteProtected || !applyButton)
// return;
//
// var text = ImGui.GetClipboardText();
// if (!text.Any())
// return;
//
// try
// {
// _selection!.Data.Load(text, out _);
// _designs.SaveToFile();
// }
// catch (Exception e)
// {
// PluginLog.Information($"{e}");
// }
// }
//
// private void DrawNewFolderButton()
// {
// ImGui.PushFont(UiBuilder.IconFont);
// if (ImGui.Button(FontAwesomeIcon.FolderPlus.ToIconString(), Vector2.UnitX * SelectorWidth / 5))
// OpenDesignNamePopup(DesignNameUse.NewFolder);
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip("Create a new, empty Folder.");
//
// DrawDesignNamePopup(DesignNameUse.NewFolder);
// }
//
// private void DrawNewDesignButton()
// {
// ImGui.PushFont(UiBuilder.IconFont);
// if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), Vector2.UnitX * SelectorWidth / 5))
// OpenDesignNamePopup(DesignNameUse.NewDesign);
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip("Create a new, empty Design.");
//
// DrawDesignNamePopup(DesignNameUse.NewDesign);
// }
//
// private void DrawClipboardDesignButton()
// {
// ImGui.PushFont(UiBuilder.IconFont);
// if (ImGui.Button(FontAwesomeIcon.Paste.ToIconString(), Vector2.UnitX * SelectorWidth / 5))
// OpenDesignNamePopup(DesignNameUse.FromClipboard);
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip("Create a new design from the customization string in your clipboard.");
//
// DrawDesignNamePopup(DesignNameUse.FromClipboard);
// }
//
// private void DrawDeleteDesignButton()
// {
// ImGui.PushFont(UiBuilder.IconFont);
// var style = _selection == null;
// if (style)
// ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
// if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString(), Vector2.UnitX * SelectorWidth / 5) && _selection != null)
// {
// _designs.DeleteAllChildren(_selection, false);
// _selection = null;
// }
//
// ImGui.PopFont();
// if (style)
// ImGui.PopStyleVar();
// ImGuiUtil.HoverTooltip("Delete the currently selected Design.");
// }
//
// private void DrawDuplicateDesignButton()
// {
// ImGui.PushFont(UiBuilder.IconFont);
// if (_selection == null)
// ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
// if (ImGui.Button(FontAwesomeIcon.Clone.ToIconString(), Vector2.UnitX * SelectorWidth / 5) && _selection != null)
// OpenDesignNamePopup(DesignNameUse.DuplicateDesign);
// ImGui.PopFont();
// if (_selection == null)
// ImGui.PopStyleVar();
// ImGuiUtil.HoverTooltip(
// "Clone the currently selected Design.\nHold Shift to only clone the customizations.\nHold Control to only clone the equipment.");
//
// DrawDesignNamePopup(DesignNameUse.DuplicateDesign);
// }
//
// private void DrawDesignSelectorButtons()
// {
// using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
// .Push(ImGuiStyleVar.FrameRounding, 0f);
//
// DrawNewFolderButton();
// ImGui.SameLine();
// DrawNewDesignButton();
// ImGui.SameLine();
// DrawClipboardDesignButton();
// ImGui.SameLine();
// DrawDuplicateDesignButton();
// ImGui.SameLine();
// DrawDeleteDesignButton();
// }
//
// private void DrawDesignHeaderButtons()
// {
// DrawCopyClipboardButton(_selection!.Data);
// ImGui.SameLine();
// DrawPasteClipboardButton();
// ImGui.SameLine();
// DrawApplyToPlayerButton(_selection!.Data);
// if (!_inGPose)
// {
// ImGui.SameLine();
// DrawApplyToTargetButton(_selection!.Data);
// }
//
// ImGui.SameLine();
// DrawCheckbox("Write Protected", _selection!.Data.WriteProtected, v => _selection!.Data.WriteProtected = v, false);
// }
//
// private void DrawDesignPanel()
// {
// if (ImGui.BeginChild("##details", -Vector2.One * 0.001f, true))
// {
// DrawDesignHeaderButtons();
// var data = _selection!.Data;
// var prot = _selection!.Data.WriteProtected;
// if (prot)
// {
// ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.8f);
// data = data.Copy();
// }
//
// DrawGeneralSettings(data, prot);
// var mask = data.WriteEquipment;
// if (DrawEquip(data.Equipment, ref mask) && !prot)
// {
// data.WriteEquipment = mask;
// _designs.SaveToFile();
// }
//
// if (DrawCustomization(ref data.Customizations) && !prot)
// _designs.SaveToFile();
//
// if (DrawMiscellaneous(data, null) && !prot)
// _designs.SaveToFile();
//
// if (prot)
// ImGui.PopStyleVar();
//
// ImGui.EndChild();
// }
// }
//
// private void DrawSaves()
// {
// using var style = ImRaii.PushStyle(ImGuiStyleVar.IndentSpacing, 12.5f * ImGui.GetIO().FontGlobalScale);
// using var tab = ImRaii.TabItem("Designs");
// _inDesignMode = tab.Success;
// if (!_inDesignMode)
// return;
//
// DrawDesignSelector();
//
// if (_selection != null)
// {
// ImGui.SameLine();
// DrawDesignPanel();
// }
// }
//
// private void DrawCheckbox(string label, bool value, Action<bool> setter, bool prot)
// {
// var tmp = value;
// if (ImGui.Checkbox(label, ref tmp) && tmp != value)
// {
// setter(tmp);
// if (!prot)
// _designs.SaveToFile();
// }
// }
//
// private void DrawGeneralSettings(CharacterSave data, bool prot)
// {
// ImGui.BeginGroup();
// DrawCheckbox("Apply Customizations", data.WriteCustomizations, v => data.WriteCustomizations = v, prot);
// DrawCheckbox("Write Weapon State", data.SetWeaponState, v => data.SetWeaponState = v, prot);
// ImGui.EndGroup();
// ImGui.SameLine();
// ImGui.BeginGroup();
// DrawCheckbox("Write Hat State", data.SetHatState, v => data.SetHatState = v, prot);
// DrawCheckbox("Write Visor State", data.SetVisorState, v => data.SetVisorState = v, prot);
// ImGui.EndGroup();
// }
//
// private void RenameChildInput(IFileSystemBase child)
// {
// ImGui.SetNextItemWidth(150);
// if (!ImGui.InputTextWithHint("##fsNewName", "Rename...", ref _newChildName, 64,
// ImGuiInputTextFlags.EnterReturnsTrue))
// return;
//
// if (_newChildName.Any() && _newChildName != child.Name)
// try
// {
// var oldPath = child.FullName();
// if (_designs.FileSystem.Rename(child, _newChildName))
// _designs.UpdateAllChildren(oldPath, child);
// }
// catch (Exception e)
// {
// PluginLog.Error($"Could not rename {child.Name} to {_newChildName}:\n{e}");
// }
// else if (child is Folder f)
// try
// {
// var oldPath = child.FullName();
// if (_designs.FileSystem.Merge(f, f.Parent, true))
// _designs.UpdateAllChildren(oldPath, f.Parent);
// }
// catch (Exception e)
// {
// PluginLog.Error($"Could not merge folder {child.Name} into parent:\n{e}");
// }
//
// _newChildName = string.Empty;
// }
//
// private void ContextMenu(IFileSystemBase child)
// {
// var label = $"##fsPopup{child.FullName()}";
// if (ImGui.BeginPopup(label))
// {
// if (ImGui.MenuItem("Delete") && ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift)
// _designs.DeleteAllChildren(child, false);
// ImGuiUtil.HoverTooltip("Hold Control and Shift to delete.");
//
// RenameChildInput(child);
//
// if (child is Design d && ImGui.MenuItem("Copy to Clipboard"))
// ImGui.SetClipboardText(d.Data.ToBase64());
//
// ImGui.EndPopup();
// }
//
// if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
// {
// _newChildName = child.Name;
// ImGui.OpenPopup(label);
// }
// }
//
// private static uint GetDesignColor(CharacterSave save)
// {
// const uint white = 0xFFFFFFFF;
// const uint grey = 0xFF808080;
// if (!Glamourer.Config.ColorDesigns)
// return white;
//
// var changesStates = save.SetHatState || save.SetVisorState || save.SetWeaponState || save.IsWet || save.Alpha != 1.0f;
// if (save.WriteCustomizations)
// if (save.WriteEquipment != CharacterEquipMask.None)
// return white;
// else
// return changesStates ? white : Glamourer.Config.CustomizationColor;
//
// if (save.WriteEquipment != CharacterEquipMask.None)
// return changesStates ? white : Glamourer.Config.EquipmentColor;
//
// return changesStates ? Glamourer.Config.StateColor : grey;
// }
//
// private void DrawFolderContent(Folder folder, SortMode mode)
// {
// foreach (var child in folder.AllChildren(mode).ToArray())
// {
// if (child.IsFolder(out var subFolder))
// {
// var treeNode = ImGui.TreeNodeEx($"{subFolder.Name}##{_totalObject}");
// DrawOrnaments(child);
//
// if (treeNode)
// {
// DrawFolderContent(subFolder, mode);
// ImGui.TreePop();
// }
// else
// {
// _totalObject += subFolder.TotalDescendantLeaves();
// }
// }
// else
// {
// if (child is not Design d)
// continue;
//
// ++_totalObject;
// var color = GetDesignColor(d.Data);
// using var c = ImRaii.PushColor(ImGuiCol.Text, color);
//
// var selected = ImGui.Selectable($"{child.Name}##{_totalObject}", ReferenceEquals(child, _selection));
// c.Pop();
// DrawOrnaments(child);
//
// if (Glamourer.Config.ShowLocks && d.Data.WriteProtected)
// {
// ImGui.SameLine();
// using var font = ImRaii.PushFont(UiBuilder.IconFont);
// c.Push(ImGuiCol.Text, color);
// ImGui.TextUnformatted(FontAwesomeIcon.Lock.ToIconString());
// }
//
// if (selected)
// _selection = d;
// }
// }
// }
//
// private void DrawOrnaments(IFileSystemBase child)
// {
// FileSystemImGui.DragDropSource(child);
// if (FileSystemImGui.DragDropTarget(_designs.FileSystem, child, out var oldPath, out var draggedFolder))
// _designs.UpdateAllChildren(oldPath, draggedFolder!);
// ContextMenu(child);
// }
//}

View file

@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Dalamud.Data;
using Dalamud.Interface;
using Glamourer.Designs;
using Glamourer.Services;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment;
public class EquipmentDrawer
{
private readonly ItemManager _items;
private readonly FilterComboColors _stainCombo;
private readonly StainData _stainData;
private readonly ItemCombo[] _itemCombo;
private readonly Dictionary<(FullEquipType, EquipSlot), WeaponCombo> _weaponCombo;
public EquipmentDrawer(DataManager gameData, ItemManager items)
{
_items = items;
_stainData = items.Stains;
_stainCombo = new FilterComboColors(140,
_stainData.Data.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))));
_itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e)).ToArray();
_weaponCombo = new Dictionary<(FullEquipType, EquipSlot), WeaponCombo>(FullEquipTypeExtensions.WeaponTypes.Count * 2);
foreach (var type in Enum.GetValues<FullEquipType>())
{
if (type.ToSlot() is EquipSlot.MainHand)
_weaponCombo.TryAdd((type, EquipSlot.MainHand), new WeaponCombo(gameData, items, type, EquipSlot.MainHand));
else if (type.ToSlot() is EquipSlot.OffHand)
_weaponCombo.TryAdd((type, EquipSlot.OffHand), new WeaponCombo(gameData, items, type, EquipSlot.OffHand));
var offhand = type.Offhand();
if (offhand is not FullEquipType.Unknown && !_weaponCombo.ContainsKey((offhand, EquipSlot.OffHand)))
_weaponCombo.TryAdd((offhand, EquipSlot.OffHand), new WeaponCombo(gameData, items, type, EquipSlot.OffHand));
}
_weaponCombo.Add((FullEquipType.Unknown, EquipSlot.MainHand), new WeaponCombo(gameData, items, FullEquipType.Unknown, EquipSlot.MainHand));
}
private string VerifyRestrictedGear(Item gear, EquipSlot slot, Gender gender, Race race)
{
if (slot.IsAccessory())
return gear.Name;
var (changed, _) = _items.ResolveRestrictedGear(gear.Model, slot, race, gender);
if (changed)
return gear.Name + " (Restricted)";
return gear.Name;
}
public bool DrawArmor(Item current, EquipSlot slot, out Item armor, Gender gender = Gender.Unknown, Race race = Race.Unknown)
{
Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(DrawArmor)} on {slot}.");
var combo = _itemCombo[slot.ToIndex()];
armor = current;
var change = combo.Draw(VerifyRestrictedGear(armor, slot, gender, race), armor.ItemId, 320 * ImGuiHelpers.GlobalScale);
if (armor.ModelBase.Value != 0)
{
ImGuiUtil.HoverTooltip("Right-click to clear.");
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
change = true;
armor = ItemManager.NothingItem(slot);
}
else if (change)
{
armor = combo.CurrentSelection.WithStain(armor.Stain);
}
}
else if (change)
{
armor = combo.CurrentSelection.WithStain(armor.Stain);
}
return change;
}
public bool DrawStain(StainId current, EquipSlot slot, out Stain stain)
{
var found = _stainData.TryGetValue(current, out stain);
if (!_stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found))
return false;
return _stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain);
}
public bool DrawMainhand(Weapon current, bool drawAll, out Weapon weapon)
{
weapon = current;
if (!_weaponCombo.TryGetValue((drawAll ? FullEquipType.Unknown : current.Type, EquipSlot.MainHand), out var combo))
return false;
if (!combo.Draw(weapon.Name, weapon.ItemId, 320 * ImGuiHelpers.GlobalScale))
return false;
weapon = combo.CurrentSelection.WithStain(current.Stain);
return true;
}
public bool DrawOffhand(Weapon current, FullEquipType mainType, out Weapon weapon)
{
weapon = current;
var offType = mainType.Offhand();
if (offType == FullEquipType.Unknown)
return false;
if (!_weaponCombo.TryGetValue((offType, EquipSlot.OffHand), out var combo))
return false;
var change = combo.Draw(weapon.Name, weapon.ItemId, 320 * ImGuiHelpers.GlobalScale);
if (offType.ToSlot() is EquipSlot.OffHand && weapon.ModelBase.Value != 0)
{
ImGuiUtil.HoverTooltip("Right-click to clear.");
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
change = true;
weapon = ItemManager.NothingItem(offType);
}
}
else if (change)
{
weapon = combo.CurrentSelection.WithStain(current.Stain);
}
return change;
}
public bool DrawApply(Design design, EquipSlot slot, out bool enabled)
=> DrawCheckbox($"##apply{slot}", design.DoApplyEquip(slot), out enabled);
public bool DrawApplyStain(Design design, EquipSlot slot, out bool enabled)
=> DrawCheckbox($"##applyStain{slot}", design.DoApplyStain(slot), out enabled);
private static bool DrawCheckbox(string label, bool value, out bool on)
{
var ret = ImGuiUtil.Checkbox(label, string.Empty, value, v => value = v);
on = value;
return ret;
}
public bool DrawVisor(Design design, out bool on)
=> DrawCheckbox("##visorToggled", design.Visor.ForcedValue, out on);
public bool DrawVisor(ActiveDesign design, out bool on)
=> DrawCheckbox("##visorToggled", design.IsVisorToggled, out on);
public bool DrawHat(Design design, out bool on)
=> DrawCheckbox("##hatVisible", design.Hat.ForcedValue, out on);
public bool DrawHat(ActiveDesign design, out bool on)
=> DrawCheckbox("##hatVisible", design.IsHatVisible, out on);
public bool DrawWeapon(Design design, out bool on)
=> DrawCheckbox("##weaponVisible", design.Weapon.ForcedValue, out on);
public bool DrawWeapon(ActiveDesign design, out bool on)
=> DrawCheckbox("##weaponVisible", design.IsWeaponVisible, out on);
public bool DrawWetness(Design design, out bool on)
=> DrawCheckbox("##wetness", design.Wetness.ForcedValue, out on);
public bool DrawWetness(ActiveDesign design, out bool on)
=> DrawCheckbox("##wetnessVisible", design.IsWet, out on);
public bool DrawApplyVisor(Design design, out bool on)
=> DrawCheckbox("##applyVisor", design.Visor.Enabled, out on);
public bool DrawApplyWetness(Design design, out bool on)
=> DrawCheckbox("##applyWetness", design.Wetness.Enabled, out on);
public bool DrawApplyHatState(Design design, out bool on)
=> DrawCheckbox("##applyHatState", design.Hat.Enabled, out on);
public bool DrawApplyWeaponState(Design design, out bool on)
=> DrawCheckbox("##applyWeaponState", design.Weapon.Enabled, out on);
}

View file

@ -0,0 +1,105 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Glamourer.Designs;
using Glamourer.Services;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Item = Glamourer.Designs.Item;
namespace Glamourer.Gui.Equipment;
public sealed class ItemCombo : FilterComboCache<Item>
{
public readonly string Label;
private uint _currentItem;
public ItemCombo(DataManager gameData, ItemManager items, EquipSlot slot)
: base(() => GetItems(items, slot))
{
Label = GetLabel(gameData, slot);
_currentItem = ItemManager.NothingId(slot);
}
protected override void DrawList(float width, float itemHeight)
{
base.DrawList(width, itemHeight);
if (NewSelection != null && Items.Count > NewSelection.Value)
CurrentSelection = Items[NewSelection.Value];
}
protected override int UpdateCurrentSelected(int currentSelected)
{
if (CurrentSelection.ItemId != _currentItem)
{
CurrentSelectionIdx = Items.IndexOf(i => i.ItemId == _currentItem);
CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
return base.UpdateCurrentSelected(CurrentSelectionIdx);
}
return currentSelected;
}
public bool Draw(string previewName, uint previewIdx, float width)
{
_currentItem = previewIdx;
return Draw(Label, previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
}
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var obj = Items[globalIdx];
var name = ToString(obj);
var ret = ImGui.Selectable(name, selected);
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
ImGuiUtil.RightAlign($"({obj.ModelBase.Value}-{obj.Variant})");
return ret;
}
protected override bool IsVisible(int globalIndex, LowerString filter)
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelBase.ToString());
protected override string ToString(Item obj)
=> obj.Name;
private static string GetLabel(DataManager gameData, EquipSlot slot)
{
var sheet = gameData.GetExcelSheet<Addon>()!;
return slot switch
{
EquipSlot.Head => sheet.GetRow(740)?.Text.ToString() ?? "Head",
EquipSlot.Body => sheet.GetRow(741)?.Text.ToString() ?? "Body",
EquipSlot.Hands => sheet.GetRow(742)?.Text.ToString() ?? "Hands",
EquipSlot.Legs => sheet.GetRow(744)?.Text.ToString() ?? "Legs",
EquipSlot.Feet => sheet.GetRow(745)?.Text.ToString() ?? "Feet",
EquipSlot.Ears => sheet.GetRow(746)?.Text.ToString() ?? "Ears",
EquipSlot.Neck => sheet.GetRow(747)?.Text.ToString() ?? "Neck",
EquipSlot.Wrists => sheet.GetRow(748)?.Text.ToString() ?? "Wrists",
EquipSlot.RFinger => sheet.GetRow(749)?.Text.ToString() ?? "Right Ring",
EquipSlot.LFinger => sheet.GetRow(750)?.Text.ToString() ?? "Left Ring",
_ => string.Empty,
};
}
private static IReadOnlyList<Item> GetItems(ItemManager items, EquipSlot slot)
{
var nothing = ItemManager.NothingItem(slot);
if (!items.ItemService.AwaitedService.TryGetValue(slot.ToEquipType(), out var list))
return new[]
{
nothing,
};
var enumerable = list.Select(i => new Item(i));
if (slot.IsEquipment())
enumerable = enumerable.Append(ItemManager.SmallClothesItem(slot));
return enumerable.OrderBy(i => i.Name).Prepend(nothing).ToList();
}
}

View file

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Glamourer.Designs;
using Glamourer.Services;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Equipment;
public sealed class WeaponCombo : FilterComboCache<Weapon>
{
public readonly string Label;
private uint _currentItem;
public WeaponCombo(DataManager gameData, ItemManager items, FullEquipType type, EquipSlot offhand)
: base(offhand is EquipSlot.OffHand ? () => GetOff(items, type) : () => GetMain(items, type))
=> Label = GetLabel(gameData, offhand);
protected override void DrawList(float width, float itemHeight)
{
base.DrawList(width, itemHeight);
if (NewSelection != null && Items.Count > NewSelection.Value)
CurrentSelection = Items[NewSelection.Value];
}
protected override int UpdateCurrentSelected(int currentSelected)
{
if (CurrentSelection.ItemId != _currentItem)
{
CurrentSelectionIdx = Items.IndexOf(i => i.ItemId == _currentItem);
CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
return base.UpdateCurrentSelected(CurrentSelectionIdx);
}
return currentSelected;
}
public bool Draw(string previewName, uint previewIdx, float width)
{
_currentItem = previewIdx;
return Draw(Label, previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
}
protected override bool DrawSelectable(int globalIdx, bool selected)
{
var obj = Items[globalIdx];
var name = ToString(obj);
var ret = ImGui.Selectable(name, selected);
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
ImGuiUtil.RightAlign($"({obj.ModelBase.Value}-{obj.WeaponBase.Value}-{obj.Variant})");
return ret;
}
protected override bool IsVisible(int globalIndex, LowerString filter)
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelBase.ToString());
protected override string ToString(Weapon obj)
=> obj.Name;
private static string GetLabel(DataManager gameData, EquipSlot offhand)
{
var sheet = gameData.GetExcelSheet<Addon>()!;
return offhand is EquipSlot.OffHand
? sheet.GetRow(739)?.Text.ToString() ?? "Off Hand"
: sheet.GetRow(738)?.Text.ToString() ?? "Main Hand";
}
private static IReadOnlyList<Weapon> GetMain(ItemManager items, FullEquipType type)
{
var list = new List<Weapon>();
if (type is FullEquipType.Unknown)
foreach (var t in Enum.GetValues<FullEquipType>().Where(t => t.ToSlot() == EquipSlot.MainHand))
list.AddRange(items.ItemService.AwaitedService[t].Select(w => new Weapon(w, false)));
else if (type.ToSlot() is EquipSlot.MainHand)
list.AddRange(items.ItemService.AwaitedService[type].Select(w => new Weapon(w, false)));
list.Sort((w1, w2) => string.CompareOrdinal(w1.Name, w2.Name));
return list;
}
private static IReadOnlyList<Weapon> GetOff(ItemManager items, FullEquipType type)
{
if (type.ToSlot() == EquipSlot.OffHand)
{
var nothing = ItemManager.NothingItem(type);
if (!items.ItemService.AwaitedService.TryGetValue(type, out var list))
return new[]
{
nothing,
};
return list.Select(w => new Weapon(w, true)).OrderBy(w => w.Name).Prepend(nothing).ToList();
}
else if (items.ItemService.AwaitedService.TryGetValue(type, out var list))
{
return list.Select(w => new Weapon(w, true)).OrderBy(w => w.Name).ToList();
}
return Array.Empty<Weapon>();
}
}

View file

@ -0,0 +1,30 @@
using System;
using Dalamud.Interface;
using Dalamud.Interface.Windowing;
namespace Glamourer.Gui;
public class GlamourerWindowSystem : IDisposable
{
private readonly WindowSystem _windowSystem = new("Glamourer");
private readonly UiBuilder _uiBuilder;
private readonly Interface _ui;
public GlamourerWindowSystem(UiBuilder uiBuilder, Interface ui)
{
_uiBuilder = uiBuilder;
_ui = ui;
_windowSystem.AddWindow(ui);
_uiBuilder.Draw += _windowSystem.Draw;
_uiBuilder.OpenConfigUi += _ui.Toggle;
}
public void Dispose()
{
_uiBuilder.Draw -= _windowSystem.Draw;
_uiBuilder.OpenConfigUi -= _ui.Toggle;
}
public void Toggle()
=> _ui.Toggle();
}

View file

@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using ImGui = ImGuiNET.ImGui;
namespace Glamourer.Gui;
public partial class Interface
{
private class ActorTab
{
private readonly Interface _main;
private readonly ActiveDesign.Manager _activeDesigns;
private readonly ObjectManager _objects;
private readonly TargetManager _targets;
private readonly ActorService _actors;
private readonly ItemManager _items;
public ActorTab(Interface main, ActiveDesign.Manager activeDesigns, ObjectManager objects, TargetManager targets, ActorService actors,
ItemManager items)
{
_main = main;
_activeDesigns = activeDesigns;
_objects = objects;
_targets = targets;
_actors = actors;
_items = items;
}
private ActorIdentifier _identifier = ActorIdentifier.Invalid;
private ActorData _currentData = ActorData.Invalid;
private ActiveDesign? _currentSave;
public void Draw()
{
using var tab = ImRaii.TabItem("Actors");
if (!tab)
return;
DrawActorSelector();
if (!_objects.TryGetValue(_identifier, out _currentData))
_currentData = ActorData.Invalid;
ImGui.SameLine();
DrawPanel();
}
private unsafe void DrawPanel()
{
if (_identifier == ActorIdentifier.Invalid)
return;
using var group = ImRaii.Group();
DrawPanelHeader();
using var child = ImRaii.Child("##ActorPanel", -Vector2.One, true);
if (!child || _currentSave == null)
return;
if (_currentData.Valid)
_currentSave.Initialize(_items, _currentData.Objects[0]);
RevertButton();
ActorDebug.Draw(_currentSave.ModelData);
return;
if (_main._customizationDrawer.Draw(_currentSave.ModelData.Customize, _identifier.Type == IdentifierType.Special))
_activeDesigns.ChangeCustomize(_currentSave, _main._customizationDrawer.Changed, _main._customizationDrawer.Customize.Data,
false);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var current = _currentSave.Armor(slot);
if (_main._equipmentDrawer.DrawStain(current.Stain, slot, out var stain))
_activeDesigns.ChangeStain(_currentSave, slot, stain.RowIndex, false);
ImGui.SameLine();
if (_main._equipmentDrawer.DrawArmor(current, slot, out var armor, _currentSave.ModelData.Customize.Gender,
_currentSave.ModelData.Customize.Race))
_activeDesigns.ChangeEquipment(_currentSave, slot, armor, false);
}
var currentMain = _currentSave.WeaponMain;
if (_main._equipmentDrawer.DrawStain(currentMain.Stain, EquipSlot.MainHand, out var stainMain))
_activeDesigns.ChangeStain(_currentSave, EquipSlot.MainHand, stainMain.RowIndex, false);
ImGui.SameLine();
_main._equipmentDrawer.DrawMainhand(currentMain, true, out var main);
if (currentMain.Type.Offhand() != FullEquipType.Unknown)
{
var currentOff = _currentSave.WeaponOff;
if (_main._equipmentDrawer.DrawStain(currentOff.Stain, EquipSlot.OffHand, out var stainOff))
_activeDesigns.ChangeStain(_currentSave, EquipSlot.OffHand, stainOff.RowIndex, false);
ImGui.SameLine();
_main._equipmentDrawer.DrawOffhand(currentOff, main.Type, out var off);
}
if (_main._equipmentDrawer.DrawVisor(_currentSave, out var value))
_activeDesigns.ChangeVisor(_currentSave, value, false);
}
private const uint RedHeaderColor = 0xFF1818C0;
private const uint GreenHeaderColor = 0xFF18C018;
private unsafe void RevertButton()
{
if (ImGui.Button("Revert"))
_activeDesigns.RevertDesign(_currentSave!);
//foreach (var actor in _currentData.Objects)
// _currentSave!.ApplyToActor(actor);
//
//if (_currentData.Objects.Count > 0)
// _currentSave = _manipulations.GetOrCreateSave(_currentData.Objects[0]);
//
//_currentSave!.Reset();
if (_currentData.Objects.Count > 0)
ImGui.TextUnformatted(_currentData.Objects[0].Pointer->GameObject.DataID.ToString());
//VisorBox();
}
//private unsafe void VisorBox()
//{
// var (flags, mask) = (_currentSave!.Data.Flags & (ApplicationFlags.SetVisor | ApplicationFlags.Visor)) switch
// {
// ApplicationFlags.SetVisor => (0u, 3u),
// ApplicationFlags.Visor => (1u, 3u),
// ApplicationFlags.SetVisor | ApplicationFlags.Visor => (3u, 3u),
// _ => (2u, 3u),
// };
// var tmp = flags;
// if (ImGui.CheckboxFlags("Visor Toggled", ref tmp, mask))
// {
// _currentSave.Data.Flags = flags switch
// {
// 0 => (_currentSave.Data.Flags | ApplicationFlags.Visor) & ~ApplicationFlags.SetVisor,
// 1 => _currentSave.Data.Flags | ApplicationFlags.SetVisor,
// 2 => _currentSave.Data.Flags | ApplicationFlags.SetVisor,
// _ => _currentSave.Data.Flags & ~(ApplicationFlags.SetVisor | ApplicationFlags.Visor),
// };
// if (_currentSave.Data.Flags.HasFlag(ApplicationFlags.SetVisor))
// {
// var on = _currentSave.Data.Flags.HasFlag(ApplicationFlags.Visor);
// foreach (var actor in _currentData.Objects.Where(a => a.IsHuman && a.DrawObject))
// RedrawManager.SetVisor(actor.DrawObject.Pointer, on);
// }
// }
//}
private void DrawPanelHeader()
{
var color = _currentData.Valid ? GreenHeaderColor : RedHeaderColor;
var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
using var c = ImRaii.PushColor(ImGuiCol.Text, color)
.Push(ImGuiCol.Button, buttonColor)
.Push(ImGuiCol.ButtonHovered, buttonColor)
.Push(ImGuiCol.ButtonActive, buttonColor);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.Button($"{_currentData.Label}##playerHeader", -Vector2.UnitX);
}
//private void DrawActorPanel()
//{
// using var group = ImRaii.Group();
// if (!_data.Identifier.IsValid)
// return;
//
// if (DrawCustomization(_currentSave.Customize, _currentSave.Equipment, !_data.Modifiable))
// //Glamourer.RedrawManager.Set(_data.Actor.Address, _character);
// Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
//
// if (ImGui.Button("Set Machinist Goggles"))
// Glamourer.RedrawManager.ChangeEquip(_data.Actor, EquipSlot.Head, new CharacterArmor(265, 1, 0));
//
// if (ImGui.Button("Set Weapon"))
// Glamourer.RedrawManager.LoadWeapon(_data.Actor.Address, new CharacterWeapon(0x00C9, 0x004E, 0x0001, 0x00),
// new CharacterWeapon(0x0065, 0x003D, 0x0001, 0x00));
//
// if (ImGui.Button("Set Customize"))
// {
// unsafe
// {
// var data = _data.Actor.Customize.Data->Clone();
// Glamourer.RedrawManager.UpdateCustomize(_data.Actor.DrawObject, new Customize(&data)
// {
// SkinColor = 154,
// });
// }
// }
//}
//
//private void DrawMonsterPanel()
//{
// using var group = ImRaii.Group();
// var currentModel = (uint)_data.Actor.ModelId;
// var models = GameData.Models(Dalamud.GameData);
// var currentData = models.Models.TryGetValue(currentModel, out var c) ? c.FirstName : $"#{currentModel}";
// using var combo = ImRaii.Combo("Model Id", currentData);
// if (!combo)
// return;
//
// foreach (var (id, data) in models.Models)
// {
// if (ImGui.Selectable(data.FirstName, id == currentModel) && id != currentModel)
// {
// _data.Actor.SetModelId((int)id);
// Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
// }
//
// ImGuiUtil.HoverTooltip(data.AllNames);
// }
//}
private LowerString _actorFilter = LowerString.Empty;
private void DrawActorSelector()
{
using var group = ImRaii.Group();
var oldSpacing = ImGui.GetStyle().ItemSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.SetNextItemWidth(_actorSelectorWidth);
LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64);
DrawSelector(oldSpacing);
DrawSelectionButtons();
}
private void DrawSelector(Vector2 oldSpacing)
{
using var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -ImGui.GetFrameHeight()), true);
if (!child)
return;
_objects.Update();
if (!_activeDesigns.TryGetValue(_identifier, out _currentSave))
_currentSave = null;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(_objects, skips, CheckFilter, DrawSelectable);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
if (_currentSave == null)
{
_identifier = ActorIdentifier.Invalid;
_currentData = ActorData.Invalid;
}
}
private bool CheckFilter(KeyValuePair<ActorIdentifier, ActorData> pair)
=> _actorFilter.IsEmpty || pair.Value.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase);
private void DrawSelectable(KeyValuePair<ActorIdentifier, ActorData> pair)
{
var equal = pair.Key.Equals(_identifier);
if (ImGui.Selectable(pair.Value.Label, equal) || equal)
{
_identifier = pair.Key.CreatePermanent();
_currentData = pair.Value;
if (!_activeDesigns.TryGetValue(_identifier, out _currentSave))
_currentSave = _currentData.Valid ? _activeDesigns.GetOrCreateSave(_currentData.Objects[0]) : null;
}
}
private void DrawSelectionButtons()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
var buttonWidth = new Vector2(_actorSelectorWidth / 2, 0);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth
, "Select the local player character.", !_objects.Player, true))
_identifier = _objects.Player.GetIdentifier(_actors.AwaitedService);
ImGui.SameLine();
Actor targetActor = _targets.Target?.Address;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth,
"Select the current target, if it is in the list.", _objects.IsInGPose || !targetActor, true))
_identifier = targetActor.GetIdentifier(_actors.AwaitedService);
}
}
}

View file

@ -0,0 +1,55 @@
using System;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Util;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
namespace Glamourer.Gui;
public partial class Interface
{
private class DebugDataTab
{
private readonly CustomizationService _service;
public DebugDataTab(CustomizationService service)
=> _service = service;
public void Draw()
{
if (!_service.Valid)
return;
using var tab = ImRaii.TabItem("Debug");
if (!tab)
return;
foreach (var clan in _service.AwaitedService.Clans)
{
foreach (var gender in _service.AwaitedService.Genders)
DrawCustomizationInfo(_service.AwaitedService.GetList(clan, gender));
}
}
public void DrawCustomizationInfo(CustomizationSet set)
{
if (!ImGui.CollapsingHeader($"{CustomizeExtensions.ClanName(_service.AwaitedService, set.Clan, set.Gender)} {set.Gender}"))
return;
using var table = ImRaii.Table("data", 5);
if (!table)
return;
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
ImGuiUtil.DrawTableColumn(index.ToString());
ImGuiUtil.DrawTableColumn(set.Option(index));
ImGuiUtil.DrawTableColumn(set.IsAvailable(index) ? "Available" : "Unavailable");
ImGuiUtil.DrawTableColumn(set.Type(index).ToString());
ImGuiUtil.DrawTableColumn(set.Count(index).ToString());
}
}
}
}

View file

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Actors;
namespace Glamourer.Gui;
public partial class Interface
{
private class DebugStateTab
{
private readonly ActiveDesign.Manager _activeDesigns;
private LowerString _manipulationFilter = LowerString.Empty;
private ActorIdentifier _selection = ActorIdentifier.Invalid;
private ActiveDesign? _save = null;
private bool _delete = false;
public DebugStateTab(ActiveDesign.Manager activeDesigns)
=> _activeDesigns = activeDesigns;
[Conditional("DEBUG")]
public void Draw()
{
using var tab = ImRaii.TabItem("Current Manipulations");
if (!tab)
return;
DrawManipulationSelector();
if (_save == null)
return;
ImGui.SameLine();
DrawActorPanel();
if (_delete)
{
_delete = false;
_activeDesigns.DeleteSave(_selection);
_selection = ActorIdentifier.Invalid;
}
}
private void DrawSelector(Vector2 oldSpacing)
{
using var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -1), true);
if (!child)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(_activeDesigns, skips, CheckFilter, DrawSelectable);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
}
private void DrawManipulationSelector()
{
using var group = ImRaii.Group();
var oldSpacing = ImGui.GetStyle().ItemSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.SetNextItemWidth(_actorSelectorWidth);
LowerString.InputWithHint("##actorFilter", "Filter...", ref _manipulationFilter, 64);
_save = null;
DrawSelector(oldSpacing);
}
private bool CheckFilter(KeyValuePair<ActorIdentifier, ActiveDesign> data)
{
if (data.Key.Equals(_selection))
_save = data.Value;
return _manipulationFilter.Length == 0 || _manipulationFilter.IsContained(data.Key.ToString()!);
}
private void DrawSelectable(KeyValuePair<ActorIdentifier, ActiveDesign> data)
{
var equal = data.Key.Equals(_selection);
if (ImGui.Selectable(data.Key.ToString(), equal))
{
_selection = data.Key;
_save = data.Value;
}
}
private void DrawActorPanel()
{
using var group = ImRaii.Group();
if (ImGui.Button("Delete"))
_delete = true;
}
}
}

View file

@ -0,0 +1,101 @@
using System;
using System.Numerics;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Gui.Designs;
using Glamourer.Interop;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui;
public partial class Interface
{
private class DesignTab : IDisposable
{
public readonly DesignFileSystemSelector Selector;
private readonly Interface _main;
private readonly DesignFileSystem _fileSystem;
private readonly DesignManager _designManager;
private readonly ActiveDesign.Manager _activeDesignManager;
private readonly ObjectManager _objects;
public DesignTab(Interface main, DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState,
ActiveDesign.Manager activeDesignManager, ObjectManager objects)
{
_main = main;
_designManager = designManager;
_fileSystem = fileSystem;
_activeDesignManager = activeDesignManager;
_objects = objects;
Selector = new DesignFileSystemSelector(designManager, fileSystem, keyState);
}
public void Dispose()
=> Selector.Dispose();
public void Draw()
{
using var tab = ImRaii.TabItem("Designs");
if (!tab)
return;
Selector.Draw(GetDesignSelectorSize());
ImGui.SameLine();
DrawDesignPanel();
}
public float GetDesignSelectorSize()
=> 200f * ImGuiHelpers.GlobalScale;
private void ApplySelfButton()
{
var self = _objects.Player;
if (!ImGuiUtil.DrawDisabledButton("Apply to Self", Vector2.Zero, string.Empty, !self.Valid))
return;
var design = _activeDesignManager.GetOrCreateSave(self);
_activeDesignManager.ApplyDesign(design, Selector.Selected!, false);
}
public void DrawDesignPanel()
{
if (Selector.Selected == null)
return;
using var group = ImRaii.Group();
ApplySelfButton();
using var child = ImRaii.Child("##DesignPanel", new Vector2(-0.001f), true, ImGuiWindowFlags.HorizontalScrollbar);
if (!child)
return;
ActorDebug.Draw(Selector.Selected.ModelData);
_main._customizationDrawer.Draw(Selector.Selected.ModelData.Customize, CustomizeFlagExtensions.All, true);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var current = Selector.Selected.Armor(slot);
_main._equipmentDrawer.DrawStain(current.Stain, slot, out var stain);
ImGui.SameLine();
_main._equipmentDrawer.DrawArmor(current, slot, out var armor);
}
var currentMain = Selector.Selected.WeaponMain;
_main._equipmentDrawer.DrawStain(currentMain.Stain, EquipSlot.MainHand, out var stainMain);
ImGui.SameLine();
_main._equipmentDrawer.DrawMainhand(currentMain, true, out var main);
if (currentMain.Type.Offhand() != FullEquipType.Unknown)
{
var currentOff = Selector.Selected.WeaponOff;
_main._equipmentDrawer.DrawStain(currentOff.Stain, EquipSlot.OffHand, out var stainOff);
ImGui.SameLine();
_main._equipmentDrawer.DrawOffhand(currentOff, main.Type, out var off);
}
}
}
}

View file

@ -0,0 +1,84 @@
using System;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
namespace Glamourer.Gui;
public partial class Interface
{
private void Checkmark(string label, string tooltip, bool value, Action<bool> setter)
{
if (ImGuiUtil.Checkbox(label, tooltip, value, setter))
_config.Save();
}
private void ChangeAndSave<T>(T value, T currentValue, Action<T> setter) where T : IEquatable<T>
{
if (value.Equals(currentValue))
return;
setter(value);
_config.Save();
}
private void DrawColorPicker(string name, string tooltip, uint value, uint defaultValue, Action<uint> setter)
{
const ImGuiColorEditFlags flags = ImGuiColorEditFlags.AlphaPreviewHalf | ImGuiColorEditFlags.NoInputs;
var tmp = ImGui.ColorConvertU32ToFloat4(value);
if (ImGui.ColorEdit4($"##{name}", ref tmp, flags))
ChangeAndSave(ImGui.ColorConvertFloat4ToU32(tmp), value, setter);
ImGui.SameLine();
if (ImGui.Button($"Default##{name}"))
ChangeAndSave(defaultValue, value, setter);
ImGuiUtil.HoverTooltip(
$"Reset to default: #{defaultValue & 0xFF:X2}{(defaultValue >> 8) & 0xFF:X2}{(defaultValue >> 16) & 0xFF:X2}{defaultValue >> 24:X2}");
ImGui.SameLine();
ImGui.Text(name);
ImGuiUtil.HoverTooltip(tooltip);
}
private static void DrawRestorePenumbraButton()
{
//const string buttonLabel = "Re-Register Penumbra";
// TODO
//if (ImGui.Button(buttonLabel))
// Glamourer.Penumbra.Reattach(true);
//ImGuiUtil.HoverTooltip(
// "If Penumbra did not register the functions for some reason, pressing this button might help restore functionality.");
}
private void DrawSettingsTab()
{
using var tab = ImRaii.TabItem("Settings");
if (!tab)
return;
ImGui.Dummy(_spacing);
Checkmark("Folders First", "Sort Folders before all designs instead of lexicographically.", _config.FoldersFirst,
v => _config.FoldersFirst = v);
Checkmark("Color Designs", "Color the names of designs in the selector using the colors from below for the given cases.",
_config.ColorDesigns,
v => _config.ColorDesigns = v);
Checkmark("Show Locks", "Write-protected Designs show a lock besides their name in the selector.", _config.ShowLocks,
v => _config.ShowLocks = v);
DrawRestorePenumbraButton();
Checkmark("Apply Fixed Designs",
"Automatically apply fixed designs to characters and redraw them when anything changes.",
_config.ApplyFixedDesigns,
v => { _config.ApplyFixedDesigns = v; });
ImGui.Dummy(_spacing);
DrawColorPicker("Customization Color", "The color for designs that only apply their character customization.",
_config.CustomizationColor, Configuration.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);
DrawColorPicker("State Color", "The color for designs that only apply some state modification.",
_config.StateColor, Configuration.DefaultStateColor, c => _config.StateColor = c);
}
}

View file

@ -0,0 +1,17 @@
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
namespace Glamourer.Gui;
public partial class Interface
{
private static Vector2 _spacing = Vector2.Zero;
private static float _actorSelectorWidth;
private static void UpdateState()
{
_spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 };
_actorSelectorWidth = 200 * ImGuiHelpers.GlobalScale;
}
}

View file

@ -0,0 +1,88 @@
using System;
using System.Numerics;
using Dalamud.Data;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using Dalamud.Plugin;
using Glamourer.Designs;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.State;
using ImGuiNET;
using OtterGui.Raii;
namespace Glamourer.Gui;
public partial class Interface : Window, IDisposable
{
private readonly DalamudPluginInterface _pi;
private readonly EquipmentDrawer _equipmentDrawer;
private readonly CustomizationDrawer _customizationDrawer;
private readonly Configuration _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)
: base(GetLabel())
{
_pi = pi;
_config = config;
_equipmentDrawer = new EquipmentDrawer(gameData, items);
_customizationDrawer = new CustomizationDrawer(pi, customization, items);
pi.UiBuilder.DisableGposeUiHide = true;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(675, 675),
MaximumSize = ImGui.GetIO().DisplaySize,
};
_actorTab = new ActorTab(this, activeDesigns, objects, targets, actors, items);
_debugStateTab = new DebugStateTab(activeDesigns);
_debugDataTab = new DebugDataTab(customization);
_designTab = new DesignTab(this, designManager, fileSystem, keyState, activeDesigns, objects);
}
public override void Draw()
{
using var tabBar = ImRaii.TabBar("##Tabs");
if (!tabBar)
return;
try
{
UpdateState();
_actorTab.Draw();
_designTab.Draw();
DrawSettingsTab();
_debugStateTab.Draw();
_debugDataTab.Draw();
// DrawSaves();
// DrawFixedDesignsTab();
// DrawRevertablesTab();
}
catch (Exception e)
{
PluginLog.Error($"Unexpected Error during Draw:\n{e}");
}
}
public void Dispose()
{
_pi.UiBuilder.OpenConfigUi -= Toggle;
_customizationDrawer.Dispose();
_designTab.Dispose();
}
private static string GetLabel()
=> Glamourer.Version.Length == 0
? "Glamourer###GlamourerConfigWindow"
: $"Glamourer v{Glamourer.Version}###GlamourerConfigWindow";
}

View file

@ -0,0 +1,290 @@

namespace Glamourer.Gui;
public partial class Interface
{
//private readonly CharacterSave _currentSave = new();
//private string _newDesignName = string.Empty;
//private bool _keyboardFocus;
//private bool _holdShift;
//private bool _holdCtrl;
//private const string DesignNamePopupLabel = "Save Design As...";
//
//private void DrawPlayerHeader()
//{
// var color = _player == null ? RedHeaderColor : GreenHeaderColor;
// var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
// using var c = ImRaii.PushColor(ImGuiCol.Text, color)
// .Push(ImGuiCol.Button, buttonColor)
// .Push(ImGuiCol.ButtonHovered, buttonColor)
// .Push(ImGuiCol.ButtonActive, buttonColor);
// using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
// .Push(ImGuiStyleVar.FrameRounding, 0);
// ImGui.Button($"{_currentLabel}##playerHeader", -Vector2.UnitX * 0.0001f);
//}
//
//private static void DrawCopyClipboardButton(CharacterSave save)
//{
// ImGui.PushFont(UiBuilder.IconFont);
// if (ImGui.Button(FontAwesomeIcon.Clipboard.ToIconString()))
// ImGui.SetClipboardText(save.ToBase64());
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip("Copy customization code to clipboard.");
//}
//
//private static void ConditionalApply(CharacterSave save, Character player)
//{
// if (ImGui.GetIO().KeyShift)
// save.ApplyOnlyCustomizations(player);
// else if (ImGui.GetIO().KeyCtrl)
// save.ApplyOnlyEquipment(player);
// else
// save.Apply(player);
//}
//
//private static CharacterSave ConditionalCopy(CharacterSave save, bool shift, bool ctrl)
//{
// var copy = save.Copy();
// if (shift)
// {
// copy.Load(new CharacterEquipment());
// copy.SetHatState = false;
// copy.SetVisorState = false;
// copy.SetWeaponState = false;
// copy.WriteEquipment = CharacterEquipMask.None;
// }
// else if (ctrl)
// {
// copy.Load(CharacterCustomization.Default);
// copy.SetHatState = false;
// copy.SetVisorState = false;
// copy.SetWeaponState = false;
// copy.WriteCustomizations = false;
// }
//
// return copy;
//}
//
//private bool DrawApplyClipboardButton()
//{
// ImGui.PushFont(UiBuilder.IconFont);
// var applyButton = ImGui.Button(FontAwesomeIcon.Paste.ToIconString()) && _player != null;
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip(
// "Apply customization code from clipboard.\nHold Shift to apply only customizations.\nHold Control to apply only equipment.");
//
// if (!applyButton)
// return false;
//
// try
// {
// var text = ImGui.GetClipboardText();
// if (!text.Any())
// return false;
//
// var save = CharacterSave.FromString(text);
// ConditionalApply(save, _player!);
// }
// catch (Exception e)
// {
// PluginLog.Information($"{e}");
// return false;
// }
//
// return true;
//}
//
//private void DrawSaveDesignButton()
//{
// ImGui.PushFont(UiBuilder.IconFont);
// if (ImGui.Button(FontAwesomeIcon.Save.ToIconString()))
// OpenDesignNamePopup(DesignNameUse.SaveCurrent);
//
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip("Save the current design.\nHold Shift to save only customizations.\nHold Control to save only equipment.");
//
// DrawDesignNamePopup(DesignNameUse.SaveCurrent);
//}
//
//private void DrawTargetPlayerButton()
//{
// if (ImGui.Button("Target Player"))
// Dalamud.Targets.SetTarget(_player);
//}
//
//private void DrawApplyToPlayerButton(CharacterSave save)
//{
// if (!ImGui.Button("Apply to Self"))
// return;
//
// var player = _inGPose
// ? (Character?)Dalamud.Objects[GPoseObjectId]
// : Dalamud.ClientState.LocalPlayer;
// var fallback = _inGPose ? Dalamud.ClientState.LocalPlayer : null;
// if (player == null)
// return;
//
// ConditionalApply(save, player);
// if (_inGPose)
// ConditionalApply(save, fallback!);
// Glamourer.Penumbra.UpdateCharacters(player, fallback);
//}
//
//
//private static Character? TransformToCustomizable(Character? actor)
//{
// if (actor == null)
// return null;
//
// if (actor.ModelType() == 0)
// return actor;
//
// actor.SetModelType(0);
// CharacterCustomization.Default.Write(actor.Address);
// return actor;
//}
//
//private void DrawApplyToTargetButton(CharacterSave save)
//{
// if (!ImGui.Button("Apply to Target"))
// return;
//
// var player = TransformToCustomizable(CharacterFactory.Convert(Dalamud.Targets.Target));
// if (player == null)
// return;
//
// var fallBackCharacter = _gPoseActors.TryGetValue(player.Name.ToString(), out var f) ? f : null;
// ConditionalApply(save, player);
// if (fallBackCharacter != null)
// ConditionalApply(save, fallBackCharacter!);
// Glamourer.Penumbra.UpdateCharacters(player, fallBackCharacter);
//}
//
//private void DrawRevertButton()
//{
// if (!ImGuiUtil.DrawDisabledButton("Revert", Vector2.Zero, string.Empty, _player == null))
// return;
//
// Glamourer.RevertableDesigns.Revert(_player!);
// var fallBackCharacter = _gPoseActors.TryGetValue(_player!.Name.ToString(), out var f) ? f : null;
// if (fallBackCharacter != null)
// Glamourer.RevertableDesigns.Revert(fallBackCharacter);
// Glamourer.Penumbra.UpdateCharacters(_player, fallBackCharacter);
//}
//
//private void SaveNewDesign(CharacterSave save)
//{
// try
// {
// var (folder, name) = _designs.FileSystem.CreateAllFolders(_newDesignName);
// if (!name.Any())
// return;
//
// var newDesign = new Design(folder, name) { Data = save };
// folder.AddChild(newDesign);
// _designs.Designs[newDesign.FullName()] = save;
// _designs.SaveToFile();
// }
// catch (Exception e)
// {
// PluginLog.Error($"Could not save new design {_newDesignName}:\n{e}");
// }
//}
//
//private void DrawMonsterPanel()
//{
// if (DrawApplyClipboardButton())
// Glamourer.Penumbra.UpdateCharacters(_player!);
//
// ImGui.SameLine();
// if (ImGui.Button("Convert to Character"))
// {
// TransformToCustomizable(_player);
// _currentLabel = _currentLabel.Replace("(Monster)", "(NPC)");
// Glamourer.Penumbra.UpdateCharacters(_player!);
// }
//
// if (!_inGPose)
// {
// ImGui.SameLine();
// DrawTargetPlayerButton();
// }
//
// var currentModel = _player!.ModelType();
// using var combo = ImRaii.Combo("Model Id", currentModel.ToString());
// if (!combo)
// return;
//
// foreach (var (id, _) in _models.Skip(1))
// {
// if (!ImGui.Selectable($"{id:D6}##models", id == currentModel) || id == currentModel)
// continue;
//
// _player!.SetModelType((int)id);
// Glamourer.Penumbra.UpdateCharacters(_player!);
// }
//}
//
//private void DrawPlayerPanel()
//{
// DrawCopyClipboardButton(_currentSave);
// ImGui.SameLine();
// var changes = !_currentSave.WriteProtected && DrawApplyClipboardButton();
// ImGui.SameLine();
// DrawSaveDesignButton();
// ImGui.SameLine();
// DrawApplyToPlayerButton(_currentSave);
// if (!_inGPose)
// {
// ImGui.SameLine();
// DrawApplyToTargetButton(_currentSave);
// if (_player != null && !_currentSave.WriteProtected)
// {
// ImGui.SameLine();
// DrawTargetPlayerButton();
// }
// }
//
// var data = _currentSave;
// if (!_currentSave.WriteProtected)
// {
// ImGui.SameLine();
// DrawRevertButton();
// }
// else
// {
// ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.8f);
// data = data.Copy();
// }
//
// if (DrawCustomization(ref data.Customizations) && _player != null)
// {
// Glamourer.RevertableDesigns.Add(_player);
// _currentSave.Customizations.Write(_player.Address);
// changes = true;
// }
//
// changes |= DrawEquip(data.Equipment);
// changes |= DrawMiscellaneous(data, _player);
//
// if (_player != null && changes)
// Glamourer.Penumbra.UpdateCharacters(_player);
// if (_currentSave.WriteProtected)
// ImGui.PopStyleVar();
//}
//
//private void DrawActorPanel()
//{
// using var group = ImRaii.Group();
// DrawPlayerHeader();
// using var child = ImRaii.Child("##playerData", -Vector2.One, true);
// if (!child)
// return;
//
// if (_player == null || _player.ModelType() == 0)
// DrawPlayerPanel();
// else
// DrawMonsterPanel();
//}
}

View file

@ -0,0 +1,196 @@
using Dalamud.Interface;
using Glamourer.Structs;
using ImGuiNET;
using Lumina.Text;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui;
//internal partial class Interface
//{
// private bool DrawStainSelector(ComboWithFilter<Stain> stainCombo, EquipSlot slot, StainId stainIdx)
// {
// stainCombo.PostPreview = null;
// if (_stains.TryGetValue((byte)stainIdx, out var stain))
// {
// var previewPush = PushColor(stain, ImGuiCol.FrameBg);
// stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush);
// }
//
// var change = stainCombo.Draw(string.Empty, out var newStain) && !newStain.RowIndex.Equals(stainIdx);
// if (!change && (byte)stainIdx != 0)
// {
// ImGuiUtil.HoverTooltip("Right-click to clear.");
// if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
// {
// change = true;
// newStain = Stain.None;
// }
// }
//
// if (!change)
// return false;
//
// if (_player == null)
// return _inDesignMode && (_selection?.Data.WriteStain(slot, newStain.RowIndex) ?? false);
//
// Glamourer.RevertableDesigns.Add(_player);
// newStain.Write(_player.Address, slot);
// return true;
// }
//
// private bool DrawItemSelector(ComboWithFilter<Item> equipCombo, Lumina.Excel.GeneratedSheets.Item item, EquipSlot slot = EquipSlot.Unknown)
// {
// var currentName = item.Name.ToString();
// var change = equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && newItem.Base.RowId != item.RowId;
// if (!change && !ReferenceEquals(item, SmallClothes))
// {
// ImGuiUtil.HoverTooltip("Right-click to clear.");
// if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
// {
// change = true;
// newItem = Item.Nothing(slot);
// }
// }
//
// if (!change)
// return false;
//
// newItem = new Item(newItem.Base, newItem.Name, slot);
// if (_player == null)
// return _inDesignMode && (_selection?.Data.WriteItem(newItem) ?? false);
//
// Glamourer.RevertableDesigns.Add(_player);
// newItem.Write(_player.Address);
// return true;
// }
//
// private static bool DrawCheckbox(CharacterEquipMask flag, ref CharacterEquipMask mask)
// {
// var tmp = (uint)mask;
// var ret = false;
// if (ImGui.CheckboxFlags($"##flag_{(uint)flag}", ref tmp, (uint)flag) && tmp != (uint)mask)
// {
// mask = (CharacterEquipMask)tmp;
// ret = true;
// }
//
// if (ImGui.IsItemHovered())
// ImGui.SetTooltip("Enable writing this slot in this save.");
// return ret;
// }
//
// private static readonly Lumina.Excel.GeneratedSheets.Item SmallClothes = new()
// {
// Name = new SeString("Nothing"),
// RowId = 0,
// };
//
// private static readonly Lumina.Excel.GeneratedSheets.Item SmallClothesNpc = new()
// {
// Name = new SeString("Smallclothes (NPC)"),
// RowId = 1,
// };
//
// private static readonly Lumina.Excel.GeneratedSheets.Item Unknown = new()
// {
// Name = new SeString("Unknown"),
// RowId = 2,
// };
//
// private Lumina.Excel.GeneratedSheets.Item Identify(SetId set, WeaponType weapon, ushort variant, EquipSlot slot)
// {
// return (uint)set switch
// {
// 0 => SmallClothes,
// 9903 => SmallClothesNpc,
// _ => _identifier.Identify(set, weapon, variant, slot.ToSlot()).FirstOrDefault() ?? Unknown,
// };
// }
//
// private bool DrawEquipSlot(EquipSlot slot, CharacterArmor equip)
// {
// var (equipCombo, stainCombo) = _combos[slot];
//
// var ret = DrawStainSelector(stainCombo, slot, equip.Stain);
// ImGui.SameLine();
// var item = Identify(equip.Set, new WeaponType(), equip.Variant, slot);
// ret |= DrawItemSelector(equipCombo, item, slot);
//
// return ret;
// }
//
// private bool DrawEquipSlotWithCheck(EquipSlot slot, CharacterArmor equip, CharacterEquipMask flag, ref CharacterEquipMask mask)
// {
// var ret = DrawCheckbox(flag, ref mask);
// ImGui.SameLine();
// ret |= DrawEquipSlot(slot, equip);
// return ret;
// }
//
// private bool DrawWeapon(EquipSlot slot, CharacterWeapon weapon)
// {
// var (equipCombo, stainCombo) = _combos[slot];
//
// var ret = DrawStainSelector(stainCombo, slot, weapon.Stain);
// ImGui.SameLine();
// var item = Identify(weapon.Set, weapon.Type, weapon.Variant, slot);
// ret |= DrawItemSelector(equipCombo, item, slot);
//
// return ret;
// }
//
// private bool DrawWeaponWithCheck(EquipSlot slot, CharacterWeapon weapon, CharacterEquipMask flag, ref CharacterEquipMask mask)
// {
// var ret = DrawCheckbox(flag, ref mask);
// ImGui.SameLine();
// ret |= DrawWeapon(slot, weapon);
// return ret;
// }
//
// private bool DrawEquip(CharacterEquipment equip)
// {
// var ret = false;
// if (ImGui.CollapsingHeader("Character Equipment"))
// {
// ret |= DrawWeapon(EquipSlot.MainHand, equip.MainHand);
// ret |= DrawWeapon(EquipSlot.OffHand, equip.OffHand);
// ret |= DrawEquipSlot(EquipSlot.Head, equip.Head);
// ret |= DrawEquipSlot(EquipSlot.Body, equip.Body);
// ret |= DrawEquipSlot(EquipSlot.Hands, equip.Hands);
// ret |= DrawEquipSlot(EquipSlot.Legs, equip.Legs);
// ret |= DrawEquipSlot(EquipSlot.Feet, equip.Feet);
// ret |= DrawEquipSlot(EquipSlot.Ears, equip.Ears);
// ret |= DrawEquipSlot(EquipSlot.Neck, equip.Neck);
// ret |= DrawEquipSlot(EquipSlot.Wrists, equip.Wrists);
// ret |= DrawEquipSlot(EquipSlot.RFinger, equip.RFinger);
// ret |= DrawEquipSlot(EquipSlot.LFinger, equip.LFinger);
// }
//
// return ret;
// }
//
// private bool DrawEquip(CharacterEquipment equip, ref CharacterEquipMask mask)
// {
// var ret = false;
// if (ImGui.CollapsingHeader("Character Equipment"))
// {
// ret |= DrawWeaponWithCheck(EquipSlot.MainHand, equip.MainHand, CharacterEquipMask.MainHand, ref mask);
// ret |= DrawWeaponWithCheck(EquipSlot.OffHand, equip.OffHand, CharacterEquipMask.OffHand, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.Head, equip.Head, CharacterEquipMask.Head, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.Body, equip.Body, CharacterEquipMask.Body, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.Hands, equip.Hands, CharacterEquipMask.Hands, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.Legs, equip.Legs, CharacterEquipMask.Legs, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.Feet, equip.Feet, CharacterEquipMask.Feet, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.Ears, equip.Ears, CharacterEquipMask.Ears, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.Neck, equip.Neck, CharacterEquipMask.Neck, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.Wrists, equip.Wrists, CharacterEquipMask.Wrists, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.RFinger, equip.RFinger, CharacterEquipMask.RFinger, ref mask);
// ret |= DrawEquipSlotWithCheck(EquipSlot.LFinger, equip.LFinger, CharacterEquipMask.LFinger, ref mask);
// }
//
// return ret;
// }
//}

View file

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Glamourer.Designs;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui.Raii;
namespace Glamourer.Gui;
//internal partial class Interface
//{
// private const string FixDragDropLabel = "##FixDragDrop";
//
// private List<string>? _fullPathCache;
// private string _newFixCharacterName = string.Empty;
// private string _newFixDesignPath = string.Empty;
// private JobGroup? _newFixDesignGroup;
// private Design? _newFixDesign;
// private int _fixDragDropIdx = -1;
//
// private static unsafe bool IsDropping()
// => ImGui.AcceptDragDropPayload(FixDragDropLabel).NativePtr != null;
//
// private void DrawFixedDesignsTab()
// {
// _newFixDesignGroup ??= Glamourer.FixedDesignManager.FixedDesigns.JobGroups[1];
//
// using var tabItem = ImRaii.TabItem("Fixed Designs");
// if (!tabItem)
// {
// _fullPathCache = null;
// _newFixDesign = null;
// _newFixDesignPath = string.Empty;
// _newFixDesignGroup = Glamourer.FixedDesignManager.FixedDesigns.JobGroups[1];
// return;
// }
//
// _fullPathCache ??= Glamourer.FixedDesignManager.FixedDesigns.Data.Select(d => d.Design.FullName()).ToList();
//
// using var table = ImRaii.Table("##FixedTable", 4);
// var buttonWidth = 23.5f * ImGuiHelpers.GlobalScale;
//
// ImGui.TableSetupColumn("##DeleteColumn", ImGuiTableColumnFlags.WidthFixed, 2 * buttonWidth);
// ImGui.TableSetupColumn("Character", ImGuiTableColumnFlags.WidthFixed, 200 * ImGuiHelpers.GlobalScale);
// ImGui.TableSetupColumn("Jobs", ImGuiTableColumnFlags.WidthFixed, 175 * ImGuiHelpers.GlobalScale);
// ImGui.TableSetupColumn("Design", ImGuiTableColumnFlags.WidthStretch);
// ImGui.TableHeadersRow();
// var xPos = 0f;
//
// using var style = new ImRaii.Style();
// using var font = new ImRaii.Font();
// for (var i = 0; i < _fullPathCache.Count; ++i)
// {
// var path = _fullPathCache[i];
// var name = Glamourer.FixedDesignManager.FixedDesigns.Data[i];
//
// ImGui.TableNextRow();
// ImGui.TableNextColumn();
// style.Push(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2);
// font.Push(UiBuilder.IconFont);
// if (ImGui.Button($"{FontAwesomeIcon.Trash.ToIconChar()}##{i}"))
// {
// _fullPathCache.RemoveAt(i--);
// Glamourer.FixedDesignManager.FixedDesigns.Remove(name);
// continue;
// }
//
// var tmp = name.Enabled;
// ImGui.SameLine();
// xPos = ImGui.GetCursorPosX();
// if (ImGui.Checkbox($"##Enabled{i}", ref tmp))
// if (tmp && Glamourer.FixedDesignManager.FixedDesigns.EnableDesign(name)
// || !tmp && Glamourer.FixedDesignManager.FixedDesigns.DisableDesign(name))
// {
// Glamourer.Config.FixedDesigns[i].Enabled = tmp;
// Glamourer.Config.Save();
// }
//
// style.Pop();
// font.Pop();
// ImGui.TableNextColumn();
// ImGui.Selectable($"{name.Name}##Fix{i}");
// if (ImGui.BeginDragDropSource())
// {
// _fixDragDropIdx = i;
// ImGui.SetDragDropPayload("##FixDragDrop", IntPtr.Zero, 0);
// ImGui.Text($"Dragging {name.Name} ({path})...");
// ImGui.EndDragDropSource();
// }
//
// if (ImGui.BeginDragDropTarget())
// {
// if (IsDropping() && _fixDragDropIdx >= 0)
// {
// var d = Glamourer.FixedDesignManager.FixedDesigns.Data[_fixDragDropIdx];
// Glamourer.FixedDesignManager.FixedDesigns.Move(d, i);
// var p = _fullPathCache[_fixDragDropIdx];
// _fullPathCache.RemoveAt(_fixDragDropIdx);
// _fullPathCache.Insert(i, p);
// _fixDragDropIdx = -1;
// }
//
// ImGui.EndDragDropTarget();
// }
//
// ImGui.TableNextColumn();
// ImGui.Text(Glamourer.FixedDesignManager.FixedDesigns.Data[i].Jobs.Name);
// ImGui.TableNextColumn();
// ImGui.Text(path);
// }
//
// ImGui.TableNextRow();
// ImGui.TableNextColumn();
// font.Push(UiBuilder.IconFont);
//
// ImGui.SetCursorPosX(xPos);
// if (_newFixDesign == null || _newFixCharacterName == string.Empty)
// {
// style.Push(ImGuiStyleVar.Alpha, 0.5f);
// ImGui.Button($"{FontAwesomeIcon.Plus.ToIconChar()}##NewFix");
// style.Pop();
// }
// else if (ImGui.Button($"{FontAwesomeIcon.Plus.ToIconChar()}##NewFix"))
// {
// _fullPathCache.Add(_newFixDesignPath);
// Glamourer.FixedDesignManager.FixedDesigns.Add(_newFixCharacterName, _newFixDesign, _newFixDesignGroup.Value, false);
// _newFixCharacterName = string.Empty;
// _newFixDesignPath = string.Empty;
// _newFixDesign = null;
// _newFixDesignGroup = Glamourer.FixedDesignManager.FixedDesigns.JobGroups[1];
// }
//
// font.Pop();
// ImGui.TableNextColumn();
// ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
// ImGui.InputTextWithHint("##NewFix", "Enter new Character", ref _newFixCharacterName, 32);
// ImGui.TableNextColumn();
// ImGui.SetNextItemWidth(-1);
// using var combo = ImRaii.Combo("##NewFixDesignGroup", _newFixDesignGroup.Value.Name);
// if (combo)
// foreach (var (id, group) in Glamourer.FixedDesignManager.FixedDesigns.JobGroups)
// {
// ImGui.SetNextItemWidth(-1);
// if (ImGui.Selectable($"{group.Name}##NewFixDesignGroup", group.Name == _newFixDesignGroup.Value.Name))
// _newFixDesignGroup = group;
// }
//
// ImGui.TableNextColumn();
// ImGui.SetNextItemWidth(-1);
// using var combo2 = ImRaii.Combo("##NewFixPath", _newFixDesignPath);
// if (!combo2)
// return;
//
// foreach (var design in _plugin.Designs.FileSystem.Root.AllLeaves(SortMode.Lexicographical).Cast<Design>())
// {
// var fullName = design.FullName();
// ImGui.SetNextItemWidth(-1);
// if (!ImGui.Selectable($"{fullName}##NewFixDesign", fullName == _newFixDesignPath))
// continue;
//
// _newFixDesignPath = fullName;
// _newFixDesign = design;
// }
// }
//}

View file

@ -0,0 +1,90 @@
using System;
using System.Linq;
using Dalamud.Logging;
using Glamourer.Customization;
using Glamourer.Structs;
using ImGuiNET;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui;
//internal partial class Interface
//{
//
//
//
//
// private enum DesignNameUse
// {
// SaveCurrent,
// NewDesign,
// DuplicateDesign,
// NewFolder,
// FromClipboard,
// }
//
// private void DrawDesignNamePopup(DesignNameUse use)
// {
// if (ImGui.BeginPopup($"{DesignNamePopupLabel}{use}"))
// {
// if (ImGui.InputText("##designName", ref _newDesignName, 64, ImGuiInputTextFlags.EnterReturnsTrue)
// && _newDesignName.Any())
// {
// switch (use)
// {
// case DesignNameUse.SaveCurrent:
// SaveNewDesign(ConditionalCopy(_currentSave, _holdShift, _holdCtrl));
// break;
// case DesignNameUse.NewDesign:
// var empty = new CharacterSave();
// empty.Load(CharacterCustomization.Default);
// empty.WriteCustomizations = false;
// SaveNewDesign(empty);
// break;
// case DesignNameUse.DuplicateDesign:
// SaveNewDesign(ConditionalCopy(_selection!.Data, _holdShift, _holdCtrl));
// break;
// case DesignNameUse.NewFolder:
// _designs.FileSystem
// .CreateAllFolders($"{_newDesignName}/a"); // Filename is just ignored, but all folders are created.
// break;
// case DesignNameUse.FromClipboard:
// try
// {
// var text = ImGui.GetClipboardText();
// var save = CharacterSave.FromString(text);
// SaveNewDesign(save);
// }
// catch (Exception e)
// {
// PluginLog.Information($"Could not save new Design from Clipboard:\n{e}");
// }
//
// break;
// }
//
// _newDesignName = string.Empty;
// ImGui.CloseCurrentPopup();
// }
//
// if (_keyboardFocus)
// {
// ImGui.SetKeyboardFocusHere();
// _keyboardFocus = false;
// }
//
// ImGui.EndPopup();
// }
// }
//
// private void OpenDesignNamePopup(DesignNameUse use)
// {
// _newDesignName = string.Empty;
// _keyboardFocus = true;
// _holdCtrl = ImGui.GetIO().KeyCtrl;
// _holdShift = ImGui.GetIO().KeyShift;
// ImGui.OpenPopup($"{DesignNamePopupLabel}{use}");
// }
//}

View file

@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Numerics;
using System.Reflection;
using ImGuiNET;
using Penumbra.GameData.Enums;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Gui;
//internal partial class Interface
//{
// private const float ColorButtonWidth = 22.5f;
// private const float ColorComboWidth = 140f;
// private const float ItemComboWidth = 350f;
//
// private static readonly Vector4 GreyVector = new(0.5f, 0.5f, 0.5f, 1);
//
// private static ComboWithFilter<Stain> CreateDefaultStainCombo(IReadOnlyList<Stain> stains)
// => new("##StainCombo", ColorComboWidth, ColorButtonWidth, stains,
// s => s.Name.ToString())
// {
// Flags = ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge,
// PreList = () =>
// {
// ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
// ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
// ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
// },
// PostList = () => { ImGui.PopStyleVar(3); },
// CreateSelectable = s =>
// {
// var push = PushColor(s);
// var ret = ImGui.Button($"{s.Name}##Stain{(byte)s.RowIndex}",
// Vector2.UnitX * (ColorComboWidth - ImGui.GetStyle().ScrollbarSize));
// ImGui.PopStyleColor(push);
// return ret;
// },
// ItemsAtOnce = 12,
// };
//
// private ComboWithFilter<Item> CreateItemCombo(EquipSlot slot, IReadOnlyList<Item> items)
// => new($"{_equipSlotNames[slot]}##Equip", ItemComboWidth, ItemComboWidth, items, i => i.Name)
// {
// Flags = ImGuiComboFlags.HeightLarge,
// CreateSelectable = i =>
// {
// var ret = ImGui.Selectable(i.Name);
// var setId = $"({(int)i.MainModel.id})";
// var size = ImGui.CalcTextSize(setId).X;
// ImGui.SameLine(ImGui.GetWindowContentRegionWidth() - size - ImGui.GetStyle().ItemInnerSpacing.X);
// ImGui.TextColored(GreyVector, setId);
// return ret;
// },
// };
//
// private (ComboWithFilter<Item>, ComboWithFilter<Stain>) CreateCombos(EquipSlot slot, IReadOnlyList<Item> items,
// ComboWithFilter<Stain> defaultStain)
// => (CreateItemCombo(slot, items), new ComboWithFilter<Stain>($"##{slot}Stain", defaultStain));
//
//
// private static Dictionary<EquipSlot, string> GetEquipSlotNames()
// {
// var sheet = Dalamud.GameData.GetExcelSheet<Addon>()!;
// var ret = new Dictionary<EquipSlot, string>(12)
// {
// [EquipSlot.MainHand] = sheet.GetRow(738)?.Text.ToString() ?? "Main Hand",
// [EquipSlot.OffHand] = sheet.GetRow(739)?.Text.ToString() ?? "Off Hand",
// [EquipSlot.Head] = sheet.GetRow(740)?.Text.ToString() ?? "Head",
// [EquipSlot.Body] = sheet.GetRow(741)?.Text.ToString() ?? "Body",
// [EquipSlot.Hands] = sheet.GetRow(742)?.Text.ToString() ?? "Hands",
// [EquipSlot.Legs] = sheet.GetRow(744)?.Text.ToString() ?? "Legs",
// [EquipSlot.Feet] = sheet.GetRow(745)?.Text.ToString() ?? "Feet",
// [EquipSlot.Ears] = sheet.GetRow(746)?.Text.ToString() ?? "Ears",
// [EquipSlot.Neck] = sheet.GetRow(747)?.Text.ToString() ?? "Neck",
// [EquipSlot.Wrists] = sheet.GetRow(748)?.Text.ToString() ?? "Wrists",
// [EquipSlot.RFinger] = sheet.GetRow(749)?.Text.ToString() ?? "Right Ring",
// [EquipSlot.LFinger] = sheet.GetRow(750)?.Text.ToString() ?? "Left Ring",
// };
// return ret;
// }
//}

View file

@ -0,0 +1,52 @@
using System;
using Dalamud.Game.ClientState.Objects.Types;
using ImGuiNET;
namespace Glamourer.Gui;
//internal partial class Interface
//{
//
// private static bool DrawMiscellaneous(CharacterSave save, Character? player)
// {
// var ret = false;
// if (!ImGui.CollapsingHeader("Miscellaneous"))
// return ret;
//
// ret |= DrawCheckMark("Hat Visible", save.HatState, v =>
// {
// save.HatState = v;
// player?.SetHatVisible(v);
// });
//
// ret |= DrawCheckMark("Weapon Visible", save.WeaponState, v =>
// {
// save.WeaponState = v;
// player?.SetWeaponHidden(!v);
// });
//
// ret |= DrawCheckMark("Visor Toggled", save.VisorState, v =>
// {
// save.VisorState = v;
// player?.SetVisorToggled(v);
// });
//
// ret |= DrawCheckMark("Is Wet", save.IsWet, v =>
// {
// save.IsWet = v;
// player?.SetWetness(v);
// });
//
// var alpha = save.Alpha;
// if (ImGui.DragFloat("Alpha", ref alpha, 0.01f, 0f, 1f, "%.2f") && alpha != save.Alpha)
// {
// alpha = (float)Math.Round(alpha > 1 ? 1 : alpha < 0 ? 0 : alpha, 2);
// save.Alpha = alpha;
// ret = true;
// if (player != null)
// player.Alpha() = alpha;
// }
//
// return ret;
// }
//}

View file

@ -0,0 +1,87 @@
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using ImGuiNET;
using OtterGui.Raii;
namespace Glamourer.Gui;
//internal partial class Interface
//{
// private string? _currentRevertableName;
// private CharacterSave? _currentRevertable;
//
// private void DrawRevertablesSelector()
// {
// ImGui.BeginGroup();
// DrawPlayerFilter();
// if (!ImGui.BeginChild("##playerSelector",
// new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true))
// {
// ImGui.EndChild();
// ImGui.EndGroup();
// return;
// }
//
// foreach (var (name, save) in Glamourer.RevertableDesigns.Saves)
// {
// if (name.ToLowerInvariant().Contains(_playerFilterLower) && ImGui.Selectable(name, name == _currentRevertableName))
// {
// _currentRevertableName = name;
// _currentRevertable = save;
// }
// }
//
// using (var _ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
// {
// ImGui.EndChild();
// }
//
// DrawSelectionButtons();
// ImGui.EndGroup();
// }
//
// private void DrawRevertablePanel()
// {
// using var group = ImRaii.Group();
// {
// var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
// using var color = ImRaii.PushColor(ImGuiCol.Text, GreenHeaderColor)
// .Push(ImGuiCol.Button, buttonColor)
// .Push(ImGuiCol.ButtonHovered, buttonColor)
// .Push(ImGuiCol.ButtonActive, buttonColor);
// using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
// .Push(ImGuiStyleVar.FrameRounding, 0);
// ImGui.Button($"{_currentRevertableName}##playerHeader", -Vector2.UnitX * 0.0001f);
// }
//
// if (!ImGui.BeginChild("##revertableData", -Vector2.One, true))
// {
// ImGui.EndChild();
// return;
// }
//
// var save = _currentRevertable!.Copy();
// DrawCustomization(ref save.Customizations);
// DrawEquip(save.Equipment);
// DrawMiscellaneous(save, null);
//
// ImGui.EndChild();
// }
//
// [Conditional("DEBUG")]
// private void DrawRevertablesTab()
// {
// using var tabItem = ImRaii.TabItem("Revertables");
// if (!tabItem)
// return;
//
// DrawRevertablesSelector();
//
// if (_currentRevertableName == null)
// return;
//
// ImGui.SameLine();
// DrawRevertablePanel();
// }
//}

View file

@ -0,0 +1,165 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.Customization;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Structs;
using Penumbra.String;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Interop;
public unsafe partial struct Actor : IEquatable<Actor>, IDesignable
{
public static readonly Actor Null = new() { Pointer = null };
public FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Pointer;
public IntPtr Address
=> (IntPtr)Pointer;
public static implicit operator Actor(IntPtr? pointer)
=> new() { Pointer = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)(pointer ?? IntPtr.Zero) };
public static implicit operator IntPtr(Actor actor)
=> actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer;
public ActorIdentifier GetIdentifier(ActorManager actors)
=> actors.FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Pointer, out _, true, true, false);
public bool Identifier(ActorManager actors, out ActorIdentifier ident)
{
if (Valid)
{
ident = GetIdentifier(actors);
return true;
}
ident = ActorIdentifier.Invalid;
return false;
}
public override string ToString()
=> Pointer != null ? Utf8Name.ToString() : "Invalid";
public bool IsAvailable
=> Pointer->GameObject.GetIsTargetable();
public bool IsHuman
=> Pointer != null && Pointer->ModelCharaId == 0;
public ObjectKind ObjectKind
{
get => (ObjectKind)Pointer->GameObject.ObjectKind;
set => Pointer->GameObject.ObjectKind = (byte)value;
}
public ByteString Utf8Name
=> new(Pointer->GameObject.Name);
public byte Job
=> Pointer->ClassJob;
public DrawObject DrawObject
=> (IntPtr)Pointer->GameObject.DrawObject;
public bool Valid
=> Pointer != null;
public int Index
=> Pointer->GameObject.ObjectIndex;
public uint ModelId
{
get => (uint)Pointer->ModelCharaId;
set => Pointer->ModelCharaId = (int)value;
}
public ushort UsedMountId
=> !IsHuman ? (ushort)0 : *(ushort*)((byte*)Pointer + 0x668);
public ushort CompanionId
=> ObjectKind == ObjectKind.Companion ? *(ushort*)((byte*)Pointer + 0x1AAC) : (ushort)0;
public Customize Customize
=> new(*(CustomizeData*)&Pointer->DrawData.CustomizeData);
public CharacterEquip Equip
=> new((CharacterArmor*)&Pointer->DrawData.Head);
public CharacterWeapon MainHand
{
get => *(CharacterWeapon*)&Pointer->DrawData.MainHandModel;
set => *(CharacterWeapon*)&Pointer->DrawData.MainHandModel = value;
}
public CharacterWeapon OffHand
{
get => *(CharacterWeapon*)&Pointer->DrawData.OffHandModel;
set => *(CharacterWeapon*)&Pointer->DrawData.OffHandModel = value;
}
public unsafe bool VisorEnabled
{
get => (*(byte*)(Address + Offsets.Character.VisorToggled) & Offsets.Character.Flags.IsVisorToggled) != 0;
set => *(byte*)(Address + Offsets.Character.VisorToggled) = (byte)(value
? *(byte*)(Address + Offsets.Character.VisorToggled) | Offsets.Character.Flags.IsVisorToggled
: *(byte*)(Address + Offsets.Character.VisorToggled) & ~Offsets.Character.Flags.IsVisorToggled);
}
public unsafe bool WeaponEnabled
{
get => (*(byte*)(Address + Offsets.Character.WeaponHidden1) & Offsets.Character.Flags.IsWeaponHidden1) == 0;
set
{
ref var w1 = ref *(byte*)(Address + Offsets.Character.WeaponHidden1);
ref var w2 = ref *(byte*)(Address + Offsets.Character.WeaponHidden2);
if (value)
{
w1 = (byte)(w1 & ~Offsets.Character.Flags.IsWeaponHidden1);
w2 = (byte)(w2 & ~Offsets.Character.Flags.IsWeaponHidden2);
}
else
{
w1 = (byte)(w1 | Offsets.Character.Flags.IsWeaponHidden1);
w2 = (byte)(w2 | Offsets.Character.Flags.IsWeaponHidden2);
}
}
}
public bool IsWet { get; set; }
public void SetModelId(int value)
{
if (Pointer != null)
Pointer->ModelCharaId = value;
}
public static implicit operator bool(Actor actor)
=> actor.Pointer != null;
public static bool operator true(Actor actor)
=> actor.Pointer != null;
public static bool operator false(Actor actor)
=> actor.Pointer == null;
public static bool operator !(Actor actor)
=> actor.Pointer == null;
public bool Equals(Actor other)
=> Pointer == other.Pointer;
public override bool Equals(object? obj)
=> obj is Actor other && Equals(other);
public override int GetHashCode()
=> ((ulong)Pointer).GetHashCode();
public static bool operator ==(Actor lhs, Actor rhs)
=> lhs.Pointer == rhs.Pointer;
public static bool operator !=(Actor lhs, Actor rhs)
=> lhs.Pointer != rhs.Pointer;
}

View file

@ -0,0 +1,24 @@
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe class ChangeCustomizeService
{
public ChangeCustomizeService()
=> SignatureHelper.Initialise(this);
public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment);
[Signature(Sigs.ChangeCustomize)]
private readonly ChangeCustomizeDelegate _changeCustomize = null!;
public bool UpdateCustomize(Actor actor, CustomizeData customize)
{
if (customize.Data == null || !actor.Valid || !actor.DrawObject.Valid)
return false;
return _changeCustomize(actor.DrawObject.Pointer, customize.Data, 1);
}
}

View file

@ -0,0 +1,100 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.GameData.Structs;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Interop;
public unsafe partial struct DrawObject : IEquatable<DrawObject>, IDesignable
{
public Human* Pointer;
public IntPtr Address
=> (IntPtr)Pointer;
public static implicit operator DrawObject(IntPtr? pointer)
=> new() { Pointer = (Human*)(pointer ?? IntPtr.Zero) };
public static implicit operator IntPtr(DrawObject drawObject)
=> drawObject.Pointer == null ? IntPtr.Zero : (IntPtr)drawObject.Pointer;
public bool Valid
=> Pointer != null;
public uint ModelId
=> 0;
public bool IsWet
=> false;
public uint Type
=> (*(delegate* unmanaged<Human*, uint>**)Pointer)[50](Pointer);
public Customize Customize
=> *(Customize*)Pointer->CustomizeData;
public CharacterEquip Equip
=> new((CharacterArmor*)Pointer->EquipSlotData);
public CharacterWeapon MainHand
{
get
{
var child = (byte*)Pointer->CharacterBase.DrawObject.Object.ChildObject;
if (child == null)
return CharacterWeapon.Empty;
return *(CharacterWeapon*)(child + 0x8F0);
}
}
public unsafe CharacterWeapon OffHand
{
get
{
var child = Pointer->CharacterBase.DrawObject.Object.ChildObject;
if (child == null)
return CharacterWeapon.Empty;
var sibling = (byte*)child->NextSiblingObject;
if (sibling == null)
return CharacterWeapon.Empty;
return *(CharacterWeapon*)(sibling + 0x8F0);
}
}
public unsafe bool VisorEnabled
=> (*(byte*)(Address + 0x90) & 0x40) != 0;
public unsafe bool WeaponEnabled
=> false;
public static implicit operator bool(DrawObject actor)
=> actor.Pointer != null;
public static bool operator true(DrawObject actor)
=> actor.Pointer != null;
public static bool operator false(DrawObject actor)
=> actor.Pointer == null;
public static bool operator !(DrawObject actor)
=> actor.Pointer == null;
public bool Equals(DrawObject other)
=> Pointer == other.Pointer;
public override bool Equals(object? obj)
=> obj is DrawObject other && Equals(other);
public override int GetHashCode()
=> unchecked((int)(long)Pointer);
public static bool operator ==(DrawObject lhs, DrawObject rhs)
=> lhs.Pointer == rhs.Pointer;
public static bool operator !=(DrawObject lhs, DrawObject rhs)
=> lhs.Pointer != rhs.Pointer;
}

View file

@ -0,0 +1,17 @@
using Glamourer.Customization;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public interface IDesignable
{
public bool Valid { get; }
public uint ModelId { get; }
public Customize Customize { get; }
public CharacterEquip Equip { get; }
public CharacterWeapon MainHand { get; }
public CharacterWeapon OffHand { get; }
public bool VisorEnabled { get; }
public bool WeaponEnabled { get; }
public bool IsWet { get; }
}

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using Dalamud.Data;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Glamourer.Structs;
namespace Glamourer.Interop;
public class JobService : IDisposable
{
public readonly IReadOnlyDictionary<byte, Job> Jobs;
public readonly IReadOnlyDictionary<ushort, JobGroup> JobGroups;
public event Action<Actor, Job>? JobChanged;
public JobService(DataManager gameData)
{
SignatureHelper.Initialise(this);
Jobs = GameData.Jobs(gameData);
JobGroups = GameData.JobGroups(gameData);
_changeJobHook.Enable();
}
public void Dispose()
{
_changeJobHook.Dispose();
}
private delegate void ChangeJobDelegate(nint data, uint job);
[Signature(Sigs.ChangeJob, DetourName = nameof(ChangeJobDetour))]
private readonly Hook<ChangeJobDelegate> _changeJobHook = null!;
private void ChangeJobDetour(nint data, uint jobIndex)
{
_changeJobHook.Original(data, jobIndex);
var actor = (Actor)(data - Offsets.Character.ClassJobContainer);
var job = Jobs[(byte)jobIndex];
Glamourer.Log.Excessive($"{actor} changed job to {job}");
JobChanged?.Invoke(actor, job);
}
}

View file

@ -0,0 +1,160 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Glamourer.Services;
using Penumbra.GameData.Actors;
namespace Glamourer.Interop;
public readonly struct ActorData
{
public readonly List<Actor> Objects;
public readonly string Label;
public bool Valid
=> Objects.Count > 0;
public ActorData(Actor actor, string label)
{
Objects = new List<Actor> { actor };
Label = label;
}
public static readonly ActorData Invalid = new(false);
private ActorData(bool _)
{
Objects = new List<Actor>(0);
Label = string.Empty;
}
}
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);
private void HandleIdentifier(ActorIdentifier identifier, Actor character)
{
if (!character.DrawObject || !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 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.Identifier(_actors.AwaitedService, out var identifier))
break;
HandleIdentifier(identifier, 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 && gPose.Utf8Name.Length > 0;
}
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

@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using Glamourer.Api;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Interop;
public unsafe partial class RedrawManager : IDisposable
{
private readonly ItemManager _items;
private readonly ActorService _actors;
private readonly FixedDesignManager _fixedDesignManager;
private readonly ActiveDesign.Manager _stateManager;
private readonly PenumbraAttach _penumbra;
private readonly WeaponService _weapons;
public RedrawManager(FixedDesignManager fixedDesignManager, ActiveDesign.Manager stateManager, ItemManager items, ActorService actors,
PenumbraAttach penumbra, WeaponService weapons)
{
SignatureHelper.Initialise(this);
_fixedDesignManager = fixedDesignManager;
_stateManager = stateManager;
_items = items;
_actors = actors;
_penumbra = penumbra;
_weapons = weapons;
_penumbra.CreatingCharacterBase += OnCharacterRedraw;
_penumbra.CreatedCharacterBase += OnCharacterRedrawFinished;
}
public void Dispose()
{
}
private void OnCharacterRedraw(Actor actor, uint* modelId, Customize customize, CharacterEquip equip)
{
// Do not apply anything if the game object model id does not correspond to the draw object model id.
// This is the case if the actor is transformed to a different creature.
if (actor.ModelId != *modelId)
return;
// Check if we have a current design in use, or if not if the actor has a fixed design.
var identifier = actor.GetIdentifier(_actors.AwaitedService);
if (!_stateManager.TryGetValue(identifier, out var save))
return;
// Compare game object customize data against draw object customize data for transformations.
// Apply customization if they correspond and there is customization to apply.
var gameObjectCustomize = new Customize(*(CustomizeData*)&actor.Pointer->DrawData.CustomizeData);
if (gameObjectCustomize.Equals(customize))
customize.Load(save.ModelData.Customize);
// Compare game object equip data against draw object equip data for transformations.
// Apply each piece of equip that should be applied if they correspond.
var gameObjectEquip = new CharacterEquip((CharacterArmor*)&actor.Pointer->DrawData.Head);
if (gameObjectEquip.Equals(equip))
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
(_, equip[slot]) =
_items.ResolveRestrictedGear(save.ModelData.Armor(slot), slot, customize.Race, customize.Gender);
}
}
}
private void OnCharacterRedraw(IntPtr gameObject, string collection, IntPtr modelId, IntPtr customize, IntPtr equipData)
{
try
{
OnCharacterRedraw(gameObject, (uint*)modelId, new Customize(*(CustomizeData*)customize),
new CharacterEquip((CharacterArmor*)equipData));
}
catch (Exception e)
{
PluginLog.Error($"Error on new draw object creation:\n{e}");
}
}
private static void OnCharacterRedrawFinished(IntPtr gameObject, string collection, IntPtr drawObject)
{
//SetVisor((Human*)drawObject, true);
}
}

View file

@ -0,0 +1,77 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable
{
public UpdateSlotService()
{
SignatureHelper.Initialise(this);
_flagSlotForUpdateHook.Enable();
}
public void Dispose()
=> _flagSlotForUpdateHook.Dispose();
private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data);
public delegate void FlagSlotForUpdateDelegate(DrawObject drawObject, EquipSlot slot, ref CharacterArmor item);
// This gets called when one of the ten equip items of an existing draw object gets changed.
[Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagSlotForUpdateHook = null!;
public event FlagSlotForUpdateDelegate? EquipUpdate;
public ulong FlagSlotForUpdateInterop(DrawObject drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data)
{
InvokeFlagSlotEvent(drawObject, slot, ref data);
FlagSlotForUpdateInterop(drawObject, slot, data);
}
public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain)
{
var armor = drawObject.Equip[slot] with { Stain = stain };
UpdateSlot(drawObject, slot, armor);
}
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
InvokeFlagSlotEvent(drawObject, slot, ref *data);
return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data);
}
private void InvokeFlagSlotEvent(DrawObject drawObject, EquipSlot slot, ref CharacterArmor armor)
{
if (EquipUpdate == null)
{
Glamourer.Log.Excessive(
$"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}.");
return;
}
var iv = armor;
foreach (var del in EquipUpdate.GetInvocationList().OfType<FlagSlotForUpdateDelegate>())
{
try
{
del(drawObject, slot, ref armor);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not invoke {nameof(EquipUpdate)} Subscriber:\n{ex}");
}
}
Glamourer.Log.Excessive(
$"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}, initial armor was {iv.Set.Value}-{iv.Variant} with stain {iv.Stain.Value}.");
}
}

View file

@ -0,0 +1,78 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public class VisorService : IDisposable
{
public VisorService()
{
SignatureHelper.Initialise(this);
_setupVisorHook.Enable();
}
public void Dispose()
=> _setupVisorHook.Dispose();
public static unsafe bool GetVisorState(nint humanPtr)
{
if (humanPtr == IntPtr.Zero)
return false;
var data = (Human*)humanPtr;
var flags = &data->CharacterBase.UnkFlags_01;
return (*flags & Offsets.DrawObjectVisorStateFlag) != 0;
}
public unsafe void SetVisorState(nint humanPtr, bool on)
{
if (humanPtr == IntPtr.Zero)
return;
var data = (Human*)humanPtr;
_setupVisorHook.Original(humanPtr, (ushort) data->HeadSetID, on);
}
private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, bool on);
public delegate void UpdateVisorDelegate(DrawObject human, SetId modelId, ref bool on);
[Signature(Penumbra.GameData.Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))]
private readonly Hook<UpdateVisorDelegateInternal> _setupVisorHook = null!;
public event UpdateVisorDelegate? VisorUpdate;
private void SetupVisorDetour(nint humanPtr, ushort modelId, bool on)
{
InvokeVisorEvent(humanPtr, modelId, ref on);
_setupVisorHook.Original(humanPtr, modelId, on);
}
private void InvokeVisorEvent(DrawObject drawObject, SetId modelId, ref bool on)
{
if (VisorUpdate == null)
{
Glamourer.Log.Excessive($"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}.");
return;
}
var initialValue = on;
foreach (var del in VisorUpdate.GetInvocationList().OfType<UpdateVisorDelegate>())
{
try
{
del(drawObject, modelId, ref on);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not invoke {nameof(VisorUpdate)} Subscriber:\n{ex}");
}
}
Glamourer.Log.Excessive(
$"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}, initial call was {initialValue}.");
}
}

View file

@ -0,0 +1,120 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe class WeaponService : IDisposable
{
public WeaponService()
{
SignatureHelper.Initialise(this);
_loadWeaponHook.Enable();
}
public void Dispose()
{
_loadWeaponHook.Dispose();
}
public static readonly int CharacterWeaponOffset = (int)Marshal.OffsetOf<Character>("DrawData");
public delegate void LoadWeaponDelegate(nint offsetCharacter, uint slot, ulong weapon, byte redrawOnEquality, byte unk2,
byte skipGameObject,
byte unk4);
// Weapons for a specific character are reloaded with this function.
// The first argument is a pointer to the game object but shifted a bit inside.
// slot is 0 for main hand, 1 for offhand, 2 for unknown (always called with empty data.
// weapon argument is the new weapon data.
// redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one.
// skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change)
// unk4 seemed to be the same as unk1.
[Signature(Penumbra.GameData.Sigs.WeaponReload, DetourName = nameof(LoadWeaponDetour))]
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook = null!;
private void LoadWeaponDetour(nint characterOffset, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject,
byte unk4)
{
//var oldWeapon = weapon;
//var character = (Actor)(characterOffset - CharacterWeaponOffset);
//try
//{
// var identifier = character.GetIdentifier(_actors.AwaitedService);
// if (_fixedDesignManager.TryGetDesign(identifier, out var save))
// {
// PluginLog.Information($"Loaded weapon from fixed design for {identifier}.");
// weapon = slot switch
// {
// 0 => save.WeaponMain.Model.Value,
// 1 => save.WeaponOff.Model.Value,
// _ => weapon,
// };
// }
// else if (redrawOnEquality == 1 && _stateManager.TryGetValue(identifier, out var save2))
// {
// PluginLog.Information($"Loaded weapon from current design for {identifier}.");
// //switch (slot)
// //{
// // case 0:
// // save2.MainHand = new CharacterWeapon(weapon);
// // break;
// // case 1:
// // save2.Data.OffHand = new CharacterWeapon(weapon);
// // break;
// //}
// }
//}
//catch (Exception e)
//{
// PluginLog.Error($"Error on loading new weapon:\n{e}");
//}
// First call the regular function.
_loadWeaponHook.Original(characterOffset, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4);
Glamourer.Log.Excessive($"Weapon reloaded for {(Actor)(characterOffset - CharacterWeaponOffset)} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
// // If something changed the weapon, call it again with the actual change, not forcing redraws and skipping applying it to the game object.
// if (oldWeapon != weapon)
// _loadWeaponHook.Original(characterOffset, slot, weapon, 0 /* redraw */, unk2, 1 /* skip */, unk4);
// // If we're not actively changing the offhand and the game object has no offhand, redraw an empty offhand to fix animation problems.
// else if (slot != 1 && character.OffHand.Value == 0)
// _loadWeaponHook.Original(characterOffset, 1, 0, 1 /* redraw */, unk2, 1 /* skip */, unk4);
}
// Load a specific weapon for a character by its data and slot.
public void LoadWeapon(Actor character, EquipSlot slot, CharacterWeapon weapon)
{
switch (slot)
{
case EquipSlot.MainHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0);
return;
case EquipSlot.OffHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, weapon.Value, 0, 0, 1, 0);
return;
case EquipSlot.BothHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0);
return;
// function can also be called with '2', but does not seem to ever be.
}
}
// Load specific Main- and Offhand weapons.
public void LoadWeapon(Actor character, CharacterWeapon main, CharacterWeapon off)
{
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, main.Value, 1, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 1, 0, 1, 0);
}
public void LoadStain(Actor character, EquipSlot slot, StainId stain)
{
var weapon = slot == EquipSlot.OffHand ? character.OffHand : character.MainHand;
weapon.Stain = stain;
LoadWeapon(character, slot, weapon);
}
}

Binary file not shown.

View file

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.IO;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer.Services;
public class BackupService
{
public BackupService(Logger logger, FilenameService fileNames)
{
var files = GlamourerFiles(fileNames);
Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files);
}
/// <summary> Collect all relevant files for glamourer configuration. </summary>
private static IReadOnlyList<FileInfo> GlamourerFiles(FilenameService fileNames)
{
var list = new List<FileInfo>(16)
{
new(fileNames.ConfigFile),
new(fileNames.DesignFileSystem),
new(fileNames.MigrationDesignFile),
};
list.AddRange(fileNames.Designs());
return list;
}
}

View file

@ -0,0 +1,36 @@
using System;
using Dalamud.Game.Command;
using Glamourer.Gui;
namespace Glamourer.Services;
public class CommandService : IDisposable
{
private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],<Name for Save>";
private const string MainCommandString = "/glamourer";
private const string ApplyCommandString = "/glamour";
private readonly CommandManager _commands;
private readonly Interface _interface;
public CommandService(CommandManager commands, Interface ui)
{
_commands = commands;
_interface = ui;
_commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." });
_commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour) { HelpMessage = $"Use Glamourer Functions: {HelpString}" });
}
public void Dispose()
{
_commands.RemoveHandler(MainCommandString);
_commands.RemoveHandler(ApplyCommandString);
}
private void OnGlamourer(string command, string arguments)
=> _interface.Toggle();
private void OnGlamour(string command, string arguments)
{ }
}

View file

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Plugin;
using Glamourer.Designs;
namespace Glamourer.Services;
public class FilenameService
{
public readonly string ConfigDirectory;
public readonly string ConfigFile;
public readonly string DesignFileSystem;
public readonly string MigrationDesignFile;
public readonly string DesignDirectory;
public FilenameService(DalamudPluginInterface pi)
{
ConfigDirectory = pi.ConfigDirectory.FullName;
ConfigFile = pi.ConfigFile.FullName;
DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json");
MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json");
DesignDirectory = Path.Combine(ConfigDirectory, "designs");
}
public IEnumerable<FileInfo> Designs()
{
if (!Directory.Exists(DesignDirectory))
yield break;
foreach (var file in Directory.EnumerateFiles(DesignDirectory, "*.json", SearchOption.TopDirectoryOnly))
yield return new FileInfo(file);
}
public string DesignFile(Design design)
=> DesignFile(design.Identifier.ToString());
public string DesignFile(string identifier)
=> Path.Combine(DesignDirectory, $"{identifier}.json");
}

View file

@ -0,0 +1,189 @@
using System;
using System.Diagnostics;
using System.Linq;
using Dalamud.Data;
using Dalamud.Plugin;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Lumina.Text;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Services;
public class ItemManager : IDisposable
{
public const string Nothing = "Nothing";
public const string SmallClothesNpc = "Smallclothes (NPC)";
public const ushort SmallClothesNpcModel = 9903;
private readonly Configuration _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)
{
_config = config;
ItemSheet = gameData.GetExcelSheet<Item>()!;
IdentifierService = identifierService;
Stains = new StainData(pi, gameData, gameData.Language);
ItemService = itemService;
RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData);
DefaultSword = ItemSheet.GetRow(1601)!; // Weathered Shortsword
}
public void Dispose()
{
Stains.Dispose();
RestrictedGear.Dispose();
}
public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender)
{
if (_config.UseRestrictedGearProtection)
return RestrictedGear.ResolveRestricted(armor, slot, race, gender);
return (false, armor);
}
public readonly Item DefaultSword;
public static uint NothingId(EquipSlot slot)
=> uint.MaxValue - 128 - (uint)slot.ToSlot();
public static uint SmallclothesId(EquipSlot slot)
=> uint.MaxValue - 256 - (uint)slot.ToSlot();
public static uint NothingId(FullEquipType type)
=> uint.MaxValue - 384 - (uint)type;
public static Designs.Item NothingItem(EquipSlot slot)
{
Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(NothingItem)} on {slot}.");
return new Designs.Item(Nothing, NothingId(slot), CharacterArmor.Empty);
}
public static Designs.Weapon NothingItem(FullEquipType type)
{
Debug.Assert(type.ToSlot() == EquipSlot.OffHand, $"Called {nameof(NothingItem)} on {type}.");
return new Designs.Weapon(Nothing, NothingId(type), CharacterWeapon.Empty, type);
}
public static Designs.Item SmallClothesItem(EquipSlot slot)
{
Debug.Assert(slot.IsEquipment(), $"Called {nameof(SmallClothesItem)} on {slot}.");
return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0));
}
public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Item? item = null)
{
slot = slot.ToSlot();
if (itemId == NothingId(slot))
return (true, 0, 0, Nothing);
if (itemId == SmallclothesId(slot))
return (true, SmallClothesNpcModel, 1, SmallClothesNpc);
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, string.Intern($"Unknown #{itemId}"));
if (item.ToEquipType().ToSlot() != slot)
return (false, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"));
return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue));
}
public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Item? item = null)
{
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown);
var type = item.ToEquipType();
if (type.ToSlot() != EquipSlot.MainHand)
return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type);
return (true, (SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32),
string.Intern(item.Name.ToDalamudString().TextValue), type);
}
public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId,
FullEquipType mainType, Item? item = null)
{
var offType = mainType.Offhand();
if (itemId == NothingId(offType))
return (true, 0, 0, 0, Nothing, offType);
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown);
var type = item.ToEquipType();
if (offType != type)
return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type);
var (m, w, v) = offType.ToSlot() == EquipSlot.MainHand
? ((SetId)item.ModelSub, (WeaponType)(item.ModelSub >> 16), (byte)(item.ModelSub >> 32))
: ((SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32));
return (true, m, w, v, string.Intern(item.Name.ToDalamudString().TextValue), type);
}
public (bool Valid, uint ItemId, string ItemName) Identify(EquipSlot slot, SetId id, byte variant)
{
slot = slot.ToSlot();
if (!slot.IsEquipmentPiece())
return (false, 0, string.Intern($"Unknown ({id.Value}-{variant})"));
switch (id.Value)
{
case 0: return (true, NothingId(slot), Nothing);
case SmallClothesNpcModel: return (true, SmallclothesId(slot), SmallClothesNpc);
default:
var item = IdentifierService.AwaitedService.Identify(id, variant, slot).FirstOrDefault();
return item == null
? (false, 0, string.Intern($"Unknown ({id.Value}-{variant})"))
: (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue));
}
}
public (bool Valid, uint ItemId, string ItemName, FullEquipType Type) Identify(EquipSlot slot, SetId id, WeaponType type, byte variant,
FullEquipType mainhandType = FullEquipType.Unknown)
{
switch (slot)
{
case EquipSlot.MainHand:
{
var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault();
return item != null
? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType())
: (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), mainhandType);
}
case EquipSlot.OffHand:
{
var weaponType = mainhandType.Offhand();
if (id.Value == 0)
return (true, NothingId(weaponType), Nothing, weaponType);
var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault();
return item != null
? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType())
: (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"),
weaponType);
}
default: return (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), FullEquipType.Unknown);
}
}
}

View file

@ -0,0 +1,97 @@
using System;
using System.IO;
using System.Text;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer.Services;
/// <summary>
/// Any file type that we want to save via SaveService.
/// </summary>
public interface ISavable
{
/// <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}");
}
}
}

View file

@ -0,0 +1,80 @@
using Dalamud.Plugin;
using Glamourer.Api;
using Glamourer.Designs;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.State;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer.Services;
public static class ServiceManager
{
public static ServiceProvider CreateProvider(DalamudPluginInterface pi, Logger log)
{
var services = new ServiceCollection()
.AddSingleton(log)
.AddDalamud(pi)
.AddMeta()
.AddConfig()
.AddPenumbra()
.AddInterop()
.AddGameData()
.AddDesigns()
.AddInterface()
.AddApi();
return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
}
private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pi)
{
new DalamudServices(pi).AddServices(services);
return services;
}
private static IServiceCollection AddMeta(this IServiceCollection services)
=> services.AddSingleton<FilenameService>()
.AddSingleton<SaveService>()
.AddSingleton<FrameworkManager>()
.AddSingleton<ChatService>();
private static IServiceCollection AddConfig(this IServiceCollection services)
=> services.AddSingleton<Configuration>()
.AddSingleton<BackupService>();
private static IServiceCollection AddPenumbra(this IServiceCollection services)
=> services.AddSingleton<PenumbraAttach>();
private static IServiceCollection AddGameData(this IServiceCollection services)
=> services.AddSingleton<IdentifierService>()
.AddSingleton<ActorService>()
.AddSingleton<ItemService>()
.AddSingleton<ItemManager>()
.AddSingleton<CustomizationService>();
private static IServiceCollection AddInterop(this IServiceCollection services)
=> services.AddSingleton<ChangeCustomizeService>()
.AddSingleton<JobService>()
.AddSingleton<UpdateSlotService>()
.AddSingleton<VisorService>()
.AddSingleton<WeaponService>()
.AddSingleton<ObjectManager>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()
.AddSingleton<DesignFileSystem>()
.AddSingleton<ActiveDesign.Manager>()
.AddSingleton<FixedDesignManager>()
.AddSingleton<RedrawManager>();
private static IServiceCollection AddInterface(this IServiceCollection services)
=> services.AddSingleton<Interface>()
.AddSingleton<GlamourerWindowSystem>();
private static IServiceCollection AddApi(this IServiceCollection services)
=> services.AddSingleton<CommandService>()
.AddSingleton<Glamourer.GlamourerIpc>();
}

View file

@ -0,0 +1,105 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui;
using Dalamud.Plugin;
using Penumbra.GameData.Actors;
using System;
using System.Threading.Tasks;
using Dalamud.Game;
using Glamourer.Api;
using Glamourer.Customization;
using Penumbra.GameData.Data;
using Penumbra.GameData;
namespace Glamourer.Services;
public abstract class AsyncServiceWrapper<T>
{
public string Name { get; }
public T? Service { get; private set; }
public T AwaitedService
{
get
{
_task?.Wait();
return Service!;
}
}
public bool Valid
=> Service != null && !_isDisposed;
public event Action? FinishedCreation;
private Task? _task;
private bool _isDisposed;
protected AsyncServiceWrapper(string name, Func<T> factory)
{
Name = name;
_task = Task.Run(() =>
{
var service = factory();
if (_isDisposed)
{
if (service is IDisposable d)
d.Dispose();
}
else
{
Service = service;
Glamourer.Log.Verbose($"[{Name}] Created.");
_task = null;
}
});
_task.ContinueWith((t, x) =>
{
if (!_isDisposed)
FinishedCreation?.Invoke();
}, null);
}
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
_task = null;
if (Service is IDisposable d)
d.Dispose();
Glamourer.Log.Verbose($"[{Name}] Disposed.");
}
}
public sealed class IdentifierService : AsyncServiceWrapper<IObjectIdentifier>
{
public IdentifierService(DalamudPluginInterface pi, DataManager data)
: base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data))
{ }
}
public sealed class ItemService : AsyncServiceWrapper<ItemData>
{
public ItemService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(ItemService), () => new ItemData(pi, gameData, gameData.Language))
{ }
}
public sealed class ActorService : AsyncServiceWrapper<ActorManager>
{
public ActorService(DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, Framework framework, DataManager gameData,
GameGui gui, PenumbraAttach penumbra)
: base(nameof(ActorService),
() => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx)))
{ }
}
public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationManager>
{
public CustomizationService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData))
{ }
}

View file

@ -0,0 +1,305 @@
using System;
using System.Collections;
using Glamourer.Interop;
using Penumbra.GameData.Actors;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Api;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.State;
public sealed partial class ActiveDesign
{
[Flags]
public enum ChangeType
{
Default = 0x00,
Changed = 0x01,
Fixed = 0x02,
}
public class Manager : IReadOnlyDictionary<ActorIdentifier, ActiveDesign>
{
private readonly ActorService _actors;
private readonly ObjectManager _objects;
private readonly PenumbraAttach _penumbra;
private readonly ItemManager _items;
private readonly VisorService _visor;
private readonly ChangeCustomizeService _customize;
private readonly UpdateSlotService _updateSlot;
private readonly WeaponService _weaponService;
private readonly Dictionary<ActorIdentifier, ActiveDesign> _characterSaves = new();
public Manager(ActorService actors, ObjectManager objects, PenumbraAttach penumbra, ItemManager items, VisorService visor,
ChangeCustomizeService customize, UpdateSlotService updateSlot, WeaponService weaponService)
{
_actors = actors;
_objects = objects;
_penumbra = penumbra;
_items = items;
_visor = visor;
_customize = customize;
_updateSlot = updateSlot;
_weaponService = weaponService;
}
public IEnumerator<KeyValuePair<ActorIdentifier, ActiveDesign>> GetEnumerator()
=> _characterSaves.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _characterSaves.Count;
public bool ContainsKey(ActorIdentifier key)
=> _characterSaves.ContainsKey(key);
public bool TryGetValue(ActorIdentifier key, [NotNullWhen(true)] out ActiveDesign? value)
=> _characterSaves.TryGetValue(key, out value);
public ActiveDesign this[ActorIdentifier key]
=> _characterSaves[key];
public IEnumerable<ActorIdentifier> Keys
=> _characterSaves.Keys;
public IEnumerable<ActiveDesign> Values
=> _characterSaves.Values;
public void DeleteSave(ActorIdentifier identifier)
=> _characterSaves.Remove(identifier);
public unsafe ActiveDesign GetOrCreateSave(Actor actor)
{
var id = _actors.AwaitedService.FromObject((GameObject*)actor.Pointer, out _, false, false, false);
if (_characterSaves.TryGetValue(id, out var save))
{
save.Initialize(_items, actor);
return save;
}
id = id.CreatePermanent();
save = new ActiveDesign(_items, id, actor);
save.Initialize(_items, actor);
_characterSaves.Add(id, save);
return save;
}
public void SetWetness(ActiveDesign design, bool wet, bool fromFixed)
=> design.IsWet = wet;
public void SetHatVisible(ActiveDesign design, bool visible, bool fromFixed)
=> design.IsHatVisible = visible;
public void SetVisor(ActiveDesign design, bool toggled, bool fromFixed)
=> design.IsVisorToggled = toggled;
public void SetWeaponVisible(ActiveDesign design, bool visible, bool fromFixed)
=> design.IsWeaponVisible = visible;
public unsafe void ApplyDesign(ActiveDesign to, Design from, bool fromFixed)
{
if (to.ModelId != from.ModelId)
return;
if (from.DoApplyEquip(EquipSlot.MainHand))
ChangeMainHand(to, from.MainHandId, fromFixed);
if (from.DoApplyStain(EquipSlot.MainHand))
ChangeStain(to, EquipSlot.MainHand, from.WeaponMain.Stain, fromFixed);
if (from.DoApplyEquip(EquipSlot.OffHand))
ChangeOffHand(to, from.OffHandId, fromFixed);
if (from.DoApplyStain(EquipSlot.OffHand))
ChangeStain(to, EquipSlot.OffHand, from.WeaponOff.Stain, fromFixed);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var armor = from.Armor(slot);
if (from.DoApplyEquip(slot))
ChangeEquipment(to, slot, armor, fromFixed);
if (from.DoApplyStain(slot))
ChangeStain(to, slot, armor.Stain, fromFixed);
}
ChangeCustomize(to, from.ApplyCustomize, from.ModelData.Customize.Data, fromFixed);
if (from.Wetness.Enabled)
SetWetness(to, from.Wetness.ForcedValue, fromFixed);
if (from.Hat.Enabled)
SetHatVisible(to, from.Hat.ForcedValue, fromFixed);
if (from.Visor.Enabled)
SetVisor(to, from.Visor.ForcedValue, fromFixed);
if (from.Weapon.Enabled)
SetWeaponVisible(to, from.Weapon.ForcedValue, fromFixed);
}
public void RevertDesign(ActiveDesign design)
{
RevertCustomize(design, design.ChangedCustomize);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
RevertEquipment(design, slot, design.ChangedEquip.HasFlag(slot.ToFlag()), design.ChangedEquip.HasFlag(slot.ToStainFlag()));
RevertMainHand(design);
RevertOffHand(design);
}
public void ChangeMainHand(ActiveDesign design, uint itemId, bool fromFixed)
=> design.SetMainhand(_items, itemId);
public void ChangeOffHand(ActiveDesign design, uint itemId, bool fromFixed)
=> design.SetOffhand(_items, itemId);
public void RevertMainHand(ActiveDesign design)
{ }
public void RevertOffHand(ActiveDesign design)
{ }
public void RevertCustomize(ActiveDesign design, CustomizeFlag flags)
=> ChangeCustomize(design, flags, design._initialData.Customize.Data, false);
public void ChangeCustomize(ActiveDesign design, CustomizeFlag flags, CustomizeData newValue, bool fromFixed)
{
var customize = new Customize(newValue);
var anyChanges = false;
foreach (var option in Enum.GetValues<CustomizeIndex>())
{
var flag = option.ToFlag();
var apply = flags.HasFlag(flag);
anyChanges |= apply && design.SetCustomize(option, customize[option]);
if (design.GetCustomize(option).Value != design._initialData.Customize[option].Value)
design.ChangedCustomize |= flag;
else
design.ChangedCustomize &= ~flag;
if (fromFixed)
design.FixedCustomize |= flag;
else
design.FixedCustomize &= ~flag;
}
if (!anyChanges)
return;
_objects.Update();
if (!_objects.TryGetValue(design.Identifier, out var data))
return;
var redraw = flags.RequiresRedraw();
foreach (var obj in data.Objects)
{
if (redraw)
_penumbra.RedrawObject(obj, RedrawType.Redraw);
else
_customize.UpdateCustomize(obj, design.ModelData.Customize.Data);
}
}
public void RevertEquipment(ActiveDesign design, EquipSlot slot, bool equip, bool stain)
{
var item = design._initialData.Armor(slot);
if (equip)
{
var flag = slot.ToFlag();
design.UpdateArmor(_items, slot, item, true);
design.ChangedEquip &= ~flag;
design.FixedEquip &= ~flag;
}
if (stain)
{
var flag = slot.ToStainFlag();
design.SetStain(slot, item.Stain);
design.ChangedEquip &= ~flag;
design.FixedEquip &= ~flag;
}
_objects.Update();
if (!_objects.TryGetValue(design.Identifier, out var data))
return;
foreach (var obj in data.Objects)
_updateSlot.UpdateSlot(obj.DrawObject, slot, item);
}
public void ChangeEquipment(ActiveDesign design, EquipSlot slot, Item item, bool fromFixed)
{
var flag = slot.ToFlag();
design.SetArmor(slot, item);
var current = design.Armor(slot);
var initial = design._initialData.Armor(slot);
if (current.ModelBase.Value != initial.Set.Value || current.Variant != initial.Variant)
design.ChangedEquip |= flag;
else
design.ChangedEquip &= ~flag;
if (fromFixed)
design.FixedEquip |= flag;
else
design.FixedEquip &= ~flag;
_objects.Update();
if (!_objects.TryGetValue(design.Identifier, out var data))
return;
foreach (var obj in data.Objects)
_updateSlot.UpdateSlot(obj.DrawObject, slot, item.Model);
}
public void ChangeStain(ActiveDesign design, EquipSlot slot, StainId stain, bool fromFixed)
{
var flag = slot.ToStainFlag();
design.SetStain(slot, stain);
var (current, initial, weapon) = slot switch
{
EquipSlot.MainHand => (design.WeaponMain.Stain, design._initialData.MainHand.Stain, true),
EquipSlot.OffHand => (design.WeaponOff.Stain, design._initialData.OffHand.Stain, true),
_ => (design.Armor(slot).Stain, design._initialData.Armor(slot).Stain, false),
};
if (current.Value != initial.Value)
design.ChangedEquip |= flag;
else
design.ChangedEquip &= ~flag;
if (fromFixed)
design.FixedEquip |= flag;
else
design.FixedEquip &= ~flag;
_objects.Update();
if (!_objects.TryGetValue(design.Identifier, out var data))
return;
foreach (var obj in data.Objects)
{
if (weapon)
_weaponService.LoadStain(obj, EquipSlot.MainHand, stain);
else
_updateSlot.UpdateStain(obj.DrawObject, slot, stain);
}
}
public void ChangeVisor(ActiveDesign design, bool on, bool fromFixed)
{
var current = design.IsVisorToggled;
if (current == on)
return;
design.IsVisorToggled = on;
_objects.Update();
if (!_objects.TryGetValue(design.Identifier, out var data))
return;
foreach (var obj in data.Objects)
_visor.SetVisorState(obj.DrawObject, on);
}
}
}

View file

@ -0,0 +1,117 @@
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Interop;
using Glamourer.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
namespace Glamourer.State;
public sealed partial class ActiveDesign : DesignData
{
public readonly ActorIdentifier Identifier;
private ModelData _initialData = new();
public CustomizeFlag ChangedCustomize { get; private set; } = 0;
public CustomizeFlag FixedCustomize { get; private set; } = 0;
public EquipFlag ChangedEquip { get; private set; } = 0;
public EquipFlag FixedEquip { get; private set; } = 0;
public bool IsHatVisible { get; private set; } = false;
public bool IsWeaponVisible { get; private set; } = false;
public bool IsVisorToggled { get; private set; } = false;
public bool IsWet { get; private set; } = false;
private ActiveDesign(ItemManager items, ActorIdentifier identifier)
: base(items)
=> Identifier = identifier;
public ActiveDesign(ItemManager items, ActorIdentifier identifier, Actor actor)
: base(items)
{
Identifier = identifier;
Initialize(items, actor);
}
//public void ApplyToActor(Actor actor)
//{
// if (!actor)
// return;
//
// void Redraw()
// => Glamourer.Penumbra.RedrawObject(actor.Character, RedrawType.Redraw);
//
// if (_drawData.ModelId != actor.ModelId)
// {
// Redraw();
// return;
// }
//
// var customize1 = _drawData.Customize;
// var customize2 = actor.Customize;
// if (RedrawManager.NeedsRedraw(customize1, customize2))
// {
// Redraw();
// return;
// }
//
// Glamourer.RedrawManager.UpdateCustomize(actor, customize2);
// foreach (var slot in EquipSlotExtensions.EqdpSlots)
// Glamourer.RedrawManager.ChangeEquip(actor, slot, actor.Equip[slot]);
// Glamourer.RedrawManager.LoadWeapon(actor, actor.MainHand, actor.OffHand);
// if (actor.IsHuman && actor.DrawObject)
// RedrawManager.SetVisor(actor.DrawObject.Pointer, actor.VisorEnabled);
//}
//
public void Initialize(ItemManager items, Actor actor)
{
if (!actor)
return;
if (!_initialData.Customize.Equals(actor.Customize))
{
_initialData.Customize.Load(actor.Customize);
ModelData.Customize.Load(actor.Customize);
}
var currentEquip = actor.Equip;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var current = currentEquip[slot];
if (_initialData.Armor(slot) != current)
{
_initialData.SetPiece(slot, current.Set, current.Variant, current.Stain, out _);
UpdateArmor(items, slot, current, true);
SetStain(slot, current.Stain);
}
}
if (_initialData.MainHand != actor.MainHand)
{
_initialData.MainHand = actor.MainHand;
UpdateMainhand(items, actor.MainHand);
SetStain(EquipSlot.MainHand, actor.MainHand.Stain);
}
if (_initialData.OffHand != actor.OffHand)
{
_initialData.OffHand = actor.OffHand;
UpdateOffhand(items, actor.OffHand);
SetStain(EquipSlot.OffHand, actor.OffHand.Stain);
}
var visor = VisorService.GetVisorState(actor.DrawObject);
if (IsVisorToggled != visor)
IsVisorToggled = visor;
}
public string CreateOldBase64()
=> DesignBase64Migration.CreateOldBase64(in ModelData, EquipFlagExtensions.All, CustomizeFlagExtensions.All, IsWet, IsHatVisible, true,
IsVisorToggled,
true, IsWeaponVisible, true, false, 1f);
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using Glamourer.Designs;
using Glamourer.Interop;
using Glamourer.Structs;
using Penumbra.GameData.Actors;
namespace Glamourer.State;
public class FixedDesignManager
{
public class FixedDesign
{
public Design Design = null!;
public byte? JobCondition;
public ushort? TerritoryCondition;
public bool Applies(byte job, ushort territoryType)
=> (!JobCondition.HasValue || JobCondition.Value == job)
&& (!TerritoryCondition.HasValue || TerritoryCondition.Value == territoryType);
}
public IReadOnlyList<FixedDesign> GetDesigns(ActorIdentifier actor)
{
return Array.Empty<FixedDesign>();
}
}

View file

@ -0,0 +1,93 @@
using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.Interop;
namespace Glamourer.Util;
public static class CharacterExtensions
{
public static unsafe bool IsWet(this Character a)
=> (*((byte*)a.Address + Offsets.Character.Wetness) & Offsets.Character.Flags.IsWet) != 0;
public static unsafe bool SetWetness(this Character a, bool value)
{
var current = a.IsWet();
if (current == value)
return false;
if (value)
*((byte*)a.Address + Offsets.Character.Wetness) =
(byte)(*((byte*)a.Address + Offsets.Character.Wetness) | Offsets.Character.Flags.IsWet);
else
*((byte*)a.Address + Offsets.Character.Wetness) =
(byte)(*((byte*)a.Address + Offsets.Character.Wetness) & ~Offsets.Character.Flags.IsWet);
return true;
}
public static unsafe bool IsHatVisible(this Character a)
=> (*((byte*)a.Address + Offsets.Character.HatVisible) & Offsets.Character.Flags.IsHatHidden) == 0;
public static unsafe bool SetHatVisible(this Character a, bool visible)
{
var current = a.IsHatVisible();
if (current == visible)
return false;
if (visible)
*((byte*)a.Address + Offsets.Character.HatVisible) =
(byte)(*((byte*)a.Address + Offsets.Character.HatVisible) & ~Offsets.Character.Flags.IsHatHidden);
else
*((byte*)a.Address + Offsets.Character.HatVisible) =
(byte)(*((byte*)a.Address + Offsets.Character.HatVisible) | Offsets.Character.Flags.IsHatHidden);
return true;
}
public static unsafe bool IsVisorToggled(this Character a)
=> (*((byte*)a.Address + Offsets.Character.VisorToggled) & Offsets.Character.Flags.IsVisorToggled)
== Offsets.Character.Flags.IsVisorToggled;
public static unsafe bool SetVisorToggled(this Character a, bool toggled)
{
var current = a.IsVisorToggled();
if (current == toggled)
return false;
if (toggled)
*((byte*)a.Address + Offsets.Character.VisorToggled) =
(byte)(*((byte*)a.Address + Offsets.Character.VisorToggled) | Offsets.Character.Flags.IsVisorToggled);
else
*((byte*)a.Address + Offsets.Character.VisorToggled) =
(byte)(*((byte*)a.Address + Offsets.Character.VisorToggled) & ~Offsets.Character.Flags.IsVisorToggled);
return true;
}
public static unsafe bool IsWeaponHidden(this Character a)
=> (*((byte*)a.Address + Offsets.Character.WeaponHidden1) & Offsets.Character.Flags.IsWeaponHidden1)
== Offsets.Character.Flags.IsWeaponHidden1
&& (*((byte*)a.Address + Offsets.Character.WeaponHidden2) & Offsets.Character.Flags.IsWeaponHidden2)
== Offsets.Character.Flags.IsWeaponHidden2;
public static unsafe bool SetWeaponHidden(this Character a, bool value)
{
var hidden = a.IsWeaponHidden();
if (hidden == value)
return false;
var val1 = *((byte*)a.Address + Offsets.Character.WeaponHidden1);
var val2 = *((byte*)a.Address + Offsets.Character.WeaponHidden2);
if (value)
{
*((byte*)a.Address + Offsets.Character.WeaponHidden1) = (byte)(val1 | Offsets.Character.Flags.IsWeaponHidden1);
*((byte*)a.Address + Offsets.Character.WeaponHidden2) = (byte)(val2 | Offsets.Character.Flags.IsWeaponHidden2);
}
else
{
*((byte*)a.Address + Offsets.Character.WeaponHidden1) = (byte)(val1 & ~Offsets.Character.Flags.IsWeaponHidden1);
*((byte*)a.Address + Offsets.Character.WeaponHidden2) = (byte)(val2 & ~Offsets.Character.Flags.IsWeaponHidden2);
}
return true;
}
public static unsafe ref float Alpha(this Character a)
=> ref *(float*)((byte*)a.Address + Offsets.Character.Alpha);
}

View file

@ -0,0 +1,127 @@
using System;
using Glamourer.Customization;
using Glamourer.Services;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Util;
public static class CustomizeExtensions
{
// In languages other than english the actual clan name may depend on gender.
public static string ClanName(ICustomizationManager customization, SubRace race, Gender gender)
{
if (gender == Gender.FemaleNpc)
gender = Gender.Female;
if (gender == Gender.MaleNpc)
gender = Gender.Male;
return (gender, race) switch
{
(Gender.Male, SubRace.Midlander) => customization.GetName(CustomName.MidlanderM),
(Gender.Male, SubRace.Highlander) => customization.GetName(CustomName.HighlanderM),
(Gender.Male, SubRace.Wildwood) => customization.GetName(CustomName.WildwoodM),
(Gender.Male, SubRace.Duskwight) => customization.GetName(CustomName.DuskwightM),
(Gender.Male, SubRace.Plainsfolk) => customization.GetName(CustomName.PlainsfolkM),
(Gender.Male, SubRace.Dunesfolk) => customization.GetName(CustomName.DunesfolkM),
(Gender.Male, SubRace.SeekerOfTheSun) => customization.GetName(CustomName.SeekerOfTheSunM),
(Gender.Male, SubRace.KeeperOfTheMoon) => customization.GetName(CustomName.KeeperOfTheMoonM),
(Gender.Male, SubRace.Seawolf) => customization.GetName(CustomName.SeawolfM),
(Gender.Male, SubRace.Hellsguard) => customization.GetName(CustomName.HellsguardM),
(Gender.Male, SubRace.Raen) => customization.GetName(CustomName.RaenM),
(Gender.Male, SubRace.Xaela) => customization.GetName(CustomName.XaelaM),
(Gender.Male, SubRace.Helion) => customization.GetName(CustomName.HelionM),
(Gender.Male, SubRace.Lost) => customization.GetName(CustomName.LostM),
(Gender.Male, SubRace.Rava) => customization.GetName(CustomName.RavaM),
(Gender.Male, SubRace.Veena) => customization.GetName(CustomName.VeenaM),
(Gender.Female, SubRace.Midlander) => customization.GetName(CustomName.MidlanderF),
(Gender.Female, SubRace.Highlander) => customization.GetName(CustomName.HighlanderF),
(Gender.Female, SubRace.Wildwood) => customization.GetName(CustomName.WildwoodF),
(Gender.Female, SubRace.Duskwight) => customization.GetName(CustomName.DuskwightF),
(Gender.Female, SubRace.Plainsfolk) => customization.GetName(CustomName.PlainsfolkF),
(Gender.Female, SubRace.Dunesfolk) => customization.GetName(CustomName.DunesfolkF),
(Gender.Female, SubRace.SeekerOfTheSun) => customization.GetName(CustomName.SeekerOfTheSunF),
(Gender.Female, SubRace.KeeperOfTheMoon) => customization.GetName(CustomName.KeeperOfTheMoonF),
(Gender.Female, SubRace.Seawolf) => customization.GetName(CustomName.SeawolfF),
(Gender.Female, SubRace.Hellsguard) => customization.GetName(CustomName.HellsguardF),
(Gender.Female, SubRace.Raen) => customization.GetName(CustomName.RaenF),
(Gender.Female, SubRace.Xaela) => customization.GetName(CustomName.XaelaF),
(Gender.Female, SubRace.Helion) => customization.GetName(CustomName.HelionM),
(Gender.Female, SubRace.Lost) => customization.GetName(CustomName.LostM),
(Gender.Female, SubRace.Rava) => customization.GetName(CustomName.RavaF),
(Gender.Female, SubRace.Veena) => customization.GetName(CustomName.VeenaF),
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
};
}
public static string ClanName(this Customize customize, ICustomizationManager customization)
=> ClanName(customization, customize.Clan, customize.Gender);
// Change a gender and fix up all required customizations afterwards.
public static CustomizeFlag ChangeGender(this Customize customize, CharacterEquip equip, Gender gender, ItemManager items, ICustomizationManager customization)
{
if (customize.Gender == gender)
return 0;
FixRestrictedGear(items, customize, equip, gender, customize.Race);
customize.Gender = gender;
return CustomizeFlag.Gender | FixUpAttributes(customization, customize);
}
// Change a race and fix up all required customizations afterwards.
public static CustomizeFlag ChangeRace(this Customize customize, CharacterEquip equip, SubRace clan, ItemManager items, ICustomizationManager customization)
{
if (customize.Clan == clan)
return 0;
var race = clan.ToRace();
var gender = race == Race.Hrothgar ? Gender.Male : customize.Gender; // TODO Female Hrothgar
FixRestrictedGear(items, customize, equip, gender, race);
var flags = CustomizeFlag.Race | CustomizeFlag.Clan;
if (gender != customize.Gender)
flags |= CustomizeFlag.Gender;
customize.Gender = gender;
customize.Race = race;
customize.Clan = clan;
return flags | FixUpAttributes(customization, customize);
}
// Go through a whole customization struct and fix up all settings that need fixing.
private static CustomizeFlag FixUpAttributes(ICustomizationManager customization, Customize customize)
{
var set = customization.GetList(customize.Clan, customize.Gender);
CustomizeFlag flags = 0;
foreach (CustomizeIndex id in Enum.GetValues(typeof(CustomizeIndex)))
{
switch (id)
{
case CustomizeIndex.Race: break;
case CustomizeIndex.Clan: break;
case CustomizeIndex.BodyType: break;
case CustomizeIndex.Gender: break;
case CustomizeIndex.Highlights: break;
case CustomizeIndex.Face: break;
default:
var count = set.Count(id);
if (set.DataByValue(id, customize[id], out _, customize.Face) < 0)
{
customize[id] = count == 0 ? CustomizeValue.Zero : set.Data(id, 0).Value;
flags |= id.ToFlag();
}
break;
}
}
return flags;
}
private static void FixRestrictedGear(ItemManager items, Customize customize, CharacterEquip equip, Gender gender, Race race)
{
if (!equip || race == customize.Race && gender == customize.Gender)
return;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
(_, equip[slot]) = items.ResolveRestrictedGear(equip[slot], slot, race, gender);
}
}