This commit is contained in:
Ottermandias 2023-05-12 16:20:52 +02:00
parent 85adc3626e
commit 10631341cb
31 changed files with 1883 additions and 1507 deletions

View file

@ -5,61 +5,53 @@ namespace Glamourer.Customization;
public unsafe struct Customize public unsafe struct Customize
{ {
public readonly Penumbra.GameData.Structs.CustomizeData* Data; public Penumbra.GameData.Structs.CustomizeData Data;
public Customize(Penumbra.GameData.Structs.CustomizeData* data) public Customize(in Penumbra.GameData.Structs.CustomizeData data)
=> Data = data; => Data = data;
public Customize(ref Penumbra.GameData.Structs.CustomizeData data)
{
fixed (Penumbra.GameData.Structs.CustomizeData* ptr = &data)
{
Data = ptr;
}
}
public Race Race public Race Race
{ {
get => (Race)Data->Get(CustomizeIndex.Race).Value; get => (Race)Data.Get(CustomizeIndex.Race).Value;
set => Data->Set(CustomizeIndex.Race, (CustomizeValue)(byte)value); set => Data.Set(CustomizeIndex.Race, (CustomizeValue)(byte)value);
} }
public Gender Gender public Gender Gender
{ {
get => (Gender)Data->Get(CustomizeIndex.Gender).Value + 1; get => (Gender)Data.Get(CustomizeIndex.Gender).Value + 1;
set => Data->Set(CustomizeIndex.Gender, (CustomizeValue)(byte)value - 1); set => Data.Set(CustomizeIndex.Gender, (CustomizeValue)(byte)value - 1);
} }
public CustomizeValue BodyType public CustomizeValue BodyType
{ {
get => Data->Get(CustomizeIndex.BodyType); get => Data.Get(CustomizeIndex.BodyType);
set => Data->Set(CustomizeIndex.BodyType, value); set => Data.Set(CustomizeIndex.BodyType, value);
} }
public SubRace Clan public SubRace Clan
{ {
get => (SubRace)Data->Get(CustomizeIndex.Clan).Value; get => (SubRace)Data.Get(CustomizeIndex.Clan).Value;
set => Data->Set(CustomizeIndex.Clan, (CustomizeValue)(byte)value); set => Data.Set(CustomizeIndex.Clan, (CustomizeValue)(byte)value);
} }
public CustomizeValue Face public CustomizeValue Face
{ {
get => Data->Get(CustomizeIndex.Face); get => Data.Get(CustomizeIndex.Face);
set => Data->Set(CustomizeIndex.Face, value); set => Data.Set(CustomizeIndex.Face, value);
} }
public static readonly Penumbra.GameData.Structs.CustomizeData Default = GenerateDefault(); public static readonly Customize Default = GenerateDefault();
public static readonly Penumbra.GameData.Structs.CustomizeData Empty = new(); public static readonly Customize Empty = new();
public CustomizeValue Get(CustomizeIndex index) public CustomizeValue Get(CustomizeIndex index)
=> Data->Get(index); => Data.Get(index);
public void Set(CustomizeIndex flag, CustomizeValue index) public bool Set(CustomizeIndex flag, CustomizeValue index)
=> Data->Set(flag, index); => Data.Set(flag, index);
public bool Equals(Customize other) public bool Equals(Customize other)
=> Penumbra.GameData.Structs.CustomizeData.Equals(Data, other.Data); => Equals(Data, other.Data);
public CustomizeValue this[CustomizeIndex index] public CustomizeValue this[CustomizeIndex index]
{ {
@ -67,9 +59,9 @@ public unsafe struct Customize
set => Set(index, value); set => Set(index, value);
} }
private static Penumbra.GameData.Structs.CustomizeData GenerateDefault() private static Customize GenerateDefault()
{ {
var ret = new Penumbra.GameData.Structs.CustomizeData(); var ret = new Customize();
ret.Set(CustomizeIndex.BodyType, (CustomizeValue)1); ret.Set(CustomizeIndex.BodyType, (CustomizeValue)1);
ret.Set(CustomizeIndex.Height, (CustomizeValue)50); ret.Set(CustomizeIndex.Height, (CustomizeValue)50);
ret.Set(CustomizeIndex.Face, (CustomizeValue)1); ret.Set(CustomizeIndex.Face, (CustomizeValue)1);
@ -94,16 +86,16 @@ public unsafe struct Customize
} }
public void Load(Customize other) public void Load(Customize other)
=> Data->Read(other.Data); => Data.Read(&other.Data);
public void Write(IntPtr target) public void Write(nint target)
=> Data->Write((void*)target); => Data.Write((void*)target);
public bool LoadBase64(string data) public bool LoadBase64(string data)
=> Data->LoadBase64(data); => Data.LoadBase64(data);
public string WriteBase64() public string WriteBase64()
=> Data->WriteBase64(); => Data.WriteBase64();
public static CustomizeFlag Compare(Customize lhs, Customize rhs) public static CustomizeFlag Compare(Customize lhs, Customize rhs)
{ {

View file

@ -5,7 +5,9 @@ using Dalamud.Logging;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using System; using System;
using Glamourer.Api;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Services;
using Glamourer.State; using Glamourer.State;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
@ -30,6 +32,10 @@ public partial class Glamourer
private readonly ObjectTable _objectTable; private readonly ObjectTable _objectTable;
private readonly DalamudPluginInterface _pluginInterface; 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<string, string?>? ProviderGetAllCustomization;
internal ICallGateProvider<Character?, string?>? ProviderGetAllCustomizationFromCharacter; internal ICallGateProvider<Character?, string?>? ProviderGetAllCustomizationFromCharacter;
@ -43,10 +49,15 @@ public partial class Glamourer
internal ICallGateProvider<Character?, object>? ProviderRevertCharacter; internal ICallGateProvider<Character?, object>? ProviderRevertCharacter;
internal ICallGateProvider<int>? ProviderGetApiVersion; internal ICallGateProvider<int>? ProviderGetApiVersion;
public GlamourerIpc(ObjectTable objectTable, DalamudPluginInterface pluginInterface) public GlamourerIpc(ObjectTable objectTable, DalamudPluginInterface pluginInterface, ActiveDesign.Manager stateManager,
ItemManager items, PenumbraAttach penumbra, ActorService actors)
{ {
_objectTable = objectTable; _objectTable = objectTable;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_stateManager = stateManager;
_items = items;
_penumbra = penumbra;
_actors = actors;
InitializeProviders(); InitializeProviders();
} }
@ -211,9 +222,9 @@ public partial class Glamourer
if (character == null) if (character == null)
return; return;
//var design = Design.CreateTemporaryFromBase64(customization, true, true); var design = Design.CreateTemporaryFromBase64(_items, customization, true, true);
//var active = _glamourer._stateManager.GetOrCreateSave(character.Address); var active = _stateManager.GetOrCreateSave(character.Address);
//_glamourer._stateManager.ApplyDesign(active, design, false); _stateManager.ApplyDesign(active, design, false);
} }
private void ApplyOnlyCustomization(string customization, string characterName) private void ApplyOnlyCustomization(string customization, string characterName)
@ -232,20 +243,21 @@ public partial class Glamourer
{ {
if (character == null) if (character == null)
return; return;
//var design = Design.CreateTemporaryFromBase64(customization, true, false);
//var active = _glamourer._stateManager.GetOrCreateSave(character.Address); var design = Design.CreateTemporaryFromBase64(_items, customization, true, false);
//_glamourer._stateManager.ApplyDesign(active, design, false); var active = _stateManager.GetOrCreateSave(character.Address);
_stateManager.ApplyDesign(active, design, false);
} }
private void ApplyOnlyEquipment(string customization, string characterName) private void ApplyOnlyEquipment(string customization, string characterName)
{ {
foreach (var gameObject in _objectTable) foreach (var gameObject in _objectTable)
{ {
if (gameObject.Name.ToString() == characterName) if (gameObject.Name.ToString() != characterName)
{ continue;
ApplyOnlyEquipment(customization, gameObject as Character);
return; ApplyOnlyEquipment(customization, gameObject as Character);
} return;
} }
} }
@ -253,9 +265,10 @@ public partial class Glamourer
{ {
if (character == null) if (character == null)
return; return;
//var design = Design.CreateTemporaryFromBase64(customization, false, true);
//var active = _glamourer._stateManager.GetOrCreateSave(character.Address); var design = Design.CreateTemporaryFromBase64(_items, customization, false, true);
//_glamourer._stateManager.ApplyDesign(active, design, false); var active = _stateManager.GetOrCreateSave(character.Address);
_stateManager.ApplyDesign(active, design, false);
} }
private void Revert(string characterName) private void Revert(string characterName)
@ -274,9 +287,9 @@ public partial class Glamourer
if (character == null) if (character == null)
return; return;
//var ident = Actors.FromObject(character, true, false, false); var ident = _actors.AwaitedService.FromObject(character, true, false, false);
//_glamourer._stateManager.DeleteSave(ident); _stateManager.DeleteSave(ident);
//_glamourer._penumbra.RedrawObject(character.Address, RedrawType.Redraw); _penumbra.RedrawObject(character.Address, RedrawType.Redraw);
} }
private string? GetAllCustomization(Character? character) private string? GetAllCustomization(Character? character)
@ -284,12 +297,11 @@ public partial class Glamourer
if (character == null) if (character == null)
return null; return null;
//var ident = Actors.FromObject(character, true, false, false); var ident = _actors.AwaitedService.FromObject(character, true, false, false);
//if (!_glamourer._stateManager.TryGetValue(ident, out var design)) if (!_stateManager.TryGetValue(ident, out var design))
// design = new ActiveDesign(ident, character.Address); design = new ActiveDesign(_items, ident, character.Address);
//
//return design.CreateOldBase64(); return design.CreateOldBase64();
return null;
} }
private string? GetAllCustomization(string characterName) private string? GetAllCustomization(string characterName)

View file

@ -9,11 +9,6 @@ using Penumbra.Api.Helpers;
namespace Glamourer.Api; namespace Glamourer.Api;
public class CommunicatorService
{
}
public unsafe class PenumbraAttach : IDisposable public unsafe class PenumbraAttach : IDisposable
{ {
public const int RequiredPenumbraBreakingVersion = 4; public const int RequiredPenumbraBreakingVersion = 4;

View file

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

View file

@ -9,421 +9,363 @@ using Glamourer.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Classes;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
namespace Glamourer.Designs; namespace Glamourer.Designs;
public partial class Design public class DesignManager
{ {
public partial class Manager 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
{ {
public const string DesignFolderName = "designs"; Created,
public readonly string DesignFolder; Deleted,
ReloadedAll,
Renamed,
ChangedDescription,
AddedTag,
RemovedTag,
ChangedTag,
Customize,
Equip,
Weapon,
Stain,
ApplyCustomize,
ApplyEquip,
Other,
}
private readonly ItemManager _items; public delegate void DesignChangeDelegate(DesignChangeType type, Design design, object? changeData = null);
private readonly SaveService _saveService;
private readonly List<Design> _designs = new();
public enum DesignChangeType public event DesignChangeDelegate? DesignChange;
{
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 IReadOnlyList<Design> Designs
=> _designs;
public event DesignChangeDelegate DesignChange; public DesignManager(DalamudPluginInterface pi, SaveService saveService, ItemManager items)
{
public IReadOnlyList<Design> Designs _saveService = saveService;
=> _designs; _items = items;
DesignFolder = SetDesignFolder(pi);
public Manager(DalamudPluginInterface pi, SaveService saveService, ItemManager items) LoadDesigns();
{ MigrateOldDesigns(pi, Path.Combine(new DirectoryInfo(DesignFolder).Parent!.FullName, "Designs.json"));
_saveService = saveService; }
_items = items;
DesignFolder = SetDesignFolder(pi);
DesignChange += OnChange;
LoadDesigns();
MigrateOldDesigns(pi, Path.Combine(new DirectoryInfo(DesignFolder).Parent!.FullName, "Designs.json"));
}
private void OnChange(DesignChangeType type, Design design, object? _)
{
switch (type)
{
case DesignChangeType.Created:
SaveDesignInternal(design);
return;
case DesignChangeType.Renamed:
case DesignChangeType.ChangedDescription:
case DesignChangeType.AddedTag:
case DesignChangeType.RemovedTag:
case DesignChangeType.ChangedTag:
case DesignChangeType.Customize:
case DesignChangeType.Equip:
case DesignChangeType.Weapon:
case DesignChangeType.Stain:
case DesignChangeType.ApplyCustomize:
case DesignChangeType.ApplyEquip:
case DesignChangeType.Other:
SaveDesign(design);
return;
}
}
private static string SetDesignFolder(DalamudPluginInterface pi)
{
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}");
}
private static string SetDesignFolder(DalamudPluginInterface pi)
{
var ret = Path.Combine(pi.GetPluginConfigDirectory(), DesignFolderName);
if (Directory.Exists(ret))
return ret; return ret;
try
{
Directory.CreateDirectory(ret);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not create design folder directory at {ret}:\n{ex}");
} }
private string CreateFileName(Design design) return ret;
=> Path.Combine(DesignFolder, $"{design.Identifier}.json"); }
public void SaveDesign(Design design) public void LoadDesigns()
=> _saveService.QueueSave(design); {
_designs.Clear();
private void SaveDesignInternal(Design design) List<(Design, string)> invalidNames = new();
var skipped = 0;
foreach (var file in new DirectoryInfo(DesignFolder).EnumerateFiles("*.json", SearchOption.TopDirectoryOnly))
{ {
var fileName = CreateFileName(design);
try try
{ {
var data = design.JsonSerialize().ToString(Formatting.Indented); var text = File.ReadAllText(file.FullName);
File.WriteAllText(fileName, data); var data = JObject.Parse(text);
Glamourer.Log.Debug($"Saved design {design.Identifier}."); 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) catch (Exception ex)
{ {
Glamourer.Log.Error($"Could not save design {design.Identifier} to file:\n{ex}"); Glamourer.Log.Error($"Could not load design, skipped:\n{ex}");
++skipped;
} }
} }
public void LoadDesigns() var failed = 0;
foreach (var (design, name) in invalidNames)
{ {
_designs.Clear(); try
List<(Design, string)> invalidNames = new();
var skipped = 0;
foreach (var file in new DirectoryInfo(DesignFolder).EnumerateFiles("*.json", SearchOption.TopDirectoryOnly))
{ {
try var correctName = _saveService.FileNames.DesignFile(design);
{ File.Move(name, correctName, false);
var text = File.ReadAllText(file.FullName); Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
var data = JObject.Parse(text);
var 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;
}
} }
catch (Exception ex)
var failed = 0;
foreach (var (design, name) in invalidNames)
{ {
try ++failed;
{ Glamourer.Log.Error($"Failed to move invalid design file from {Path.GetFileName(name)}:\n{ex}");
var correctName = CreateFileName(design);
File.Move(name, correctName, false);
Glamourer.Log.Information($"Moved invalid design file from {Path.GetFileName(name)} to {Path.GetFileName(correctName)}.");
}
catch (Exception ex)
{
++failed;
Glamourer.Log.Error($"Failed to move invalid design file from {Path.GetFileName(name)}:\n{ex}");
}
} }
}
if (invalidNames.Count > 0) 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( Glamourer.Log.Information(
$"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}"); $"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}");
DesignChange.Invoke(DesignChangeType.ReloadedAll, null!);
}
public Design Create(string name) Glamourer.Log.Information(
{ $"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
var design = new Design(_items) DesignChange?.Invoke(DesignChangeType.ReloadedAll, null!);
{ }
CreationDate = DateTimeOffset.UtcNow,
Identifier = CreateNewGuid(),
Index = _designs.Count,
Name = name,
};
_designs.Add(design);
Glamourer.Log.Debug($"Added new design {design.Identifier}.");
DesignChange.Invoke(DesignChangeType.Created, design);
return design;
}
public void Delete(Design design) public Design Create(string name)
{
var design = new Design(_items)
{ {
_designs.RemoveAt(design.Index); CreationDate = DateTimeOffset.UtcNow,
foreach (var d in _designs.Skip(design.Index + 1)) Identifier = CreateNewGuid(),
--d.Index; Index = _designs.Count,
var fileName = CreateFileName(design); Name = name,
try };
{ _designs.Add(design);
File.Delete(fileName); Glamourer.Log.Debug($"Added new design {design.Identifier}.");
Glamourer.Log.Debug($"Deleted design {design.Identifier}."); _saveService.ImmediateSave(design);
DesignChange.Invoke(DesignChangeType.Deleted, design); DesignChange?.Invoke(DesignChangeType.Created, design);
} return design;
catch (Exception ex) }
{
Glamourer.Log.Error($"Could not delete design file for {design.Identifier}:\n{ex}");
}
}
public void Rename(Design design, string newName) 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 oldName = design.Name.Text; var guid = Guid.NewGuid();
var oldFileName = CreateFileName(design); if (_designs.All(d => d.Identifier != guid))
if (File.Exists(oldFileName)) 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 try
{ {
File.Delete(oldFileName); 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) catch (Exception ex)
{ {
Glamourer.Log.Error($"Could not delete old design file for rename from {design.Identifier}:\n{ex}"); Glamourer.Log.Error($"Could not migrate design {name}:\n{ex}");
return; ++errors;
} }
design.Name = newName;
Glamourer.Log.Debug($"Renamed design {design.Identifier}.");
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}.");
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}.");
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}.");
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);
SaveDesign(design);
Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
DesignChange.Invoke(DesignChangeType.ChangedTag, design);
}
public void ChangeCustomize(Design design, CustomizeIndex idx, CustomizeValue value)
{
var old = design.GetCustomize(idx);
if (design.SetCustomize(idx, value))
{
Glamourer.Log.Debug($"Changed customize {idx} in design {design.Identifier} from {old.Value} to {value.Value}");
DesignChange.Invoke(DesignChangeType.Customize, design, idx);
}
}
public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
{
if (design.SetApplyCustomize(idx, value))
{
Glamourer.Log.Debug($"Set applying of customization {idx} to {value}.");
DesignChange.Invoke(DesignChangeType.ApplyCustomize, design, idx);
}
}
public void ChangeEquip(Design design, EquipSlot slot, uint itemId, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var old = design.Armor(slot);
if (design.SetArmor(_items, slot, itemId, item))
{
var n = design.Armor(slot);
Glamourer.Log.Debug(
$"Set {slot} equipment piece in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId}).");
DesignChange.Invoke(DesignChangeType.Equip, design, slot);
}
}
public void ChangeWeapon(Design design, uint itemId, EquipSlot offhand, Lumina.Excel.GeneratedSheets.Item? item = null)
{
var (old, change, n) = offhand == EquipSlot.OffHand
? (design.WeaponOff, design.SetOffhand(_items, itemId, item), design.WeaponOff)
: (design.WeaponMain, design.SetMainhand(_items, itemId, item), design.WeaponMain);
if (change)
{
Glamourer.Log.Debug(
$"Set {offhand} weapon in design {design.Identifier} from {old.Name} ({old.ItemId}) to {n.Name} ({n.ItemId}).");
DesignChange.Invoke(DesignChangeType.Weapon, design, offhand);
}
}
public void ChangeApplyEquip(Design design, EquipSlot slot, bool value)
{
if (design.SetApplyEquip(slot, value))
{
Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}.");
DesignChange.Invoke(DesignChangeType.ApplyEquip, design, slot);
}
}
public void ChangeStain(Design design, EquipSlot slot, StainId stain)
{
if (design.SetStain(slot, stain))
{
Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}.");
DesignChange.Invoke(DesignChangeType.Stain, design, slot);
}
}
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
{
if (design.SetApplyStain(slot, value))
{
Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}.");
DesignChange.Invoke(DesignChangeType.Stain, design, slot);
}
}
private Guid CreateNewGuid()
{
while (true)
{
var guid = Guid.NewGuid();
if (_designs.All(d => d.Identifier != guid))
return guid;
}
}
private bool Add(Design design, string? message)
{
if (_designs.Any(d => d == design || d.Identifier == design.Identifier))
return false;
design.Index = _designs.Count;
_designs.Add(design);
if (!message.IsNullOrEmpty())
Glamourer.Log.Debug(message);
DesignChange.Invoke(DesignChangeType.Created, design);
return true;
}
private void MigrateOldDesigns(DalamudPluginInterface pi, string filePath)
{
if (!File.Exists(filePath))
return;
var errors = 0;
var successes = 0;
try
{
var text = File.ReadAllText(filePath);
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(text) ?? new Dictionary<string, string>();
var migratedFileSystemPaths = new Dictionary<string, string>(dict.Count);
foreach (var (name, base64) in dict)
{
try
{
var actualName = Path.GetFileName(name);
var design = new Design(_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 DesignFileSystem.MigrateOldPaths(pi, migratedFileSystemPaths);
{ Glamourer.Log.Information($"Successfully migrated {successes} old designs. Failed to migrate {errors} designs.");
File.Move(filePath, Path.ChangeExtension(filePath, ".json.bak")); }
Glamourer.Log.Information($"Moved migrated design file {filePath} to backup file."); catch (Exception e)
} {
catch (Exception ex) Glamourer.Log.Error($"Could not migrate old design file {filePath}:\n{e}");
{ }
Glamourer.Log.Error($"Could not move migrated design file {filePath} to backup file:\n{ex}");
} 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

@ -11,24 +11,24 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Designs; namespace Glamourer.Designs;
public partial class Design : DesignBase, ISavable public partial class Design : DesignData, ISavable
{ {
public const int FileVersion = 1; public const int FileVersion = 1;
public Guid Identifier { get; private init; } public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; private init; } public DateTimeOffset CreationDate { get; internal init; }
public LowerString Name { get; private set; } = LowerString.Empty; public LowerString Name { get; internal set; } = LowerString.Empty;
public string Description { get; private set; } = string.Empty; public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; private set; } = Array.Empty<string>(); public string[] Tags { get; internal set; } = Array.Empty<string>();
public int Index { get; private set; } public int Index { get; internal set; }
public EquipFlag ApplyEquip { get; private set; } public EquipFlag ApplyEquip { get; internal set; }
public CustomizeFlag ApplyCustomize { get; private set; } public CustomizeFlag ApplyCustomize { get; internal set; }
public QuadBool Wetness { get; private set; } = QuadBool.NullFalse; public QuadBool Wetness { get; internal set; } = QuadBool.NullFalse;
public QuadBool Visor { get; private set; } = QuadBool.NullFalse; public QuadBool Visor { get; internal set; } = QuadBool.NullFalse;
public QuadBool Hat { get; private set; } = QuadBool.NullFalse; public QuadBool Hat { get; internal set; } = QuadBool.NullFalse;
public QuadBool Weapon { get; private set; } = QuadBool.NullFalse; public QuadBool Weapon { get; internal set; } = QuadBool.NullFalse;
public bool WriteProtected { get; private set; } public bool WriteProtected { get; internal set; }
public bool DoApplyEquip(EquipSlot slot) public bool DoApplyEquip(EquipSlot slot)
=> ApplyEquip.HasFlag(slot.ToFlag()); => ApplyEquip.HasFlag(slot.ToFlag());
@ -39,7 +39,7 @@ public partial class Design : DesignBase, ISavable
public bool DoApplyCustomize(CustomizeIndex idx) public bool DoApplyCustomize(CustomizeIndex idx)
=> ApplyCustomize.HasFlag(idx.ToFlag()); => ApplyCustomize.HasFlag(idx.ToFlag());
private bool SetApplyEquip(EquipSlot slot, bool value) internal bool SetApplyEquip(EquipSlot slot, bool value)
{ {
var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag(); var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag();
if (newValue == ApplyEquip) if (newValue == ApplyEquip)
@ -49,7 +49,7 @@ public partial class Design : DesignBase, ISavable
return true; return true;
} }
private bool SetApplyStain(EquipSlot slot, bool value) internal bool SetApplyStain(EquipSlot slot, bool value)
{ {
var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag(); var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag();
if (newValue == ApplyEquip) if (newValue == ApplyEquip)
@ -59,7 +59,7 @@ public partial class Design : DesignBase, ISavable
return true; return true;
} }
private bool SetApplyCustomize(CustomizeIndex idx, bool value) internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{ {
var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag(); var newValue = value ? ApplyCustomize | idx.ToFlag() : ApplyCustomize & ~idx.ToFlag();
if (newValue == ApplyCustomize) if (newValue == ApplyCustomize)
@ -70,7 +70,7 @@ public partial class Design : DesignBase, ISavable
} }
private Design(ItemManager items) internal Design(ItemManager items)
: base(items) : base(items)
{ } { }
@ -78,15 +78,15 @@ public partial class Design : DesignBase, ISavable
{ {
var ret = new JObject var ret = new JObject
{ {
[nameof(FileVersion)] = FileVersion, [nameof(FileVersion)] = FileVersion,
[nameof(Identifier)] = Identifier, [nameof(Identifier)] = Identifier,
[nameof(CreationDate)] = CreationDate, [nameof(CreationDate)] = CreationDate,
[nameof(Name)] = Name.Text, [nameof(Name)] = Name.Text,
[nameof(Description)] = Description, [nameof(Description)] = Description,
[nameof(Tags)] = JArray.FromObject(Tags), [nameof(Tags)] = JArray.FromObject(Tags),
[nameof(WriteProtected)] = WriteProtected, [nameof(WriteProtected)] = WriteProtected,
[nameof(CharacterData.Equipment)] = SerializeEquipment(), [nameof(ModelData.Equipment)] = SerializeEquipment(),
[nameof(CharacterData.Customize)] = SerializeCustomize(), [nameof(ModelData.Customize)] = SerializeCustomize(),
}; };
return ret; return ret;
} }
@ -104,9 +104,9 @@ public partial class Design : DesignBase, ISavable
var ret = new JObject() var ret = new JObject()
{ {
[nameof(MainHand)] = [nameof(MainHandId)] =
Serialize(MainHand, CharacterData.MainHand.Stain, DoApplyEquip(EquipSlot.MainHand), DoApplyStain(EquipSlot.MainHand)), Serialize(MainHandId, ModelData.MainHand.Stain, DoApplyEquip(EquipSlot.MainHand), DoApplyStain(EquipSlot.MainHand)),
[nameof(OffHand)] = Serialize(OffHand, CharacterData.OffHand.Stain, DoApplyEquip(EquipSlot.OffHand), [nameof(OffHandId)] = Serialize(OffHandId, ModelData.OffHand.Stain, DoApplyEquip(EquipSlot.OffHand),
DoApplyStain(EquipSlot.OffHand)), DoApplyStain(EquipSlot.OffHand)),
}; };
@ -129,7 +129,7 @@ public partial class Design : DesignBase, ISavable
{ {
[nameof(ModelId)] = ModelId, [nameof(ModelId)] = ModelId,
}; };
var customize = CharacterData.Customize; var customize = ModelData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>()) foreach (var idx in Enum.GetValues<CustomizeIndex>())
{ {
var data = customize[idx]; var data = customize[idx];
@ -154,7 +154,7 @@ public partial class Design : DesignBase, ISavable
}; };
} }
private static Design LoadDesignV1(ItemManager items, JObject json, out bool changes) internal static Design LoadDesignV1(ItemManager items, JObject json, out bool changes)
{ {
static string[] ParseTags(JObject json) static string[] ParseTags(JObject json)
{ {
@ -176,7 +176,7 @@ public partial class Design : DesignBase, ISavable
return design; return design;
} }
private static bool LoadEquip(ItemManager items, JToken? equip, Design design) internal static bool LoadEquip(ItemManager items, JToken? equip, Design design)
{ {
if (equip == null) if (equip == null)
return true; return true;
@ -241,12 +241,12 @@ public partial class Design : DesignBase, ISavable
return changes; return changes;
} }
private static bool LoadCustomize(JToken? json, Design design) internal static bool LoadCustomize(JToken? json, Design design)
{ {
if (json == null) if (json == null)
return true; return true;
var customize = design.CharacterData.Customize; var customize = design.ModelData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>()) foreach (var idx in Enum.GetValues<CustomizeIndex>())
{ {
var tok = json[idx.ToString()]; var tok = json[idx.ToString()];
@ -263,20 +263,21 @@ public partial class Design : DesignBase, ISavable
public void MigrateBase64(ItemManager items, string base64) public void MigrateBase64(ItemManager items, string base64)
{ {
var data = MigrateBase64(base64, out var applyEquip, out var applyCustomize, out var writeProtected, out var wet, out var hat, var data = DesignBase64Migration.MigrateBase64(base64, out var applyEquip, out var applyCustomize, out var writeProtected, out var wet,
out var visor, out var weapon); out var hat,
out var visor, out var weapon);
UpdateMainhand(items, data.MainHand); UpdateMainhand(items, data.MainHand);
UpdateOffhand(items, data.OffHand); UpdateOffhand(items, data.OffHand);
foreach (var slot in EquipSlotExtensions.EqdpSlots) foreach (var slot in EquipSlotExtensions.EqdpSlots)
UpdateArmor(items, slot, data.Equipment[slot], true); UpdateArmor(items, slot, data.Equipment[slot], true);
CharacterData.CustomizeData = data.CustomizeData; ModelData.CustomizeData = data.CustomizeData;
ApplyEquip = applyEquip; ApplyEquip = applyEquip;
ApplyCustomize = applyCustomize; ApplyCustomize = applyCustomize;
WriteProtected = writeProtected; WriteProtected = writeProtected;
Wetness = wet; Wetness = wet;
Hat = hat; Hat = hat;
Visor = visor; Visor = visor;
Weapon = weapon; Weapon = weapon;
} }
public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip) public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip)
@ -296,13 +297,14 @@ public partial class Design : DesignBase, ISavable
// Outdated. // Outdated.
public string CreateOldBase64() public string CreateOldBase64()
=> CreateOldBase64(in CharacterData, ApplyEquip, ApplyCustomize, Wetness == QuadBool.True, Hat.ForcedValue, Hat.Enabled, => DesignBase64Migration.CreateOldBase64(in ModelData, ApplyEquip, ApplyCustomize, Wetness == QuadBool.True, Hat.ForcedValue,
Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f); Hat.Enabled,
Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f);
public string ToFilename(FilenameService fileNames) public string ToFilename(FilenameService fileNames)
=> fileNames.DesignFile(this); => fileNames.DesignFile(this);
public void Save(StreamWriter writer) public void Save(StreamWriter writer)
{ {
using var j = new JsonTextWriter(writer) using var j = new JsonTextWriter(writer)
{ {

View file

@ -1,453 +0,0 @@
using System;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Util;
using OtterGui.Classes;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignBase
{
protected CharacterData CharacterData = CharacterData.Default;
public FullEquipType MainhandType { get; protected set; }
public uint Head { get; protected set; } = ItemManager.NothingId(EquipSlot.Head);
public uint Body { get; protected set; } = ItemManager.NothingId(EquipSlot.Body);
public uint Hands { get; protected set; } = ItemManager.NothingId(EquipSlot.Hands);
public uint Legs { get; protected set; } = ItemManager.NothingId(EquipSlot.Legs);
public uint Feet { get; protected set; } = ItemManager.NothingId(EquipSlot.Feet);
public uint Ears { get; protected set; } = ItemManager.NothingId(EquipSlot.Ears);
public uint Neck { get; protected set; } = ItemManager.NothingId(EquipSlot.Neck);
public uint Wrists { get; protected set; } = ItemManager.NothingId(EquipSlot.Wrists);
public uint RFinger { get; protected set; } = ItemManager.NothingId(EquipSlot.RFinger);
public uint LFinger { get; protected set; } = ItemManager.NothingId(EquipSlot.RFinger);
public uint MainHand { get; protected set; }
public uint OffHand { get; protected set; }
public string HeadName { get; protected set; } = ItemManager.Nothing;
public string BodyName { get; protected set; } = ItemManager.Nothing;
public string HandsName { get; protected set; } = ItemManager.Nothing;
public string LegsName { get; protected set; } = ItemManager.Nothing;
public string FeetName { get; protected set; } = ItemManager.Nothing;
public string EarsName { get; protected set; } = ItemManager.Nothing;
public string NeckName { get; protected set; } = ItemManager.Nothing;
public string WristsName { get; protected set; } = ItemManager.Nothing;
public string RFingerName { get; protected set; } = ItemManager.Nothing;
public string LFingerName { get; protected set; } = ItemManager.Nothing;
public string MainhandName { get; protected set; }
public string OffhandName { get; protected set; }
public Customize Customize()
=> CharacterData.Customize;
public CharacterEquip Equipment()
=> CharacterData.Equipment;
public DesignBase(ItemManager items)
{
MainHand = items.DefaultSword.RowId;
(_, CharacterData.MainHand.Set, CharacterData.MainHand.Type, CharacterData.MainHand.Variant, MainhandName, MainhandType) =
items.Resolve(MainHand, items.DefaultSword);
OffHand = ItemManager.NothingId(MainhandType.Offhand());
(_, CharacterData.OffHand.Set, CharacterData.OffHand.Type, CharacterData.OffHand.Variant, OffhandName, _) =
items.Resolve(OffHand, MainhandType);
}
public uint ModelId
=> CharacterData.ModelId;
public Item Armor(EquipSlot slot)
{
return slot switch
{
EquipSlot.Head => new Item(HeadName, Head, CharacterData.Head),
EquipSlot.Body => new Item(BodyName, Body, CharacterData.Body),
EquipSlot.Hands => new Item(HandsName, Hands, CharacterData.Hands),
EquipSlot.Legs => new Item(LegsName, Legs, CharacterData.Legs),
EquipSlot.Feet => new Item(FeetName, Feet, CharacterData.Feet),
EquipSlot.Ears => new Item(EarsName, Ears, CharacterData.Ears),
EquipSlot.Neck => new Item(NeckName, Neck, CharacterData.Neck),
EquipSlot.Wrists => new Item(WristsName, Wrists, CharacterData.Wrists),
EquipSlot.RFinger => new Item(RFingerName, RFinger, CharacterData.RFinger),
EquipSlot.LFinger => new Item(LFingerName, LFinger, CharacterData.LFinger),
_ => throw new Exception("Invalid equip slot for item."),
};
}
public Weapon WeaponMain
=> new(MainhandName, MainHand, CharacterData.MainHand, MainhandType);
public Weapon WeaponOff
=> Weapon.Offhand(OffhandName, OffHand, CharacterData.OffHand, MainhandType);
public CustomizeValue GetCustomize(CustomizeIndex idx)
=> Customize()[idx];
protected bool SetCustomize(CustomizeIndex idx, CustomizeValue value)
{
var c = Customize();
if (c[idx] == value)
return false;
c[idx] = value;
return true;
}
protected bool SetArmor(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);
}
protected bool SetArmor(EquipSlot slot, Item item)
=> SetArmor(slot, item.ModelBase, item.Variant, item.Name, item.ItemId);
protected bool UpdateArmor(ItemManager items, EquipSlot slot, CharacterArmor armor, bool force)
{
if (!force)
switch (slot)
{
case EquipSlot.Head when CharacterData.Head.Value == armor.Value: return false;
case EquipSlot.Body when CharacterData.Body.Value == armor.Value: return false;
case EquipSlot.Hands when CharacterData.Hands.Value == armor.Value: return false;
case EquipSlot.Legs when CharacterData.Legs.Value == armor.Value: return false;
case EquipSlot.Feet when CharacterData.Feet.Value == armor.Value: return false;
case EquipSlot.Ears when CharacterData.Ears.Value == armor.Value: return false;
case EquipSlot.Neck when CharacterData.Neck.Value == armor.Value: return false;
case EquipSlot.Wrists when CharacterData.Wrists.Value == armor.Value: return false;
case EquipSlot.RFinger when CharacterData.RFinger.Value == armor.Value: return false;
case EquipSlot.LFinger when CharacterData.LFinger.Value == armor.Value: return false;
}
var (valid, id, name) = items.Identify(slot, armor.Set, armor.Variant);
if (!valid)
return false;
return SetArmor(slot, armor.Set, armor.Variant, name, id);
}
protected bool SetMainhand(ItemManager items, uint mainId, Lumina.Excel.GeneratedSheets.Item? main = null)
{
if (mainId == MainHand)
return false;
var (valid, set, weapon, variant, name, type) = items.Resolve(mainId, main);
if (!valid)
return false;
var fixOffhand = type.Offhand() != MainhandType.Offhand();
MainHand = mainId;
MainhandName = name;
MainhandType = type;
CharacterData.MainHand.Set = set;
CharacterData.MainHand.Type = weapon;
CharacterData.MainHand.Variant = variant;
if (fixOffhand)
SetOffhand(items, ItemManager.NothingId(type.Offhand()));
return true;
}
protected bool SetOffhand(ItemManager items, uint offId, Lumina.Excel.GeneratedSheets.Item? off = null)
{
if (offId == OffHand)
return false;
var (valid, set, weapon, variant, name, type) = items.Resolve(offId, MainhandType, off);
if (!valid)
return false;
OffHand = offId;
OffhandName = name;
CharacterData.OffHand.Set = set;
CharacterData.OffHand.Type = weapon;
CharacterData.OffHand.Variant = variant;
return true;
}
protected bool UpdateMainhand(ItemManager items, CharacterWeapon weapon)
{
if (weapon.Value == CharacterData.MainHand.Value)
return false;
var (valid, id, name, type) = items.Identify(EquipSlot.MainHand, weapon.Set, weapon.Type, (byte)weapon.Variant);
if (!valid || id == MainHand)
return false;
var fixOffhand = type.Offhand() != MainhandType.Offhand();
MainHand = id;
MainhandName = name;
MainhandType = type;
CharacterData.MainHand.Set = weapon.Set;
CharacterData.MainHand.Type = weapon.Type;
CharacterData.MainHand.Variant = weapon.Variant;
CharacterData.MainHand.Stain = weapon.Stain;
if (fixOffhand)
SetOffhand(items, ItemManager.NothingId(type.Offhand()));
return true;
}
protected bool UpdateOffhand(ItemManager items, CharacterWeapon weapon)
{
if (weapon.Value == CharacterData.OffHand.Value)
return false;
var (valid, id, name, _) = items.Identify(EquipSlot.OffHand, weapon.Set, weapon.Type, (byte)weapon.Variant, MainhandType);
if (!valid || id == OffHand)
return false;
OffHand = id;
OffhandName = name;
CharacterData.OffHand.Set = weapon.Set;
CharacterData.OffHand.Type = weapon.Type;
CharacterData.OffHand.Variant = weapon.Variant;
CharacterData.OffHand.Stain = weapon.Stain;
return true;
}
protected bool SetStain(EquipSlot slot, StainId id)
{
return slot switch
{
EquipSlot.MainHand => SetIfDifferent(ref CharacterData.MainHand.Stain, id),
EquipSlot.OffHand => SetIfDifferent(ref CharacterData.OffHand.Stain, id),
EquipSlot.Head => SetIfDifferent(ref CharacterData.Head.Stain, id),
EquipSlot.Body => SetIfDifferent(ref CharacterData.Body.Stain, id),
EquipSlot.Hands => SetIfDifferent(ref CharacterData.Hands.Stain, id),
EquipSlot.Legs => SetIfDifferent(ref CharacterData.Legs.Stain, id),
EquipSlot.Feet => SetIfDifferent(ref CharacterData.Feet.Stain, id),
EquipSlot.Ears => SetIfDifferent(ref CharacterData.Ears.Stain, id),
EquipSlot.Neck => SetIfDifferent(ref CharacterData.Neck.Stain, id),
EquipSlot.Wrists => SetIfDifferent(ref CharacterData.Wrists.Stain, id),
EquipSlot.RFinger => SetIfDifferent(ref CharacterData.RFinger.Stain, id),
EquipSlot.LFinger => SetIfDifferent(ref CharacterData.LFinger.Stain, id),
_ => false,
};
}
protected static bool SetIfDifferent<T>(ref T old, T value) where T : IEquatable<T>
{
if (old.Equals(value))
return false;
old = value;
return true;
}
private bool SetArmor(EquipSlot slot, SetId set, byte variant, string name, uint id)
{
var changes = false;
switch (slot)
{
case EquipSlot.Head:
changes |= SetIfDifferent(ref CharacterData.Head.Set, set);
changes |= SetIfDifferent(ref CharacterData.Head.Variant, variant);
changes |= HeadName != name;
HeadName = name;
changes |= Head != id;
Head = id;
return changes;
case EquipSlot.Body:
changes |= SetIfDifferent(ref CharacterData.Body.Set, set);
changes |= SetIfDifferent(ref CharacterData.Body.Variant, variant);
changes |= BodyName != name;
BodyName = name;
changes |= Body != id;
Body = id;
return changes;
case EquipSlot.Hands:
changes |= SetIfDifferent(ref CharacterData.Hands.Set, set);
changes |= SetIfDifferent(ref CharacterData.Hands.Variant, variant);
changes |= HandsName != name;
HandsName = name;
changes |= Hands != id;
Hands = id;
return changes;
case EquipSlot.Legs:
changes |= SetIfDifferent(ref CharacterData.Legs.Set, set);
changes |= SetIfDifferent(ref CharacterData.Legs.Variant, variant);
changes |= LegsName != name;
LegsName = name;
changes |= Legs != id;
Legs = id;
return changes;
case EquipSlot.Feet:
changes |= SetIfDifferent(ref CharacterData.Feet.Set, set);
changes |= SetIfDifferent(ref CharacterData.Feet.Variant, variant);
changes |= FeetName != name;
FeetName = name;
changes |= Feet != id;
Feet = id;
return changes;
case EquipSlot.Ears:
changes |= SetIfDifferent(ref CharacterData.Ears.Set, set);
changes |= SetIfDifferent(ref CharacterData.Ears.Variant, variant);
changes |= EarsName != name;
EarsName = name;
changes |= Ears != id;
Ears = id;
return changes;
case EquipSlot.Neck:
changes |= SetIfDifferent(ref CharacterData.Neck.Set, set);
changes |= SetIfDifferent(ref CharacterData.Neck.Variant, variant);
changes |= NeckName != name;
NeckName = name;
changes |= Neck != id;
Neck = id;
return changes;
case EquipSlot.Wrists:
changes |= SetIfDifferent(ref CharacterData.Wrists.Set, set);
changes |= SetIfDifferent(ref CharacterData.Wrists.Variant, variant);
changes |= WristsName != name;
WristsName = name;
changes |= Wrists != id;
Wrists = id;
return changes;
case EquipSlot.RFinger:
changes |= SetIfDifferent(ref CharacterData.RFinger.Set, set);
changes |= SetIfDifferent(ref CharacterData.RFinger.Variant, variant);
changes |= RFingerName != name;
RFingerName = name;
changes |= RFinger != id;
RFinger = id;
return changes;
case EquipSlot.LFinger:
changes |= SetIfDifferent(ref CharacterData.LFinger.Set, set);
changes |= SetIfDifferent(ref CharacterData.LFinger.Variant, variant);
changes |= LFingerName != name;
LFingerName = name;
changes |= LFinger != id;
LFinger = id;
return changes;
default: return false;
}
}
protected const int Base64Size = 89;
public static CharacterData MigrateBase64(string base64, out EquipFlag equipFlags, out CustomizeFlag customizeFlags,
out bool writeProtected, 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);
writeProtected = (applicationFlags & 0x20) != 0;
equipFlags = 0;
equipFlags |= (equipFlagsS & 0x0001) != 0 ? EquipFlag.Mainhand | EquipFlag.MainhandStain : 0;
equipFlags |= (equipFlagsS & 0x0002) != 0 ? EquipFlag.Offhand | EquipFlag.OffhandStain : 0;
var flag = 0x0002u;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
flag <<= 1;
equipFlags |= (equipFlagsS & flag) != 0 ? slot.ToFlag() | slot.ToStainFlag() : 0;
}
var data = new CharacterData();
unsafe
{
fixed (byte* ptr = bytes)
{
data.CustomizeData.Read(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())
data.Equipment[slot] = eq[idx];
}
}
return data;
}
public static unsafe string CreateOldBase64(in CharacterData save, EquipFlag equipFlags, CustomizeFlag customizeFlags, bool wet, bool hat,
bool setHat, bool visor, bool setVisor, bool weapon, bool setWeapon, bool writeProtected, 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)
| (writeProtected ? 0x20 : 0));
data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0)
| (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0)
| (equipFlags.HasFlag(EquipFlag.Head) ? 0x04 : 0)
| (equipFlags.HasFlag(EquipFlag.Body) ? 0x08 : 0)
| (equipFlags.HasFlag(EquipFlag.Hands) ? 0x10 : 0)
| (equipFlags.HasFlag(EquipFlag.Legs) ? 0x20 : 0)
| (equipFlags.HasFlag(EquipFlag.Feet) ? 0x40 : 0)
| (equipFlags.HasFlag(EquipFlag.Ears) ? 0x80 : 0));
data[3] = (byte)((equipFlags.HasFlag(EquipFlag.Neck) ? 0x01 : 0)
| (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0)
| (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0)
| (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0));
save.CustomizeData.Write(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,432 @@
using System;
using System.Runtime.InteropServices;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Services;
using OtterGui.Classes;
using OtterGui;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
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.CustomizeData.Read(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())
data.Equipment[slot] = eq[idx];
}
}
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.CustomizeData.Write(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

@ -8,7 +8,6 @@ using Dalamud.Plugin;
using Glamourer.Services; using Glamourer.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using OtterGui.Filesystem; using OtterGui.Filesystem;
namespace Glamourer.Designs; namespace Glamourer.Designs;
@ -20,9 +19,9 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
public readonly string DesignFileSystemFile; public readonly string DesignFileSystemFile;
private readonly SaveService _saveService; private readonly SaveService _saveService;
private readonly Design.Manager _designManager; private readonly DesignManager _designManager;
public DesignFileSystem(Design.Manager designManager, DalamudPluginInterface pi, SaveService saveService) public DesignFileSystem(DesignManager designManager, DalamudPluginInterface pi, SaveService saveService)
{ {
DesignFileSystemFile = GetDesignFileSystemFile(pi); DesignFileSystemFile = GetDesignFileSystemFile(pi);
_designManager = designManager; _designManager = designManager;
@ -75,11 +74,11 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
_saveService.QueueSave(this); _saveService.QueueSave(this);
} }
private void OnDataChange(Design.Manager.DesignChangeType type, Design design, object? data) private void OnDataChange(DesignManager.DesignChangeType type, Design design, object? data)
{ {
switch (type) switch (type)
{ {
case Design.Manager.DesignChangeType.Created: case DesignManager.DesignChangeType.Created:
var originalName = design.Name.Text.FixName(); var originalName = design.Name.Text.FixName();
var name = originalName; var name = originalName;
var counter = 1; var counter = 1;
@ -88,14 +87,14 @@ public sealed class DesignFileSystem : FileSystem<Design>, IDisposable, ISavable
CreateLeaf(Root, name, design); CreateLeaf(Root, name, design);
break; break;
case Design.Manager.DesignChangeType.Deleted: case DesignManager.DesignChangeType.Deleted:
if (FindLeaf(design, out var leaf)) if (FindLeaf(design, out var leaf))
Delete(leaf); Delete(leaf);
break; break;
case Design.Manager.DesignChangeType.ReloadedAll: case DesignManager.DesignChangeType.ReloadedAll:
Reload(); Reload();
break; break;
case Design.Manager.DesignChangeType.Renamed when data is string oldName: case DesignManager.DesignChangeType.Renamed when data is string oldName:
var old = oldName.FixName(); var old = oldName.FixName();
if (Find(old, out var child) && child is not Folder) if (Find(old, out var child) && child is not Folder)
Rename(child, design.Name); Rename(child, design.Name);

View file

@ -0,0 +1,489 @@
using System;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.String.Functions;
namespace Glamourer.Designs;
[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 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 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

@ -16,19 +16,17 @@ public class DrawObjectManager : IDisposable
private readonly ItemManager _items; private readonly ItemManager _items;
private readonly ActorService _actors; private readonly ActorService _actors;
private readonly ActiveDesign.Manager _manager; private readonly ActiveDesign.Manager _manager;
private readonly Interop.Interop _interop;
private readonly PenumbraAttach _penumbra; private readonly PenumbraAttach _penumbra;
public DrawObjectManager(ItemManager items, ActorService actors, ActiveDesign.Manager manager, Interop.Interop interop, public DrawObjectManager(ItemManager items, ActorService actors, ActiveDesign.Manager manager,
PenumbraAttach penumbra) PenumbraAttach penumbra)
{ {
_items = items; _items = items;
_actors = actors; _actors = actors;
_manager = manager; _manager = manager;
_interop = interop;
_penumbra = penumbra; _penumbra = penumbra;
_interop.EquipUpdate += FixEquipment; //_interop.EquipUpdate += FixEquipment;
_penumbra.CreatingCharacterBase += ApplyActiveDesign; _penumbra.CreatingCharacterBase += ApplyActiveDesign;
} }
@ -62,7 +60,7 @@ public class DrawObjectManager : IDisposable
var gameObjectCustomize = gameObject.Customize; var gameObjectCustomize = gameObject.Customize;
var customize = new Customize((CustomizeData*)customizePtr); var customize = new Customize((CustomizeData*)customizePtr);
if (gameObjectCustomize.Equals(customize)) if (gameObjectCustomize.Equals(customize))
customize.Load(design.Customize()); customize.Load(design.Customize);
// Compare game object equip data against draw object equip data for transformations. // Compare game object equip data against draw object equip data for transformations.
// Apply each piece of equip that should be applied if they correspond. // Apply each piece of equip that should be applied if they correspond.
@ -70,7 +68,7 @@ public class DrawObjectManager : IDisposable
var equip = new CharacterEquip((CharacterArmor*)equipDataPtr); var equip = new CharacterEquip((CharacterArmor*)equipDataPtr);
if (gameObjectEquip.Equals(equip)) if (gameObjectEquip.Equals(equip))
{ {
var saveEquip = design.Equipment(); var saveEquip = design.Equipment;
foreach (var slot in EquipSlotExtensions.EquipmentSlots) foreach (var slot in EquipSlotExtensions.EquipmentSlots)
{ {
(_, equip[slot]) = (_, equip[slot]) =
@ -82,6 +80,6 @@ public class DrawObjectManager : IDisposable
public void Dispose() public void Dispose()
{ {
_penumbra.CreatingCharacterBase -= ApplyActiveDesign; _penumbra.CreatingCharacterBase -= ApplyActiveDesign;
_interop.EquipUpdate -= FixEquipment; //_interop.EquipUpdate -= FixEquipment;
} }
} }

View file

@ -1,6 +1,7 @@
using System.Reflection; using System.Reflection;
using Dalamud.Plugin; using Dalamud.Plugin;
using Glamourer.Gui; using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.Services; using Glamourer.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes; using OtterGui.Classes;
@ -33,6 +34,12 @@ public partial class Glamourer : IDalamudPlugin
_services.GetRequiredService<GlamourerWindowSystem>(); _services.GetRequiredService<GlamourerWindowSystem>();
_services.GetRequiredService<CommandService>(); _services.GetRequiredService<CommandService>();
_services.GetRequiredService<GlamourerIpc>(); _services.GetRequiredService<GlamourerIpc>();
_services.GetRequiredService<ChangeCustomizeService>();
_services.GetRequiredService<JobService>();
_services.GetRequiredService<UpdateSlotService>();
_services.GetRequiredService<VisorService>();
_services.GetRequiredService<WeaponService>();
_services.GetRequiredService<RedrawManager>();
} }
catch catch
{ {

View file

@ -9,34 +9,34 @@ namespace Glamourer.Gui.Designs;
public sealed class DesignFileSystemSelector : FileSystemSelector<Design, DesignFileSystemSelector.DesignState> public sealed class DesignFileSystemSelector : FileSystemSelector<Design, DesignFileSystemSelector.DesignState>
{ {
private readonly Design.Manager _manager; private readonly DesignManager _designManager;
public struct DesignState public struct DesignState
{ } { }
public DesignFileSystemSelector(Design.Manager manager, DesignFileSystem fileSystem, KeyState keyState) public DesignFileSystemSelector(DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState)
: base(fileSystem, keyState) : base(fileSystem, keyState)
{ {
_manager = manager; _designManager = designManager;
_manager.DesignChange += OnDesignChange; _designManager.DesignChange += OnDesignChange;
AddButton(DeleteButton, 1000); AddButton(DeleteButton, 1000);
} }
public override void Dispose() public override void Dispose()
{ {
base.Dispose(); base.Dispose();
_manager.DesignChange -= OnDesignChange; _designManager.DesignChange -= OnDesignChange;
} }
private void OnDesignChange(Design.Manager.DesignChangeType type, Design design, object? oldData) private void OnDesignChange(DesignManager.DesignChangeType type, Design design, object? oldData)
{ {
switch (type) switch (type)
{ {
case Design.Manager.DesignChangeType.ReloadedAll: case DesignManager.DesignChangeType.ReloadedAll:
case Design.Manager.DesignChangeType.Renamed: case DesignManager.DesignChangeType.Renamed:
case Design.Manager.DesignChangeType.AddedTag: case DesignManager.DesignChangeType.AddedTag:
case Design.Manager.DesignChangeType.ChangedTag: case DesignManager.DesignChangeType.ChangedTag:
case Design.Manager.DesignChangeType.RemovedTag: case DesignManager.DesignChangeType.RemovedTag:
SetFilterDirty(); SetFilterDirty();
break; break;
} }
@ -54,6 +54,6 @@ public sealed class DesignFileSystemSelector : FileSystemSelector<Design, Design
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true)
&& Selected != null) && Selected != null)
_manager.Delete(Selected); _designManager.Delete(Selected);
} }
} }

View file

@ -40,7 +40,6 @@ public partial class Interface
private ActorIdentifier _identifier = ActorIdentifier.Invalid; private ActorIdentifier _identifier = ActorIdentifier.Invalid;
private ActorData _currentData = ActorData.Invalid; private ActorData _currentData = ActorData.Invalid;
private string _currentLabel = string.Empty;
private ActiveDesign? _currentSave; private ActiveDesign? _currentSave;
public void Draw() public void Draw()
@ -52,8 +51,6 @@ public partial class Interface
DrawActorSelector(); DrawActorSelector();
if (!_objects.TryGetValue(_identifier, out _currentData)) if (!_objects.TryGetValue(_identifier, out _currentData))
_currentData = ActorData.Invalid; _currentData = ActorData.Invalid;
else
_currentLabel = _currentData.Label;
ImGui.SameLine(); ImGui.SameLine();
@ -65,7 +62,6 @@ public partial class Interface
if (_identifier == ActorIdentifier.Invalid) if (_identifier == ActorIdentifier.Invalid)
return; return;
using var group = ImRaii.Group(); using var group = ImRaii.Group();
DrawPanelHeader(); DrawPanelHeader();
using var child = ImRaii.Child("##ActorPanel", -Vector2.One, true); using var child = ImRaii.Child("##ActorPanel", -Vector2.One, true);
@ -76,7 +72,7 @@ public partial class Interface
_currentSave.Initialize(_items, _currentData.Objects[0]); _currentSave.Initialize(_items, _currentData.Objects[0]);
RevertButton(); RevertButton();
if (_main._customizationDrawer.Draw(_currentSave.Customize(), _identifier.Type == IdentifierType.Special)) if (_main._customizationDrawer.Draw(_currentSave.Customize, _identifier.Type == IdentifierType.Special))
_activeDesigns.ChangeCustomize(_currentSave, _main._customizationDrawer.Changed, _main._customizationDrawer.CustomizeData, _activeDesigns.ChangeCustomize(_currentSave, _main._customizationDrawer.Changed, _main._customizationDrawer.CustomizeData,
false); false);
@ -86,8 +82,8 @@ public partial class Interface
if (_main._equipmentDrawer.DrawStain(current.Stain, slot, out var stain)) if (_main._equipmentDrawer.DrawStain(current.Stain, slot, out var stain))
_activeDesigns.ChangeStain(_currentSave, slot, stain.RowIndex, false); _activeDesigns.ChangeStain(_currentSave, slot, stain.RowIndex, false);
ImGui.SameLine(); ImGui.SameLine();
if (_main._equipmentDrawer.DrawArmor(current, slot, out var armor, _currentSave.Customize().Gender, if (_main._equipmentDrawer.DrawArmor(current, slot, out var armor, _currentSave.Customize.Gender,
_currentSave.Customize().Race)) _currentSave.Customize.Race))
_activeDesigns.ChangeEquipment(_currentSave, slot, armor, false); _activeDesigns.ChangeEquipment(_currentSave, slot, armor, false);
} }
@ -166,7 +162,7 @@ public partial class Interface
.Push(ImGuiCol.ButtonActive, buttonColor); .Push(ImGuiCol.ButtonActive, buttonColor);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0); .Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.Button($"{_currentLabel}##playerHeader", -Vector2.UnitX); ImGui.Button($"{_currentData.Label}##playerHeader", -Vector2.UnitX);
} }
//private void DrawActorPanel() //private void DrawActorPanel()
@ -244,10 +240,18 @@ public partial class Interface
return; return;
_objects.Update(); _objects.Update();
if (!_activeDesigns.TryGetValue(_identifier, out _currentSave))
_currentSave = null;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldSpacing); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight()); var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(_objects, skips, CheckFilter, DrawSelectable); var remainder = ImGuiClip.FilteredClippedDraw(_objects, skips, CheckFilter, DrawSelectable);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight()); ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
if (_currentSave == null)
{
_identifier = ActorIdentifier.Invalid;
_currentData = ActorData.Invalid;
}
} }
private bool CheckFilter(KeyValuePair<ActorIdentifier, ActorData> pair) private bool CheckFilter(KeyValuePair<ActorIdentifier, ActorData> pair)
@ -256,11 +260,12 @@ public partial class Interface
private void DrawSelectable(KeyValuePair<ActorIdentifier, ActorData> pair) private void DrawSelectable(KeyValuePair<ActorIdentifier, ActorData> pair)
{ {
var equal = pair.Key.Equals(_identifier); var equal = pair.Key.Equals(_identifier);
if (ImGui.Selectable(pair.Value.Label, equal) && !equal) if (ImGui.Selectable(pair.Value.Label, equal) || equal)
{ {
_identifier = pair.Key.CreatePermanent(); _identifier = pair.Key.CreatePermanent();
_currentData = pair.Value; _currentData = pair.Value;
_currentSave = _currentData.Valid ? _activeDesigns.GetOrCreateSave(_currentData.Objects[0]) : null; if (!_activeDesigns.TryGetValue(_identifier, out _currentSave))
_currentSave = _currentData.Valid ? _activeDesigns.GetOrCreateSave(_currentData.Objects[0]) : null;
} }
} }

View file

@ -2,10 +2,16 @@
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface; using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Gui.Designs; using Glamourer.Gui.Designs;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.State;
using ImGuiNET; using ImGuiNET;
using Microsoft.VisualBasic;
using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -18,14 +24,19 @@ public partial class Interface
public readonly DesignFileSystemSelector Selector; public readonly DesignFileSystemSelector Selector;
private readonly Interface _main; private readonly Interface _main;
private readonly DesignFileSystem _fileSystem; private readonly DesignFileSystem _fileSystem;
private readonly Design.Manager _manager; private readonly DesignManager _designManager;
private readonly ActiveDesign.Manager _activeDesignManager;
private readonly ObjectManager _objects;
public DesignTab(Interface main, Design.Manager manager, DesignFileSystem fileSystem, KeyState keyState) public DesignTab(Interface main, DesignManager designManager, DesignFileSystem fileSystem, KeyState keyState,
ActiveDesign.Manager activeDesignManager, ObjectManager objects)
{ {
_main = main; _main = main;
_manager = manager; _designManager = designManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
Selector = new DesignFileSystemSelector(manager, fileSystem, keyState); _activeDesignManager = activeDesignManager;
_objects = objects;
Selector = new DesignFileSystemSelector(designManager, fileSystem, keyState);
} }
public void Dispose() public void Dispose()
@ -45,13 +56,30 @@ public partial class Interface
public float GetDesignSelectorSize() public float GetDesignSelectorSize()
=> 200f * ImGuiHelpers.GlobalScale; => 200f * ImGuiHelpers.GlobalScale;
public void DrawDesignPanel() private void ApplySelfButton()
{ {
using var child = ImRaii.Child("##DesignPanel", new Vector2(-0.001f), true, ImGuiWindowFlags.HorizontalScrollbar); var self = _objects.Player;
if (!child || Selector.Selected == null) if (!ImGuiUtil.DrawDisabledButton("Apply to Self", Vector2.Zero, string.Empty, !self.Valid))
return; return;
_main._customizationDrawer.Draw(Selector.Selected.Customize(), CustomizeFlagExtensions.All, true); 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;
_main._customizationDrawer.Draw(Selector.Selected.Customize, CustomizeFlagExtensions.All, true);
foreach (var slot in EquipSlotExtensions.EqdpSlots) foreach (var slot in EquipSlotExtensions.EqdpSlots)
{ {
var current = Selector.Selected.Armor(slot); var current = Selector.Selected.Armor(slot);

View file

@ -29,7 +29,7 @@ public partial class Interface : Window, IDisposable
private readonly DebugStateTab _debugStateTab; private readonly DebugStateTab _debugStateTab;
private readonly DebugDataTab _debugDataTab; private readonly DebugDataTab _debugDataTab;
public Interface(DalamudPluginInterface pi, ItemManager items, ActiveDesign.Manager activeDesigns, Design.Manager manager, public Interface(DalamudPluginInterface pi, ItemManager items, ActiveDesign.Manager activeDesigns, DesignManager designManager,
DesignFileSystem fileSystem, ObjectManager objects, CustomizationService customization, Configuration config, DataManager gameData, TargetManager targets, ActorService actors, KeyState keyState) DesignFileSystem fileSystem, ObjectManager objects, CustomizationService customization, Configuration config, DataManager gameData, TargetManager targets, ActorService actors, KeyState keyState)
: base(GetLabel()) : base(GetLabel())
{ {
@ -46,7 +46,7 @@ public partial class Interface : Window, IDisposable
_actorTab = new ActorTab(this, activeDesigns, objects, targets, actors, items); _actorTab = new ActorTab(this, activeDesigns, objects, targets, actors, items);
_debugStateTab = new DebugStateTab(activeDesigns); _debugStateTab = new DebugStateTab(activeDesigns);
_debugDataTab = new DebugDataTab(customization); _debugDataTab = new DebugDataTab(customization);
_designTab = new DesignTab(this, manager, fileSystem, keyState); _designTab = new DesignTab(this, designManager, fileSystem, keyState, activeDesigns, objects);
} }
public override void Draw() public override void Draw()

View file

@ -39,6 +39,9 @@ public unsafe partial struct Actor : IEquatable<Actor>, IDesignable
return false; return false;
} }
public override string ToString()
=> Pointer != null ? Utf8Name.ToString() : "Invalid";
public bool IsAvailable public bool IsAvailable
=> Pointer->GameObject.GetIsTargetable(); => Pointer->GameObject.GetIsTargetable();

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,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

@ -1,14 +0,0 @@
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
namespace Glamourer.Interop;
public unsafe partial class RedrawManager
{
}

View file

@ -1,72 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Glamourer.Designs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe partial class RedrawManager
{
private delegate ulong FlagSlotForUpdateDelegate(nint drawObject, uint slot, CharacterArmor* data);
// 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<FlagSlotForUpdateDelegate> _flagSlotForUpdateHook = null!;
public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data)
=> FlagSlotForUpdateDetourBase(drawObject.Address, slot.ToIndex(), &data, true);
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)
=> FlagSlotForUpdateDetourBase(drawObject, slotIdx, data, false);
private ulong FlagSlotForUpdateDetourBase(nint drawObject, uint slotIdx, CharacterArmor* data, bool manual)
{
// try
// {
// var slot = slotIdx.ToEquipSlot();
// Glamourer.Log.Verbose(
// $"Flagged slot {slot} of 0x{(ulong)drawObject:X} for update with {data->Set.Value}-{data->Variant} (Stain {data->Stain.Value}).");
// HandleEquipUpdate(drawObject, slot, ref *data, manual);
// }
// catch (Exception ex)
// {
// Glamourer.Log.Error($"Error invoking SlotUpdate:\n{ex}");
// }
//
return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data);
//try
//{
// var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject);
// var identifier = actor.GetIdentifier();
//
// if (_fixedDesigns.TryGetDesign(identifier, out var design))
// {
// PluginLog.Information($"Loaded {slot} from fixed design for {identifier}.");
// (var replaced, *data) =
// Glamourer.Items.RestrictedGear.ResolveRestricted(design.Armor(slot).Model, slot, (Race)drawObject->Race, (Gender)drawObject->Sex);
// }
// else if (_currentManipulations.TryGetDesign(identifier, out var save2))
// {
// PluginLog.Information($"Updated {slot} from current designs for {identifier}.");
// (var replaced, *data) =
// Glamourer.Items.RestrictedGear.ResolveRestricted(*data, slot, (Race)drawObject->Race, (Gender)(drawObject->Sex + 1));
// save2.Data.Equipment[slot] = *data;
// }
//}
//catch (Exception e)
//{
// PluginLog.Error($"Error on loading new gear:\n{e}");
//}
//
//return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data);
}
}

View file

@ -1,102 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public unsafe partial class RedrawManager
{
public static readonly int CharacterWeaponOffset = (int)Marshal.OffsetOf<Character>("DrawData");
public delegate void LoadWeaponDelegate(IntPtr 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(IntPtr 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, oldWeapon, 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);
}
}

View file

@ -1,224 +1,45 @@
using System; using System;
using System.Diagnostics; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Hooking; using Dalamud.Game.ClientState;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Glamourer.Api;
using Glamourer.Customization; using Glamourer.Customization;
using Glamourer.Services; using Glamourer.Services;
using Glamourer.State; using Glamourer.State;
using Glamourer.Structs;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData; using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Interop; namespace Glamourer.Interop;
public partial class Interop : IDisposable
{
private readonly JobService _jobService;
public Interop(JobService jobService)
{
_jobService = jobService;
SignatureHelper.Initialise(this);
_changeJobHook.Enable();
_flagSlotForUpdateHook.Enable();
_setupVisorHook.Enable();
}
public void Dispose()
{
_changeJobHook.Dispose();
_flagSlotForUpdateHook.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 static unsafe void SetVisorState(nint humanPtr, bool on)
{
if (humanPtr == IntPtr.Zero)
return;
var data = (Human*)humanPtr;
var flags = &data->CharacterBase.UnkFlags_01;
var state = (*flags & Offsets.DrawObjectVisorStateFlag) != 0;
if (state == on)
return;
var newFlag = (byte)(on ? *flags | Offsets.DrawObjectVisorStateFlag : *flags & ~Offsets.DrawObjectVisorStateFlag);
*flags = (byte)(newFlag | Offsets.DrawObjectVisorToggleFlag);
}
}
public partial class Interop
{
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.Verbose($"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.Verbose(
$"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}, initial call was {initialValue}.");
}
}
public unsafe partial class Interop
{
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.Verbose(
$"{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.Verbose(
$"{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}.");
}
}
public unsafe partial class Interop
{
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)
{
Debug.Assert(customize.Data != null, "Customize was invalid.");
if (!actor.Valid || !actor.DrawObject.Valid)
return false;
return _changeCustomize(actor.DrawObject.Pointer, customize.Data, 1);
}
}
public partial class Interop
{
private delegate void ChangeJobDelegate(IntPtr data, uint job);
[Signature(Sigs.ChangeJob, DetourName = nameof(ChangeJobDetour))]
private readonly Hook<ChangeJobDelegate> _changeJobHook = null!;
private void ChangeJobDetour(IntPtr data, uint job)
{
_changeJobHook.Original(data, job);
JobChanged?.Invoke(data - Offsets.Character.ClassJobContainer, _jobService.Jobs[(byte)job]);
}
public event Action<Actor, Job>? JobChanged;
}
public unsafe partial class RedrawManager : IDisposable public unsafe partial class RedrawManager : IDisposable
{ {
private readonly ItemManager _items; private readonly ItemManager _items;
private readonly ActorService _actors; private readonly ActorService _actors;
private readonly FixedDesignManager _fixedDesignManager; private readonly FixedDesignManager _fixedDesignManager;
private readonly ActiveDesign.Manager _stateManager; private readonly ActiveDesign.Manager _stateManager;
private readonly PenumbraAttach _penumbra;
private readonly WeaponService _weapons;
public RedrawManager(FixedDesignManager fixedDesignManager, ActiveDesign.Manager stateManager, ItemManager items, ActorService actors) public RedrawManager(FixedDesignManager fixedDesignManager, ActiveDesign.Manager stateManager, ItemManager items, ActorService actors,
PenumbraAttach penumbra, WeaponService weapons)
{ {
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_fixedDesignManager = fixedDesignManager; _fixedDesignManager = fixedDesignManager;
_stateManager = stateManager; _stateManager = stateManager;
_items = items; _items = items;
_actors = actors; _actors = actors;
_flagSlotForUpdateHook.Enable(); _penumbra = penumbra;
_loadWeaponHook.Enable(); _weapons = weapons;
_penumbra.CreatingCharacterBase += OnCharacterRedraw;
_penumbra.CreatedCharacterBase += OnCharacterRedrawFinished;
} }
public void Dispose() public void Dispose()
{ {
_flagSlotForUpdateHook.Dispose();
_loadWeaponHook.Dispose();
} }
private void OnCharacterRedraw(Actor actor, uint* modelId, Customize customize, CharacterEquip equip) private void OnCharacterRedraw(Actor actor, uint* modelId, Customize customize, CharacterEquip equip)
@ -230,21 +51,21 @@ public unsafe partial class RedrawManager : IDisposable
// Check if we have a current design in use, or if not if the actor has a fixed design. // 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); var identifier = actor.GetIdentifier(_actors.AwaitedService);
if (!(_stateManager.TryGetValue(identifier, out var save) || _fixedDesignManager.TryGetDesign(identifier, out var save2))) if (!_stateManager.TryGetValue(identifier, out var save))
return; return;
// Compare game object customize data against draw object customize data for transformations. // Compare game object customize data against draw object customize data for transformations.
// Apply customization if they correspond and there is customization to apply. // Apply customization if they correspond and there is customization to apply.
var gameObjectCustomize = new Customize((CustomizeData*)actor.Pointer->CustomizeData); var gameObjectCustomize = new Customize((CustomizeData*)actor.Pointer->CustomizeData);
if (gameObjectCustomize.Equals(customize)) if (gameObjectCustomize.Equals(customize))
customize.Load(save!.Customize()); customize.Load(save!.Customize);
// Compare game object equip data against draw object equip data for transformations. // Compare game object equip data against draw object equip data for transformations.
// Apply each piece of equip that should be applied if they correspond. // Apply each piece of equip that should be applied if they correspond.
var gameObjectEquip = new CharacterEquip((CharacterArmor*)actor.Pointer->EquipSlotData); var gameObjectEquip = new CharacterEquip((CharacterArmor*)actor.Pointer->EquipSlotData);
if (gameObjectEquip.Equals(equip)) if (gameObjectEquip.Equals(equip))
{ {
var saveEquip = save!.Equipment(); var saveEquip = save!.Equipment;
foreach (var slot in EquipSlotExtensions.EqdpSlots) foreach (var slot in EquipSlotExtensions.EqdpSlots)
{ {
(_, equip[slot]) = (_, equip[slot]) =

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

View file

@ -1,16 +0,0 @@
using System.Collections.Generic;
using Dalamud.Data;
namespace Glamourer.Services;
public class JobService
{
public readonly IReadOnlyDictionary<byte, Structs.Job> Jobs;
public readonly IReadOnlyDictionary<ushort, Structs.JobGroup> JobGroups;
public JobService(DataManager gameData)
{
Jobs = GameData.Jobs(gameData);
JobGroups = GameData.JobGroups(gameData);
}
}

View file

@ -53,18 +53,22 @@ public static class ServiceManager
.AddSingleton<ActorService>() .AddSingleton<ActorService>()
.AddSingleton<ItemService>() .AddSingleton<ItemService>()
.AddSingleton<ItemManager>() .AddSingleton<ItemManager>()
.AddSingleton<CustomizationService>() .AddSingleton<CustomizationService>();
.AddSingleton<JobService>();
private static IServiceCollection AddInterop(this IServiceCollection services) private static IServiceCollection AddInterop(this IServiceCollection services)
=> services.AddSingleton<Interop.Interop>() => services.AddSingleton<ChangeCustomizeService>()
.AddSingleton<JobService>()
.AddSingleton<UpdateSlotService>()
.AddSingleton<VisorService>()
.AddSingleton<WeaponService>()
.AddSingleton<ObjectManager>(); .AddSingleton<ObjectManager>();
private static IServiceCollection AddDesigns(this IServiceCollection services) private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<Design.Manager>() => services.AddSingleton<DesignManager>()
.AddSingleton<DesignFileSystem>() .AddSingleton<DesignFileSystem>()
.AddSingleton<ActiveDesign.Manager>() .AddSingleton<ActiveDesign.Manager>()
.AddSingleton<FixedDesignManager>(); .AddSingleton<FixedDesignManager>()
.AddSingleton<RedrawManager>();
private static IServiceCollection AddInterface(this IServiceCollection services) private static IServiceCollection AddInterface(this IServiceCollection services)
=> services.AddSingleton<Interface>() => services.AddSingleton<Interface>()

View file

@ -14,28 +14,44 @@ using Penumbra.Api.Enums;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData; using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
namespace Glamourer.State; namespace Glamourer.State;
public sealed partial class ActiveDesign public sealed partial class ActiveDesign
{ {
public partial class Manager : IReadOnlyDictionary<ActorIdentifier, ActiveDesign> [Flags]
public enum ChangeType
{ {
private readonly ActorService _actors; Default = 0x00,
private readonly ObjectManager _objects; Changed = 0x01,
private readonly Interop.Interop _interop; Fixed = 0x02,
private readonly PenumbraAttach _penumbra; }
private readonly ItemManager _items;
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(); private readonly Dictionary<ActorIdentifier, ActiveDesign> _characterSaves = new();
public Manager(ActorService actors, ObjectManager objects, Interop.Interop interop, PenumbraAttach penumbra, ItemManager items) public Manager(ActorService actors, ObjectManager objects, PenumbraAttach penumbra, ItemManager items, VisorService visor,
ChangeCustomizeService customize, UpdateSlotService updateSlot, WeaponService weaponService)
{ {
_actors = actors; _actors = actors;
_objects = objects; _objects = objects;
_interop = interop; _penumbra = penumbra;
_penumbra = penumbra; _items = items;
_items = items; _visor = visor;
_customize = customize;
_updateSlot = updateSlot;
_weaponService = weaponService;
} }
public IEnumerator<KeyValuePair<ActorIdentifier, ActiveDesign>> GetEnumerator() public IEnumerator<KeyValuePair<ActorIdentifier, ActiveDesign>> GetEnumerator()
@ -99,15 +115,25 @@ public sealed partial class ActiveDesign
return; return;
if (from.DoApplyEquip(EquipSlot.MainHand)) if (from.DoApplyEquip(EquipSlot.MainHand))
ChangeMainHand(to, from.MainHand, fromFixed); ChangeMainHand(to, from.MainHandId, fromFixed);
if (from.DoApplyStain(EquipSlot.MainHand))
ChangeStain(to, EquipSlot.MainHand, from.WeaponMain.Stain, fromFixed);
if (from.DoApplyEquip(EquipSlot.OffHand)) if (from.DoApplyEquip(EquipSlot.OffHand))
ChangeOffHand(to, from.OffHand, fromFixed); ChangeOffHand(to, from.OffHandId, fromFixed);
if (from.DoApplyStain(EquipSlot.OffHand))
ChangeStain(to, EquipSlot.OffHand, from.WeaponOff.Stain, fromFixed);
foreach (var slot in EquipSlotExtensions.EqdpSlots.Where(from.DoApplyEquip)) foreach (var slot in EquipSlotExtensions.EqdpSlots)
ChangeEquipment(to, slot, from.Armor(slot), fromFixed); {
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.Customize().Data, fromFixed); ChangeCustomize(to, from.ApplyCustomize, *from.Customize.Data, fromFixed);
if (from.Wetness.Enabled) if (from.Wetness.Enabled)
SetWetness(to, from.Wetness.ForcedValue, fromFixed); SetWetness(to, from.Wetness.ForcedValue, fromFixed);
@ -177,7 +203,7 @@ public sealed partial class ActiveDesign
if (redraw) if (redraw)
_penumbra.RedrawObject(obj, RedrawType.Redraw); _penumbra.RedrawObject(obj, RedrawType.Redraw);
else else
_interop.UpdateCustomize(obj, design.CharacterData.CustomizeData); _customize.UpdateCustomize(obj, design.ModelData.CustomizeData);
} }
} }
@ -205,7 +231,7 @@ public sealed partial class ActiveDesign
return; return;
foreach (var obj in data.Objects) foreach (var obj in data.Objects)
_interop.UpdateSlot(obj.DrawObject, slot, item); _updateSlot.UpdateSlot(obj.DrawObject, slot, item);
} }
public void ChangeEquipment(ActiveDesign design, EquipSlot slot, Item item, bool fromFixed) public void ChangeEquipment(ActiveDesign design, EquipSlot slot, Item item, bool fromFixed)
@ -228,16 +254,20 @@ public sealed partial class ActiveDesign
return; return;
foreach (var obj in data.Objects) foreach (var obj in data.Objects)
_interop.UpdateSlot(obj.DrawObject, slot, item.Model); _updateSlot.UpdateSlot(obj.DrawObject, slot, item.Model);
} }
public void ChangeStain(ActiveDesign design, EquipSlot slot, StainId stain, bool fromFixed) public void ChangeStain(ActiveDesign design, EquipSlot slot, StainId stain, bool fromFixed)
{ {
var flag = slot.ToStainFlag(); var flag = slot.ToStainFlag();
design.SetStain(slot, stain); design.SetStain(slot, stain);
var current = design.Armor(slot); var (current, initial, weapon) = slot switch
var initial = design._initialData.Equipment[slot]; {
if (current.Stain.Value != initial.Stain.Value) 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.Equipment[slot].Stain, false),
};
if (current.Value != initial.Value)
design.ChangedEquip |= flag; design.ChangedEquip |= flag;
else else
design.ChangedEquip &= ~flag; design.ChangedEquip &= ~flag;
@ -251,7 +281,12 @@ public sealed partial class ActiveDesign
return; return;
foreach (var obj in data.Objects) foreach (var obj in data.Objects)
_interop.UpdateStain(obj.DrawObject, slot, stain); {
if (weapon)
_weaponService.LoadStain(obj, EquipSlot.MainHand, stain);
else
_updateSlot.UpdateStain(obj.DrawObject, slot, stain);
}
} }
public void ChangeVisor(ActiveDesign design, bool on, bool fromFixed) public void ChangeVisor(ActiveDesign design, bool on, bool fromFixed)
@ -266,7 +301,7 @@ public sealed partial class ActiveDesign
return; return;
foreach (var obj in data.Objects) foreach (var obj in data.Objects)
Interop.Interop.SetVisorState(obj.DrawObject, on); _visor.SetVisorState(obj.DrawObject, on);
} }
} }
} }

View file

@ -7,11 +7,11 @@ using Penumbra.GameData.Enums;
namespace Glamourer.State; namespace Glamourer.State;
public sealed partial class ActiveDesign : DesignBase public sealed partial class ActiveDesign : DesignData
{ {
public readonly ActorIdentifier Identifier; public readonly ActorIdentifier Identifier;
private CharacterData _initialData = new(); private ModelData _initialData = new();
public CustomizeFlag ChangedCustomize { get; private set; } = 0; public CustomizeFlag ChangedCustomize { get; private set; } = 0;
public CustomizeFlag FixedCustomize { get; private set; } = 0; public CustomizeFlag FixedCustomize { get; private set; } = 0;
@ -19,6 +19,7 @@ public sealed partial class ActiveDesign : DesignBase
public EquipFlag ChangedEquip { get; private set; } = 0; public EquipFlag ChangedEquip { get; private set; } = 0;
public EquipFlag FixedEquip { get; private set; } = 0; public EquipFlag FixedEquip { get; private set; } = 0;
public bool IsHatVisible { get; private set; } = false; public bool IsHatVisible { get; private set; } = false;
public bool IsWeaponVisible { get; private set; } = false; public bool IsWeaponVisible { get; private set; } = false;
public bool IsVisorToggled { get; private set; } = false; public bool IsVisorToggled { get; private set; } = false;
@ -73,7 +74,7 @@ public sealed partial class ActiveDesign : DesignBase
if (!_initialData.Customize.Equals(actor.Customize)) if (!_initialData.Customize.Equals(actor.Customize))
{ {
_initialData.Customize.Load(actor.Customize); _initialData.Customize.Load(actor.Customize);
Customize().Load(actor.Customize); Customize.Load(actor.Customize);
} }
var initialEquip = _initialData.Equipment; var initialEquip = _initialData.Equipment;
@ -103,13 +104,13 @@ public sealed partial class ActiveDesign : DesignBase
SetStain(EquipSlot.OffHand, actor.OffHand.Stain); SetStain(EquipSlot.OffHand, actor.OffHand.Stain);
} }
var visor = Interop.Interop.GetVisorState(actor.DrawObject); var visor = VisorService.GetVisorState(actor.DrawObject);
if (IsVisorToggled != visor) if (IsVisorToggled != visor)
IsVisorToggled = visor; IsVisorToggled = visor;
} }
public string CreateOldBase64() public string CreateOldBase64()
=> CreateOldBase64(in CharacterData, EquipFlagExtensions.All, CustomizeFlagExtensions.All, IsWet, IsHatVisible, true, => DesignBase64Migration.CreateOldBase64(in ModelData, EquipFlagExtensions.All, CustomizeFlagExtensions.All, IsWet, IsHatVisible, true,
IsVisorToggled, IsVisorToggled,
true, IsWeaponVisible, true, false, 1f); true, IsWeaponVisible, true, false, 1f);
} }

View file

@ -1,15 +1,29 @@
using System.Diagnostics.CodeAnalysis; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using Glamourer.Designs; using Glamourer.Designs;
using Glamourer.Interop; using Glamourer.Interop;
using Glamourer.Structs;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
namespace Glamourer.State; namespace Glamourer.State;
public class FixedDesignManager public class FixedDesignManager
{ {
public bool TryGetDesign(ActorIdentifier actor, [NotNullWhen(true)] out Design? save) public class FixedDesign
{ {
save = null; public Design Design;
return false; 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>();
} }
} }