mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
So much stuff...
This commit is contained in:
parent
f71b800b2e
commit
80ad0d774b
31 changed files with 2949 additions and 1226 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using Dalamud.Game.ClientState.Actors.Types;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
|
|
||||||
namespace Glamourer.Customization
|
namespace Glamourer.Customization
|
||||||
|
|
@ -13,6 +14,9 @@ namespace Glamourer.Customization
|
||||||
|
|
||||||
public ref ActorCustomization Value
|
public ref ActorCustomization Value
|
||||||
=> ref *Address;
|
=> ref *Address;
|
||||||
|
|
||||||
|
public LazyCustomization(ActorCustomization data)
|
||||||
|
=> Address = &data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -22,7 +26,36 @@ namespace Glamourer.Customization
|
||||||
public const int CustomizationOffset = 0x1898;
|
public const int CustomizationOffset = 0x1898;
|
||||||
public const int CustomizationBytes = 26;
|
public const int CustomizationBytes = 26;
|
||||||
|
|
||||||
private byte _race;
|
public static ActorCustomization Default = new()
|
||||||
|
{
|
||||||
|
Race = Race.Hyur,
|
||||||
|
Gender = Gender.Male,
|
||||||
|
BodyType = 1,
|
||||||
|
Height = 50,
|
||||||
|
Clan = SubRace.Midlander,
|
||||||
|
Face = 1,
|
||||||
|
Hairstyle = 1,
|
||||||
|
HighlightsOn = false,
|
||||||
|
SkinColor = 1,
|
||||||
|
EyeColorRight = 1,
|
||||||
|
HighlightsColor = 1,
|
||||||
|
FacialFeatures = 0,
|
||||||
|
TattooColor = 1,
|
||||||
|
Eyebrow = 1,
|
||||||
|
EyeColorLeft = 1,
|
||||||
|
EyeShape = 1,
|
||||||
|
Nose = 1,
|
||||||
|
Jaw = 1,
|
||||||
|
Mouth = 1,
|
||||||
|
LipColor = 1,
|
||||||
|
MuscleMass = 50,
|
||||||
|
TailShape = 1,
|
||||||
|
BustSize = 50,
|
||||||
|
FacePaint = 1,
|
||||||
|
FacePaintColor = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
public Race Race;
|
||||||
private byte _gender;
|
private byte _gender;
|
||||||
public byte BodyType;
|
public byte BodyType;
|
||||||
public byte Height;
|
public byte Height;
|
||||||
|
|
@ -49,12 +82,6 @@ namespace Glamourer.Customization
|
||||||
private byte _facePaint;
|
private byte _facePaint;
|
||||||
public byte FacePaintColor;
|
public byte FacePaintColor;
|
||||||
|
|
||||||
public Race Race
|
|
||||||
{
|
|
||||||
get => (Race) (_race > (byte) Race.Midlander ? _race + 1 : _race);
|
|
||||||
set => _race = (byte) (value > Race.Highlander ? value - 1 : value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Gender Gender
|
public Gender Gender
|
||||||
{
|
{
|
||||||
get => (Gender) (_gender + 1);
|
get => (Gender) (_gender + 1);
|
||||||
|
|
@ -117,12 +144,21 @@ namespace Glamourer.Customization
|
||||||
|
|
||||||
public unsafe void Read(IntPtr customizeAddress)
|
public unsafe void Read(IntPtr customizeAddress)
|
||||||
{
|
{
|
||||||
fixed (byte* ptr = &_race)
|
fixed (Race* ptr = &Race)
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(customizeAddress.ToPointer(), ptr, CustomizationBytes, CustomizationBytes);
|
Buffer.MemoryCopy(customizeAddress.ToPointer(), ptr, CustomizationBytes, CustomizationBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Read(Actor actor)
|
||||||
|
=> Read(actor.Address + CustomizationOffset);
|
||||||
|
|
||||||
|
public ActorCustomization(Actor actor)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
Read(actor.Address + CustomizationOffset);
|
||||||
|
}
|
||||||
|
|
||||||
public byte this[CustomizationId id]
|
public byte this[CustomizationId id]
|
||||||
{
|
{
|
||||||
get => id switch
|
get => id switch
|
||||||
|
|
@ -244,7 +280,7 @@ namespace Glamourer.Customization
|
||||||
|
|
||||||
public unsafe void Write(IntPtr actorAddress)
|
public unsafe void Write(IntPtr actorAddress)
|
||||||
{
|
{
|
||||||
fixed (byte* ptr = &_race)
|
fixed (Race* ptr = &Race)
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(ptr, (byte*) actorAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes);
|
Buffer.MemoryCopy(ptr, (byte*) actorAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes);
|
||||||
}
|
}
|
||||||
|
|
@ -252,7 +288,7 @@ namespace Glamourer.Customization
|
||||||
|
|
||||||
public unsafe void WriteBytes(byte[] array, int offset = 0)
|
public unsafe void WriteBytes(byte[] array, int offset = 0)
|
||||||
{
|
{
|
||||||
fixed (byte* ptr = &_race)
|
fixed (Race* ptr = &Race)
|
||||||
{
|
{
|
||||||
Marshal.Copy(new IntPtr(ptr), array, offset, CustomizationBytes);
|
Marshal.Copy(new IntPtr(ptr), array, offset, CustomizationBytes);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ namespace Glamourer.Customization
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
|
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CharaMakeParams.MenuType ToType(this CustomizationId customizationId, Race race = Race.Midlander)
|
public static CharaMakeParams.MenuType ToType(this CustomizationId customizationId, Race race = Race.Hyur)
|
||||||
=> customizationId switch
|
=> customizationId switch
|
||||||
{
|
{
|
||||||
CustomizationId.Race => CharaMakeParams.MenuType.IconSelector,
|
CustomizationId.Race => CharaMakeParams.MenuType.IconSelector,
|
||||||
|
|
|
||||||
BIN
Glamourer.zip
BIN
Glamourer.zip
Binary file not shown.
79
Glamourer/ActorExtensions.cs
Normal file
79
Glamourer/ActorExtensions.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
using Dalamud.Game.ClientState.Actors.Types;
|
||||||
|
|
||||||
|
namespace Glamourer
|
||||||
|
{
|
||||||
|
public static class ActorExtensions
|
||||||
|
{
|
||||||
|
public const int WetnessOffset = 0x19A5;
|
||||||
|
public const byte WetnessFlag = 0x10;
|
||||||
|
public const int StateFlagsOffset = 0x106C;
|
||||||
|
public const byte HatHiddenFlag = 0x01;
|
||||||
|
public const byte VisorToggledFlag = 0x10;
|
||||||
|
public const int AlphaOffset = 0x182C;
|
||||||
|
public const int WeaponHiddenOffset = 0xF64;
|
||||||
|
public const byte WeaponHiddenFlag = 0x02;
|
||||||
|
|
||||||
|
public static unsafe bool IsWet(this Actor a)
|
||||||
|
=> (*((byte*) a.Address + WetnessOffset) & WetnessFlag) != 0;
|
||||||
|
|
||||||
|
public static unsafe bool SetWetness(this Actor a, bool value)
|
||||||
|
{
|
||||||
|
var current = a.IsWet();
|
||||||
|
if (current == value)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
*((byte*) a.Address + WetnessOffset) = (byte) (*((byte*) a.Address + WetnessOffset) | WetnessFlag);
|
||||||
|
else
|
||||||
|
*((byte*) a.Address + WetnessOffset) = (byte) (*((byte*) a.Address + WetnessOffset) & ~WetnessFlag);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe ref byte StateFlags(this Actor a)
|
||||||
|
=> ref *((byte*) a.Address + StateFlagsOffset);
|
||||||
|
|
||||||
|
public static bool SetStateFlag(this Actor a, bool value, byte flag)
|
||||||
|
{
|
||||||
|
var current = a.StateFlags();
|
||||||
|
var previousValue = (current & flag) != 0;
|
||||||
|
if (previousValue == value)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
a.StateFlags() = (byte) (current | flag);
|
||||||
|
else
|
||||||
|
a.StateFlags() = (byte) (current & ~flag);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsHatHidden(this Actor a)
|
||||||
|
=> (a.StateFlags() & HatHiddenFlag) != 0;
|
||||||
|
|
||||||
|
public static unsafe bool IsWeaponHidden(this Actor a)
|
||||||
|
=> (a.StateFlags() & WeaponHiddenFlag) != 0
|
||||||
|
&& (*((byte*) a.Address + WeaponHiddenOffset) & WeaponHiddenFlag) != 0;
|
||||||
|
|
||||||
|
public static bool IsVisorToggled(this Actor a)
|
||||||
|
=> (a.StateFlags() & VisorToggledFlag) != 0;
|
||||||
|
|
||||||
|
public static bool SetHatHidden(this Actor a, bool value)
|
||||||
|
=> SetStateFlag(a, value, HatHiddenFlag);
|
||||||
|
|
||||||
|
public static unsafe bool SetWeaponHidden(this Actor a, bool value)
|
||||||
|
{
|
||||||
|
var ret = SetStateFlag(a, value, WeaponHiddenFlag);
|
||||||
|
var val = *((byte*) a.Address + WeaponHiddenOffset);
|
||||||
|
if (value)
|
||||||
|
*((byte*) a.Address + WeaponHiddenOffset) = (byte) (val | WeaponHiddenFlag);
|
||||||
|
else
|
||||||
|
*((byte*) a.Address + WeaponHiddenOffset) = (byte) (val & ~WeaponHiddenFlag);
|
||||||
|
return ret || ((val & WeaponHiddenFlag) != 0) != value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SetVisorToggled(this Actor a, bool value)
|
||||||
|
=> SetStateFlag(a, value, VisorToggledFlag);
|
||||||
|
|
||||||
|
public static unsafe ref float Alpha(this Actor a)
|
||||||
|
=> ref *(float*) ((byte*) a.Address + AlphaOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
275
Glamourer/CharacterSave.cs
Normal file
275
Glamourer/CharacterSave.cs
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
using System;
|
||||||
|
using Dalamud.Game.ClientState.Actors.Types;
|
||||||
|
using Glamourer.Customization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
namespace Glamourer
|
||||||
|
{
|
||||||
|
public class CharacterSaveConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
=> objectType == typeof(CharacterSave);
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
var token = JToken.Load(reader);
|
||||||
|
var s = token.ToObject<string>();
|
||||||
|
return CharacterSave.FromString(s!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite
|
||||||
|
=> true;
|
||||||
|
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
var s = ((CharacterSave) value).ToBase64();
|
||||||
|
serializer.Serialize(writer, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(CharacterSaveConverter))]
|
||||||
|
public class CharacterSave
|
||||||
|
{
|
||||||
|
public const byte CurrentVersion = 2;
|
||||||
|
public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + ActorCustomization.CustomizationBytes;
|
||||||
|
public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + ActorCustomization.CustomizationBytes + 4 + 1;
|
||||||
|
|
||||||
|
public const byte TotalSize = TotalSizeVersion2;
|
||||||
|
|
||||||
|
private readonly byte[] _bytes = new byte[TotalSize];
|
||||||
|
|
||||||
|
public CharacterSave()
|
||||||
|
{
|
||||||
|
_bytes[0] = CurrentVersion;
|
||||||
|
Alpha = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharacterSave Copy()
|
||||||
|
{
|
||||||
|
var ret = new CharacterSave();
|
||||||
|
_bytes.CopyTo((Span<byte>) ret._bytes);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte Version
|
||||||
|
=> _bytes[0];
|
||||||
|
|
||||||
|
public bool WriteCustomizations
|
||||||
|
{
|
||||||
|
get => (_bytes[1] & 0x01) != 0;
|
||||||
|
set => _bytes[1] = (byte) (value ? _bytes[1] | 0x01 : _bytes[1] & ~0x01);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsWet
|
||||||
|
{
|
||||||
|
get => (_bytes[1] & 0x02) != 0;
|
||||||
|
set => _bytes[1] = (byte) (value ? _bytes[1] | 0x02 : _bytes[1] & ~0x02);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetHatState
|
||||||
|
{
|
||||||
|
get => (_bytes[1] & 0x04) != 0;
|
||||||
|
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x04 : _bytes[1] & ~0x04);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetWeaponState
|
||||||
|
{
|
||||||
|
get => (_bytes[1] & 0x08) != 0;
|
||||||
|
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x08 : _bytes[1] & ~0x08);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetVisorState
|
||||||
|
{
|
||||||
|
get => (_bytes[1] & 0x10) != 0;
|
||||||
|
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x10 : _bytes[1] & ~0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WriteProtected
|
||||||
|
{
|
||||||
|
get => (_bytes[1] & 0x20) != 0;
|
||||||
|
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x20 : _bytes[1] & ~0x20);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte StateFlags
|
||||||
|
{
|
||||||
|
get => _bytes[64 + ActorCustomization.CustomizationBytes];
|
||||||
|
set => _bytes[64 + ActorCustomization.CustomizationBytes] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HatState
|
||||||
|
{
|
||||||
|
get => (StateFlags & 0x01) == 0;
|
||||||
|
set => StateFlags = (byte) (value ? StateFlags & ~0x01 : StateFlags | 0x01);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool VisorState
|
||||||
|
{
|
||||||
|
get => (StateFlags & 0x10) != 0;
|
||||||
|
set => StateFlags = (byte)(value ? StateFlags | 0x10 : StateFlags & ~0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WeaponState
|
||||||
|
{
|
||||||
|
get => (StateFlags & 0x02) == 0;
|
||||||
|
set => StateFlags = (byte)(value ? StateFlags & ~0x02 : StateFlags | 0x02);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActorEquipMask WriteEquipment
|
||||||
|
{
|
||||||
|
get => (ActorEquipMask) ((ushort) _bytes[2] | ((ushort) _bytes[3] << 8));
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_bytes[2] = (byte) ((ushort) value & 0xFF);
|
||||||
|
_bytes[3] = (byte) ((ushort) value >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe float Alpha
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
fixed (byte* ptr = &_bytes[60 + ActorCustomization.CustomizationBytes])
|
||||||
|
{
|
||||||
|
return *(float*) ptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
fixed (byte* ptr = _bytes)
|
||||||
|
{
|
||||||
|
*(ptr + 60 + ActorCustomization.CustomizationBytes + 0) = *((byte*) &value + 0);
|
||||||
|
*(ptr + 60 + ActorCustomization.CustomizationBytes + 1) = *((byte*) &value + 1);
|
||||||
|
*(ptr + 60 + ActorCustomization.CustomizationBytes + 2) = *((byte*) &value + 2);
|
||||||
|
*(ptr + 60 + ActorCustomization.CustomizationBytes + 3) = *((byte*) &value + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Load(ActorCustomization customization)
|
||||||
|
{
|
||||||
|
WriteCustomizations = true;
|
||||||
|
customization.WriteBytes(_bytes, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Load(ActorEquipment equipment, ActorEquipMask mask = ActorEquipMask.All)
|
||||||
|
{
|
||||||
|
WriteEquipment = mask;
|
||||||
|
equipment.WriteBytes(_bytes, 4 + ActorCustomization.CustomizationBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToBase64()
|
||||||
|
=> Convert.ToBase64String(_bytes);
|
||||||
|
|
||||||
|
private 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}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckRange(int idx, byte value, byte min, byte max)
|
||||||
|
{
|
||||||
|
if (value < min || value > max)
|
||||||
|
throw new Exception(
|
||||||
|
$"Can not parse Base64 string into CharacterSave:\n\tInvalid value {value} in byte {idx}, should be in [{min},{max}].");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckActorMask(byte val1, byte val2)
|
||||||
|
{
|
||||||
|
var mask = (ActorEquipMask)((ushort)val1 | ((ushort)val2 << 8));
|
||||||
|
if (mask > ActorEquipMask.All)
|
||||||
|
throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid value {mask} in byte 3 and 4.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadActor(Actor a)
|
||||||
|
{
|
||||||
|
WriteCustomizations = true;
|
||||||
|
Load(new ActorCustomization(a));
|
||||||
|
|
||||||
|
Load(new ActorEquipment(a), ActorEquipMask.All);
|
||||||
|
|
||||||
|
SetHatState = true;
|
||||||
|
SetVisorState = true;
|
||||||
|
SetWeaponState = true;
|
||||||
|
StateFlags = a.StateFlags();
|
||||||
|
|
||||||
|
IsWet = a.IsWet();
|
||||||
|
Alpha = a.Alpha();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Apply(Actor a)
|
||||||
|
{
|
||||||
|
if (WriteCustomizations)
|
||||||
|
Customizations.Write(a.Address);
|
||||||
|
if (WriteEquipment != ActorEquipMask.None)
|
||||||
|
Equipment.Write(a.Address, WriteEquipment, WriteEquipment);
|
||||||
|
a.SetWetness(IsWet);
|
||||||
|
a.Alpha() = Alpha;
|
||||||
|
if ((_bytes[1] & 0b11100) == 0b11100)
|
||||||
|
a.StateFlags() = StateFlags;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (SetHatState)
|
||||||
|
a.SetHatHidden(HatState);
|
||||||
|
if (SetVisorState)
|
||||||
|
a.SetVisorToggled(VisorState);
|
||||||
|
if (SetWeaponState)
|
||||||
|
a.SetWeaponHidden(WeaponState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Load(string base64)
|
||||||
|
{
|
||||||
|
var bytes = Convert.FromBase64String(base64);
|
||||||
|
switch (bytes[0])
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
CheckSize(bytes.Length, TotalSizeVersion1);
|
||||||
|
CheckRange(2, bytes[1], 0, 1);
|
||||||
|
Alpha = 1.0f;
|
||||||
|
bytes[0] = CurrentVersion;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
CheckSize(bytes.Length, TotalSizeVersion2);
|
||||||
|
CheckRange(2, bytes[1], 0, 0x3F);
|
||||||
|
break;
|
||||||
|
default: throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid Version {bytes[0]}.");
|
||||||
|
}
|
||||||
|
CheckActorMask(bytes[2], bytes[3]);
|
||||||
|
bytes.CopyTo(_bytes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CharacterSave FromString(string base64)
|
||||||
|
{
|
||||||
|
var ret = new CharacterSave();
|
||||||
|
ret.Load(base64);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe ref ActorCustomization Customizations
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
fixed (byte* ptr = _bytes)
|
||||||
|
{
|
||||||
|
return ref *((ActorCustomization*) (ptr + 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActorEquipment Equipment
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var ret = new ActorEquipment();
|
||||||
|
ret.FromBytes(_bytes, 4 + ActorCustomization.CustomizationBytes);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Glamourer/Designs/Design.cs
Normal file
22
Glamourer/Designs/Design.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
using Glamourer.FileSystem;
|
||||||
|
|
||||||
|
namespace Glamourer.Designs
|
||||||
|
{
|
||||||
|
public class Design : IFileSystemBase
|
||||||
|
{
|
||||||
|
public Folder Parent { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public CharacterSave Data { get; set; }
|
||||||
|
|
||||||
|
internal Design(Folder parent, string name)
|
||||||
|
{
|
||||||
|
Parent = parent;
|
||||||
|
Name = name;
|
||||||
|
Data = new CharacterSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
165
Glamourer/Designs/DesignManager.cs
Normal file
165
Glamourer/Designs/DesignManager.cs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Glamourer.FileSystem;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Glamourer.Designs
|
||||||
|
{
|
||||||
|
public class DesignManager
|
||||||
|
{
|
||||||
|
public const string FileName = "Designs.json";
|
||||||
|
private readonly FileInfo _saveFile;
|
||||||
|
|
||||||
|
public SortedList<string, CharacterSave> Designs = null!;
|
||||||
|
public FileSystem.FileSystem FileSystem { get; } = new();
|
||||||
|
|
||||||
|
public DesignManager(DalamudPluginInterface pi)
|
||||||
|
{
|
||||||
|
var saveFolder = new DirectoryInfo(pi.GetPluginConfigDirectory());
|
||||||
|
if (!saveFolder.Exists)
|
||||||
|
Directory.CreateDirectory(saveFolder.FullName);
|
||||||
|
|
||||||
|
_saveFile = new FileInfo(Path.Combine(saveFolder.FullName, FileName));
|
||||||
|
|
||||||
|
LoadFromFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildStructure()
|
||||||
|
{
|
||||||
|
FileSystem.Clear();
|
||||||
|
var anyChanges = false;
|
||||||
|
foreach (var kvp in Designs.ToArray())
|
||||||
|
{
|
||||||
|
var path = kvp.Key;
|
||||||
|
var save = kvp.Value;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (folder, name) = FileSystem.CreateAllFolders(path);
|
||||||
|
var design = new Design(folder, name) { Data = save };
|
||||||
|
folder.FindOrAddChild(design);
|
||||||
|
var fixedPath = design.FullName();
|
||||||
|
if (!string.Equals(fixedPath, path, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
Designs.Remove(path);
|
||||||
|
Designs[fixedPath] = save;
|
||||||
|
anyChanges = true;
|
||||||
|
PluginLog.Debug($"Problem loading saved designs, {path} was renamed to {fixedPath}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Problem loading saved designs, {path} was removed because:\n{e}");
|
||||||
|
Designs.Remove(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyChanges)
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool UpdateRoot(string oldPath, Design child)
|
||||||
|
{
|
||||||
|
var newPath = child.FullName();
|
||||||
|
if (string.Equals(newPath, oldPath, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Designs.Remove(oldPath);
|
||||||
|
Designs[child.FullName()] = child.Data;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateChild(string oldRootPath, string newRootPath, Design child)
|
||||||
|
{
|
||||||
|
var newPath = child.FullName();
|
||||||
|
var oldPath = $"{oldRootPath}{newPath.Remove(0, newRootPath.Length)}";
|
||||||
|
Designs.Remove(oldPath);
|
||||||
|
Designs[newPath] = child.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteAllChildren(IFileSystemBase root, bool deleteEmpty)
|
||||||
|
{
|
||||||
|
if (root is Folder f)
|
||||||
|
{
|
||||||
|
foreach (var child in f.AllLeaves(SortMode.Lexicographical))
|
||||||
|
Designs.Remove(child.FullName());
|
||||||
|
}
|
||||||
|
var fullPath = root.FullName();
|
||||||
|
root.Parent.RemoveChild(root, deleteEmpty);
|
||||||
|
Designs.Remove(fullPath);
|
||||||
|
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateAllChildren(string oldPath, IFileSystemBase root)
|
||||||
|
{
|
||||||
|
var changes = false;
|
||||||
|
switch (root)
|
||||||
|
{
|
||||||
|
case Design d:
|
||||||
|
changes |= UpdateRoot(oldPath, d);
|
||||||
|
break;
|
||||||
|
case Folder f:
|
||||||
|
{
|
||||||
|
var newRootPath = root.FullName();
|
||||||
|
if (!string.Equals(oldPath, newRootPath, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
changes = true;
|
||||||
|
foreach (var descendant in f.AllLeaves(SortMode.Lexicographical).Where(l => l is Design).Cast<Design>())
|
||||||
|
UpdateChild(oldPath, newRootPath, descendant);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes)
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveToFile()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = JsonConvert.SerializeObject(Designs, Formatting.Indented);
|
||||||
|
File.WriteAllText(_saveFile.FullName, data);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Could not write to save file {_saveFile.FullName}:\n{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadFromFile()
|
||||||
|
{
|
||||||
|
_saveFile.Refresh();
|
||||||
|
SortedList<string, CharacterSave>? designs = null;
|
||||||
|
if (_saveFile.Exists)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = File.ReadAllText(_saveFile.FullName);
|
||||||
|
designs = JsonConvert.DeserializeObject<SortedList<string, CharacterSave>>(data);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Could not load save file {_saveFile.FullName}:\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (designs == null)
|
||||||
|
{
|
||||||
|
Designs = new SortedList<string, CharacterSave>();
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Designs = designs;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildStructure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
153
Glamourer/FileSystem/FileSystem.cs
Normal file
153
Glamourer/FileSystem/FileSystem.cs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Glamourer.FileSystem
|
||||||
|
{
|
||||||
|
public class FileSystem
|
||||||
|
{
|
||||||
|
public Folder Root { get; } = Folder.CreateRoot();
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
=> Root.Children.Clear();
|
||||||
|
|
||||||
|
// Find a specific child by its path from Root.
|
||||||
|
// Returns true if the folder was found, and false if not.
|
||||||
|
// The out parameter will contain the furthest existing folder.
|
||||||
|
public bool Find(string path, out IFileSystemBase child)
|
||||||
|
{
|
||||||
|
var split = Split(path);
|
||||||
|
var folder = Root;
|
||||||
|
child = Root;
|
||||||
|
foreach (var part in split)
|
||||||
|
{
|
||||||
|
if (!folder.FindChild(part, out var c))
|
||||||
|
{
|
||||||
|
child = folder;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
child = c;
|
||||||
|
if (c is not Folder f)
|
||||||
|
return part == split.Last();
|
||||||
|
|
||||||
|
folder = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Folder CreateAllFolders(IEnumerable<string> names)
|
||||||
|
{
|
||||||
|
var last = Root;
|
||||||
|
foreach (var name in names)
|
||||||
|
last = last.FindOrCreateSubFolder(name).Item1;
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (Folder, string) CreateAllFolders(string path)
|
||||||
|
{
|
||||||
|
if (!path.Any())
|
||||||
|
return (Root, string.Empty);
|
||||||
|
|
||||||
|
var split = Split(path);
|
||||||
|
if (split.Length == 1)
|
||||||
|
return (Root, path);
|
||||||
|
|
||||||
|
return (CreateAllFolders(split.Take(split.Length - 1)), split.Last());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Rename(IFileSystemBase child, string newName)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(child, Root))
|
||||||
|
throw new InvalidOperationException("Can not rename root.");
|
||||||
|
|
||||||
|
newName = FixName(newName);
|
||||||
|
if (child.Name == newName)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (child.Parent.FindChild(newName, out var preExisting))
|
||||||
|
{
|
||||||
|
if (MergeIfFolders(child, preExisting, false))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
throw new Exception($"Can not rename {child.Name} in {child.Parent.FullName()} to {newName} because {newName} already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent = child.Parent;
|
||||||
|
parent.RemoveChildIgnoreEmpty(child);
|
||||||
|
child.Name = newName;
|
||||||
|
parent.FindOrAddChild(child);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Move(IFileSystemBase child, Folder newParent, bool deleteEmpty)
|
||||||
|
{
|
||||||
|
var oldParent = child.Parent;
|
||||||
|
if (ReferenceEquals(newParent, oldParent))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Moving into its own subfolder or itself is not allowed.
|
||||||
|
if (child.IsFolder(out var f)
|
||||||
|
&& (ReferenceEquals(newParent, f)
|
||||||
|
|| newParent.FullName().StartsWith(f.FullName(), StringComparison.InvariantCultureIgnoreCase)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (newParent.FindChild(child.Name, out var conflict))
|
||||||
|
{
|
||||||
|
if (MergeIfFolders(child, conflict, deleteEmpty))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
throw new Exception($"Can not move {child.Name} into {newParent.FullName()} because {conflict.FullName()} already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
|
oldParent.RemoveChild(child, deleteEmpty);
|
||||||
|
newParent.FindOrAddChild(child);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Merge(Folder source, Folder target, bool deleteEmpty)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(source, target))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!source.Children.Any())
|
||||||
|
{
|
||||||
|
if (deleteEmpty)
|
||||||
|
{
|
||||||
|
source.Parent.RemoveChild(source, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (source.Children.Count > 0)
|
||||||
|
Move(source.Children.First(), target, deleteEmpty); // Can throw.
|
||||||
|
|
||||||
|
source.Parent.RemoveChild(source, deleteEmpty);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MergeIfFolders(IFileSystemBase source, IFileSystemBase target, bool deleteEmpty)
|
||||||
|
{
|
||||||
|
if (source is Folder childF && target.IsFolder(out var preF))
|
||||||
|
{
|
||||||
|
Merge(childF, preF, deleteEmpty);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] Split(string path)
|
||||||
|
=> path.Split(new[]
|
||||||
|
{
|
||||||
|
'/',
|
||||||
|
}, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
private static string FixName(string name)
|
||||||
|
=> name.Replace('/', '\\');
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Glamourer/FileSystem/FileSystemImGui.cs
Normal file
57
Glamourer/FileSystem/FileSystemImGui.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
namespace Glamourer.FileSystem
|
||||||
|
{
|
||||||
|
public static class FileSystemImGui
|
||||||
|
{
|
||||||
|
public const string DraggedObjectLabel = "FSDrag";
|
||||||
|
|
||||||
|
private static unsafe bool IsDropping(string name)
|
||||||
|
=> ImGui.AcceptDragDropPayload(name).NativePtr != null;
|
||||||
|
|
||||||
|
private static IFileSystemBase? _draggedObject = null;
|
||||||
|
|
||||||
|
public static bool DragDropTarget(FileSystem fs, IFileSystemBase child, out string oldPath, out IFileSystemBase? draggedChild)
|
||||||
|
{
|
||||||
|
oldPath = string.Empty;
|
||||||
|
draggedChild = null;
|
||||||
|
var ret = false;
|
||||||
|
if (!ImGui.BeginDragDropTarget())
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
if (IsDropping(DraggedObjectLabel))
|
||||||
|
{
|
||||||
|
if (_draggedObject != null)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
oldPath = _draggedObject.FullName();
|
||||||
|
draggedChild = _draggedObject;
|
||||||
|
ret = fs.Move(_draggedObject, child.IsFolder(out var folder) ? folder : child.Parent, false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Could not drag {_draggedObject.Name} onto {child.FullName()}:\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_draggedObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndDragDropTarget();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DragDropSource(IFileSystemBase child)
|
||||||
|
{
|
||||||
|
if (!ImGui.BeginDragDropSource())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui.SetDragDropPayload(DraggedObjectLabel, IntPtr.Zero, 0);
|
||||||
|
ImGui.Text($"Moving {child.Name}...");
|
||||||
|
_draggedObject = child;
|
||||||
|
ImGui.EndDragDropSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
178
Glamourer/FileSystem/Folder.cs
Normal file
178
Glamourer/FileSystem/Folder.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Glamourer.FileSystem
|
||||||
|
{
|
||||||
|
public enum SortMode
|
||||||
|
{
|
||||||
|
FoldersFirst = 0x00,
|
||||||
|
Lexicographical = 0x01,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Folder : IFileSystemBase
|
||||||
|
{
|
||||||
|
public Folder Parent { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public readonly List<IFileSystemBase> Children = new();
|
||||||
|
|
||||||
|
public Folder(Folder parent, string name)
|
||||||
|
{
|
||||||
|
Parent = parent;
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> this.FullName();
|
||||||
|
|
||||||
|
// Return the number of all leaves with this folder in their path.
|
||||||
|
public int TotalDescendantLeaves()
|
||||||
|
{
|
||||||
|
var sum = 0;
|
||||||
|
foreach (var child in Children)
|
||||||
|
{
|
||||||
|
switch (child)
|
||||||
|
{
|
||||||
|
case Folder f:
|
||||||
|
sum += f.TotalDescendantLeaves();
|
||||||
|
break;
|
||||||
|
case Link l:
|
||||||
|
sum += l.Data is Folder fl ? fl.TotalDescendantLeaves() : 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
++sum;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all descendant mods in the specified order.
|
||||||
|
public IEnumerable<IFileSystemBase> AllLeaves(SortMode mode)
|
||||||
|
{
|
||||||
|
return GetSortedEnumerator(mode).SelectMany(f =>
|
||||||
|
{
|
||||||
|
if (f.IsFolder(out var folder))
|
||||||
|
return folder.AllLeaves(mode);
|
||||||
|
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
f,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IFileSystemBase> AllChildren(SortMode mode)
|
||||||
|
=> GetSortedEnumerator(mode);
|
||||||
|
|
||||||
|
// Get an enumerator for actually sorted objects instead of folder-first objects.
|
||||||
|
private IEnumerable<IFileSystemBase> GetSortedEnumerator(SortMode mode)
|
||||||
|
{
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case SortMode.FoldersFirst:
|
||||||
|
foreach (var child in Children.Where(c => c.IsFolder()))
|
||||||
|
yield return child;
|
||||||
|
|
||||||
|
foreach (var child in Children.Where(c => c.IsLeaf()))
|
||||||
|
yield return child;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SortMode.Lexicographical:
|
||||||
|
foreach (var child in Children)
|
||||||
|
yield return child;
|
||||||
|
|
||||||
|
break;
|
||||||
|
default: throw new InvalidEnumArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Folder CreateRoot()
|
||||||
|
=> new(null!, "");
|
||||||
|
|
||||||
|
// Find a subfolder by name. Returns true and sets folder to it if it exists.
|
||||||
|
public bool FindChild(string name, out IFileSystemBase ret)
|
||||||
|
{
|
||||||
|
var idx = Search(name);
|
||||||
|
ret = idx >= 0 ? Children[idx] : this;
|
||||||
|
return idx >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if an equivalent child to child already exists and returns its index.
|
||||||
|
// If it does not exist, inserts child as a child and returns the new index.
|
||||||
|
// Also sets this as childs parent.
|
||||||
|
public int FindOrAddChild(IFileSystemBase child)
|
||||||
|
{
|
||||||
|
var idx = Search(child);
|
||||||
|
if (idx >= 0)
|
||||||
|
return idx;
|
||||||
|
|
||||||
|
idx = ~idx;
|
||||||
|
Children.Insert(idx, child);
|
||||||
|
child.Parent = this;
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if an equivalent child to child already exists and throws if it does.
|
||||||
|
// If it does not exist, inserts child as a child and returns the new index.
|
||||||
|
// Also sets this as childs parent.
|
||||||
|
public int AddChild(IFileSystemBase child)
|
||||||
|
{
|
||||||
|
var idx = Search(child);
|
||||||
|
if (idx >= 0)
|
||||||
|
throw new Exception("Could not add child: Child of that name already exists.");
|
||||||
|
|
||||||
|
idx = ~idx;
|
||||||
|
Children.Insert(idx, child);
|
||||||
|
child.Parent = this;
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if a subfolder with the given name already exists and returns it and its index.
|
||||||
|
// If it does not exists, creates and inserts it and returns the new subfolder and its index.
|
||||||
|
public (Folder, int) FindOrCreateSubFolder(string name)
|
||||||
|
{
|
||||||
|
var subFolder = new Folder(this, name);
|
||||||
|
var idx = FindOrAddChild(subFolder);
|
||||||
|
var child = Children[idx];
|
||||||
|
if (!child.IsFolder(out var folder))
|
||||||
|
throw new Exception($"The child {name} already exists in {this.FullName()} but is not a folder.");
|
||||||
|
|
||||||
|
return (folder, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove child if it exists.
|
||||||
|
// If this folder is empty afterwards and deleteEmpty is true, remove it from its parent.
|
||||||
|
public void RemoveChild(IFileSystemBase child, bool deleteEmpty)
|
||||||
|
{
|
||||||
|
RemoveChildIgnoreEmpty(child);
|
||||||
|
if (deleteEmpty)
|
||||||
|
CheckEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckEmpty()
|
||||||
|
{
|
||||||
|
if (Children.Count == 0)
|
||||||
|
Parent?.RemoveChild(this, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a child but do not remove this folder from its parent if it is empty afterwards.
|
||||||
|
internal void RemoveChildIgnoreEmpty(IFileSystemBase folder)
|
||||||
|
{
|
||||||
|
var idx = Search(folder);
|
||||||
|
if (idx < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Children[idx].Parent = null!;
|
||||||
|
Children.RemoveAt(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Search(string name)
|
||||||
|
=> Children.BinarySearch(new FileSystemObject(name), FolderStructureComparer.Default);
|
||||||
|
|
||||||
|
private int Search(IFileSystemBase child)
|
||||||
|
=> Children.BinarySearch(child, FolderStructureComparer.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
Glamourer/FileSystem/IFolderStructure.cs
Normal file
66
Glamourer/FileSystem/IFolderStructure.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Glamourer.Designs;
|
||||||
|
|
||||||
|
namespace Glamourer.FileSystem
|
||||||
|
{
|
||||||
|
internal class FolderStructureComparer : IComparer<IFileSystemBase>
|
||||||
|
{
|
||||||
|
// Compare only the direct folder names since this is only used inside an enumeration of children of one folder.
|
||||||
|
public static int Cmp(IFileSystemBase x, IFileSystemBase y)
|
||||||
|
=> ReferenceEquals(x, y) ? 0 : string.Compare(x.Name, y.Name, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
|
public int Compare(IFileSystemBase x, IFileSystemBase y)
|
||||||
|
=> Cmp(x, y);
|
||||||
|
|
||||||
|
internal static readonly FolderStructureComparer Default = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IFileSystemBase
|
||||||
|
{
|
||||||
|
public Folder Parent { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FileSystemExtensions
|
||||||
|
{
|
||||||
|
public static string FullName(this IFileSystemBase data)
|
||||||
|
=> data.Parent?.Name.Any() ?? false ? $"{data.Parent.FullName()}/{data.Name}" : data.Name;
|
||||||
|
|
||||||
|
public static bool IsLeaf(this IFileSystemBase data)
|
||||||
|
=> data is not Folder && data is not Link { Data: Folder };
|
||||||
|
|
||||||
|
public static bool IsFolder(this IFileSystemBase data)
|
||||||
|
=> data.IsFolder(out _);
|
||||||
|
|
||||||
|
public static bool IsFolder(this IFileSystemBase data, out Folder folder)
|
||||||
|
{
|
||||||
|
switch (data)
|
||||||
|
{
|
||||||
|
case Folder f:
|
||||||
|
folder = f;
|
||||||
|
return true;
|
||||||
|
case Link { Data: Folder fl }:
|
||||||
|
folder = fl;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
folder = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class FileSystemObject : IFileSystemBase
|
||||||
|
{
|
||||||
|
public FileSystemObject(string name)
|
||||||
|
=> Name = name;
|
||||||
|
|
||||||
|
public Folder Parent { get; set; } = null!;
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public string FullName()
|
||||||
|
=> Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Glamourer/FileSystem/Link.cs
Normal file
20
Glamourer/FileSystem/Link.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Glamourer.Designs;
|
||||||
|
|
||||||
|
namespace Glamourer.FileSystem
|
||||||
|
{
|
||||||
|
public class Link : IFileSystemBase
|
||||||
|
{
|
||||||
|
public Folder Parent { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public IFileSystemBase Data { get; }
|
||||||
|
|
||||||
|
public Link(Folder parent, string name, IFileSystemBase data)
|
||||||
|
{
|
||||||
|
Parent = parent;
|
||||||
|
Name = name;
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
<RootNamespace>Glamourer</RootNamespace>
|
<RootNamespace>Glamourer</RootNamespace>
|
||||||
<AssemblyName>Glamourer</AssemblyName>
|
<AssemblyName>Glamourer</AssemblyName>
|
||||||
<FileVersion>0.0.2.0</FileVersion>
|
<FileVersion>0.0.3.0</FileVersion>
|
||||||
<AssemblyVersion>0.0.2.0</AssemblyVersion>
|
<AssemblyVersion>0.0.3.0</AssemblyVersion>
|
||||||
<Company>SoftOtter</Company>
|
<Company>SoftOtter</Company>
|
||||||
<Product>Glamourer</Product>
|
<Product>Glamourer</Product>
|
||||||
<Copyright>Copyright © 2020</Copyright>
|
<Copyright>Copyright © 2020</Copyright>
|
||||||
|
|
@ -91,13 +91,13 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Glamourer.json">
|
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
|
<None Update="Glamourer.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||||
<Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll, $(TargetDir)Penumbra.Api.dll, $(TargetDir)Penumbra.PlayerWatch.dll $(SolutionDir)$(SolutionName).zip" />
|
<Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll, $(TargetDir)Penumbra.Api.dll, $(TargetDir)Penumbra.PlayerWatch.dll $(SolutionDir)$(SolutionName).zip" />
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"Name": "Glamourer",
|
"Name": "Glamourer",
|
||||||
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
|
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
|
||||||
"InternalName": "Glamourer",
|
"InternalName": "Glamourer",
|
||||||
"AssemblyVersion": "0.0.2.0",
|
"AssemblyVersion": "0.0.3.0",
|
||||||
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
|
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"DalamudApiLevel": 3,
|
"DalamudApiLevel": 3,
|
||||||
|
|
|
||||||
|
|
@ -2,397 +2,55 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using Dalamud.Game.ClientState.Actors;
|
using Dalamud.Game.ClientState.Actors;
|
||||||
using Dalamud.Game.ClientState.Actors.Types;
|
using Glamourer.Designs;
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.Customization;
|
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Lumina.Excel.GeneratedSheets;
|
|
||||||
using Penumbra.Api;
|
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
|
||||||
using Penumbra.PlayerWatch;
|
|
||||||
using Race = Penumbra.GameData.Enums.Race;
|
|
||||||
|
|
||||||
namespace Glamourer.Gui
|
namespace Glamourer.Gui
|
||||||
{
|
{
|
||||||
internal partial class Interface
|
|
||||||
{
|
|
||||||
// Push the stain color to type and if it is too bright, turn the text color black.
|
|
||||||
// Return number of pushed styles.
|
|
||||||
private static int PushColor(Stain stain, ImGuiCol type = ImGuiCol.Button)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(type, stain.RgbaColor);
|
|
||||||
if (stain.Intensity > 127)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, 0xFF101010);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update actors without triggering PlayerWatcher Events,
|
|
||||||
// then manually redraw using Penumbra.
|
|
||||||
public void UpdateActors(Actor actor)
|
|
||||||
{
|
|
||||||
var newEquip = _playerWatcher.UpdateActorWithoutEvent(actor);
|
|
||||||
GlamourerPlugin.Penumbra?.RedrawActor(actor, RedrawType.WithSettings);
|
|
||||||
|
|
||||||
// Special case for carrying over changes to the gPose actor to the regular player actor, too.
|
|
||||||
var gPose = _actors[GPoseActorId];
|
|
||||||
var player = _actors[0];
|
|
||||||
if (gPose != null && actor.Address == gPose.Address && player != null)
|
|
||||||
newEquip.Write(player.Address);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go through a whole customization struct and fix up all settings that need fixing.
|
|
||||||
private static void FixUpAttributes(LazyCustomization customization)
|
|
||||||
{
|
|
||||||
var set = GlamourerPlugin.Customization.GetList(customization.Value.Clan, customization.Value.Gender);
|
|
||||||
foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId)))
|
|
||||||
{
|
|
||||||
switch (id)
|
|
||||||
{
|
|
||||||
case CustomizationId.Race: break;
|
|
||||||
case CustomizationId.Clan: break;
|
|
||||||
case CustomizationId.BodyType: break;
|
|
||||||
case CustomizationId.Gender: break;
|
|
||||||
case CustomizationId.FacialFeaturesTattoos: break;
|
|
||||||
case CustomizationId.HighlightsOnFlag: break;
|
|
||||||
case CustomizationId.Face:
|
|
||||||
if (customization.Value.Race != Race.Hrothgar)
|
|
||||||
goto default;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
var count = set.Count(id);
|
|
||||||
if (customization.Value[id] >= count)
|
|
||||||
if (count == 0)
|
|
||||||
customization.Value[id] = 0;
|
|
||||||
else
|
|
||||||
customization.Value[id] = set.Data(id, 0).Value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change a race and fix up all required customizations afterwards.
|
|
||||||
private static bool ChangeRace(LazyCustomization customization, SubRace clan)
|
|
||||||
{
|
|
||||||
if (clan == customization.Value.Clan)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var race = clan.ToRace();
|
|
||||||
customization.Value.Race = race;
|
|
||||||
customization.Value.Clan = clan;
|
|
||||||
|
|
||||||
customization.Value.Gender = race switch
|
|
||||||
{
|
|
||||||
Race.Hrothgar => Gender.Male,
|
|
||||||
Race.Viera => Gender.Female,
|
|
||||||
_ => customization.Value.Gender,
|
|
||||||
};
|
|
||||||
|
|
||||||
FixUpAttributes(customization);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change a gender and fix up all required customizations afterwards.
|
|
||||||
private static bool ChangeGender(LazyCustomization customization, Gender gender)
|
|
||||||
{
|
|
||||||
if (gender == customization.Value.Gender)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
customization.Value.Gender = gender;
|
|
||||||
FixUpAttributes(customization);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class Interface
|
|
||||||
{
|
|
||||||
private const float ColorButtonWidth = 22.5f;
|
|
||||||
private const float ColorComboWidth = 140f;
|
|
||||||
private const float ItemComboWidth = 300f;
|
|
||||||
|
|
||||||
private ComboWithFilter<Stain> CreateDefaultStainCombo(IReadOnlyList<Stain> stains)
|
|
||||||
=> new("##StainCombo", ColorComboWidth, ColorButtonWidth, stains,
|
|
||||||
s => s.Name.ToString())
|
|
||||||
{
|
|
||||||
Flags = ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge,
|
|
||||||
PreList = () =>
|
|
||||||
{
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
|
|
||||||
},
|
|
||||||
PostList = () => { ImGui.PopStyleVar(3); },
|
|
||||||
CreateSelectable = s =>
|
|
||||||
{
|
|
||||||
var push = PushColor(s);
|
|
||||||
var ret = ImGui.Button($"{s.Name}##Stain{(byte) s.RowIndex}",
|
|
||||||
Vector2.UnitX * (ColorComboWidth - ImGui.GetStyle().ScrollbarSize));
|
|
||||||
ImGui.PopStyleColor(push);
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
ItemsAtOnce = 12,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
private ComboWithFilter<Item> CreateItemCombo(EquipSlot slot, IReadOnlyList<Item> items)
|
|
||||||
=> new($"{_equipSlotNames[slot]}##Equip", ItemComboWidth, ItemComboWidth, items, i => i.Name)
|
|
||||||
{
|
|
||||||
Flags = ImGuiComboFlags.HeightLarge,
|
|
||||||
};
|
|
||||||
|
|
||||||
private (ComboWithFilter<Item>, ComboWithFilter<Stain>) CreateCombos(EquipSlot slot, IReadOnlyList<Item> items,
|
|
||||||
ComboWithFilter<Stain> defaultStain)
|
|
||||||
=> (CreateItemCombo(slot, items), new ComboWithFilter<Stain>($"##{slot}Stain", defaultStain));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class Interface
|
|
||||||
{
|
|
||||||
private bool DrawStainSelector(ComboWithFilter<Stain> stainCombo, EquipSlot slot, StainId stainIdx)
|
|
||||||
{
|
|
||||||
stainCombo.PostPreview = null;
|
|
||||||
if (_stains.TryGetValue((byte) stainIdx, out var stain))
|
|
||||||
{
|
|
||||||
var previewPush = PushColor(stain, ImGuiCol.FrameBg);
|
|
||||||
stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stainCombo.Draw(string.Empty, out var newStain) && _player != null && !newStain.RowIndex.Equals(stainIdx))
|
|
||||||
{
|
|
||||||
newStain.Write(_player.Address, slot);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawItemSelector(ComboWithFilter<Item> equipCombo, Lumina.Excel.GeneratedSheets.Item? item)
|
|
||||||
{
|
|
||||||
var currentName = item?.Name.ToString() ?? "Nothing";
|
|
||||||
if (equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && _player != null && newItem.Base.RowId != item?.RowId)
|
|
||||||
{
|
|
||||||
newItem.Write(_player.Address);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawEquip(EquipSlot slot, ActorArmor equip)
|
|
||||||
{
|
|
||||||
var (equipCombo, stainCombo) = _combos[slot];
|
|
||||||
|
|
||||||
var ret = DrawStainSelector(stainCombo, slot, equip.Stain);
|
|
||||||
ImGui.SameLine();
|
|
||||||
var item = _identifier.Identify(equip.Set, new WeaponType(), equip.Variant, slot);
|
|
||||||
ret |= DrawItemSelector(equipCombo, item);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawWeapon(EquipSlot slot, ActorWeapon weapon)
|
|
||||||
{
|
|
||||||
var (equipCombo, stainCombo) = _combos[slot];
|
|
||||||
|
|
||||||
var ret = DrawStainSelector(stainCombo, slot, weapon.Stain);
|
|
||||||
ImGui.SameLine();
|
|
||||||
var item = _identifier.Identify(weapon.Set, weapon.Type, weapon.Variant, slot);
|
|
||||||
ret |= DrawItemSelector(equipCombo, item);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class Interface
|
|
||||||
{
|
|
||||||
private static bool DrawColorPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value)
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
if (!ImGui.BeginPopup(label, ImGuiWindowFlags.AlwaysAutoResize))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var ret = false;
|
|
||||||
var count = set.Count(id);
|
|
||||||
using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
|
||||||
.PushStyle(ImGuiStyleVar.FrameRounding, 0);
|
|
||||||
for (var i = 0; i < count; ++i)
|
|
||||||
{
|
|
||||||
var custom = set.Data(id, i);
|
|
||||||
if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)))
|
|
||||||
{
|
|
||||||
value = custom;
|
|
||||||
ret = true;
|
|
||||||
ImGui.CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i % 8 != 7)
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndPopup();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Vector2 _iconSize = Vector2.Zero;
|
|
||||||
private Vector2 _actualIconSize = Vector2.Zero;
|
|
||||||
private float _raceSelectorWidth = 0;
|
|
||||||
private float _inputIntSize = 0;
|
|
||||||
private float _comboSelectorSize = 0;
|
|
||||||
private float _percentageSize = 0;
|
|
||||||
private float _itemComboWidth = 0;
|
|
||||||
|
|
||||||
private bool InputInt(string label, ref int value, int minValue, int maxValue)
|
|
||||||
{
|
|
||||||
var ret = false;
|
|
||||||
var tmp = value + 1;
|
|
||||||
ImGui.SetNextItemWidth(_inputIntSize);
|
|
||||||
if (ImGui.InputInt(label, ref tmp, 1) && tmp != value + 1 && tmp >= minValue && tmp <= maxValue)
|
|
||||||
{
|
|
||||||
value = tmp - 1;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip($"Input Range: [{minValue}, {maxValue}]");
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (int, Customization.Customization) GetCurrentCustomization(LazyCustomization customization, CustomizationId id,
|
|
||||||
CustomizationSet set)
|
|
||||||
{
|
|
||||||
var current = set.DataByValue(id, customization.Value[id], out var custom);
|
|
||||||
if (current < 0)
|
|
||||||
{
|
|
||||||
PluginLog.Warning($"Read invalid customization value {customization.Value[id]} for {id}.");
|
|
||||||
current = 0;
|
|
||||||
custom = set.Data(id, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (current, custom!.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawColorPicker(string label, string tooltip, LazyCustomization customization, CustomizationId id,
|
|
||||||
CustomizationSet set)
|
|
||||||
{
|
|
||||||
var ret = false;
|
|
||||||
var count = set.Count(id);
|
|
||||||
|
|
||||||
var (current, custom) = GetCurrentCustomization(customization, id, set);
|
|
||||||
|
|
||||||
var popupName = $"Color Picker##{id}";
|
|
||||||
if (ImGui.ColorButton($"{current + 1}##color_{id}", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None,
|
|
||||||
_actualIconSize))
|
|
||||||
ImGui.OpenPopup(popupName);
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
|
|
||||||
using (var group = ImGuiRaii.NewGroup())
|
|
||||||
{
|
|
||||||
if (InputInt($"##text_{id}", ref current, 1, count))
|
|
||||||
{
|
|
||||||
customization.Value[id] = set.Data(id, current - 1).Value;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ImGui.Text(label);
|
|
||||||
if (tooltip.Any() && ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(tooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!DrawColorPickerPopup(popupName, set, id, out var newCustom))
|
|
||||||
return ret;
|
|
||||||
|
|
||||||
customization.Value[id] = newCustom.Value;
|
|
||||||
ret = true;
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal partial class Interface : IDisposable
|
internal partial class Interface : IDisposable
|
||||||
{
|
{
|
||||||
|
public const float SelectorWidth = 200;
|
||||||
|
public const float MinWindowWidth = 675;
|
||||||
public const int GPoseActorId = 201;
|
public const int GPoseActorId = 201;
|
||||||
private const string PluginName = "Glamourer";
|
private const string PluginName = "Glamourer";
|
||||||
private readonly string _glamourerHeader;
|
private readonly string _glamourerHeader;
|
||||||
|
|
||||||
private readonly IReadOnlyDictionary<byte, Stain> _stains;
|
private readonly IReadOnlyDictionary<byte, Stain> _stains;
|
||||||
private readonly IReadOnlyDictionary<EquipSlot, List<Item>> _equip;
|
|
||||||
private readonly ActorTable _actors;
|
private readonly ActorTable _actors;
|
||||||
private readonly IObjectIdentifier _identifier;
|
private readonly IObjectIdentifier _identifier;
|
||||||
private readonly Dictionary<EquipSlot, (ComboWithFilter<Item>, ComboWithFilter<Stain>)> _combos;
|
private readonly Dictionary<EquipSlot, (ComboWithFilter<Item>, ComboWithFilter<Stain>)> _combos;
|
||||||
private readonly IPlayerWatcher _playerWatcher;
|
|
||||||
private readonly ImGuiScene.TextureWrap? _legacyTattooIcon;
|
private readonly ImGuiScene.TextureWrap? _legacyTattooIcon;
|
||||||
private readonly Dictionary<EquipSlot, string> _equipSlotNames;
|
private readonly Dictionary<EquipSlot, string> _equipSlotNames;
|
||||||
|
private readonly DesignManager _designs;
|
||||||
|
private readonly GlamourerPlugin _plugin;
|
||||||
|
|
||||||
private bool _visible = false;
|
private bool _visible = false;
|
||||||
|
private bool _inGPose = false;
|
||||||
|
|
||||||
private Actor? _player;
|
public Interface(GlamourerPlugin plugin)
|
||||||
|
|
||||||
private static ImGuiScene.TextureWrap? GetLegacyTattooIcon()
|
|
||||||
{
|
|
||||||
using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw");
|
|
||||||
if (resource != null)
|
|
||||||
{
|
|
||||||
var rawImage = new byte[resource.Length];
|
|
||||||
resource.Read(rawImage, 0, (int) resource.Length);
|
|
||||||
return GlamourerPlugin.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Dictionary<EquipSlot, string> GetEquipSlotNames()
|
|
||||||
{
|
|
||||||
var sheet = GlamourerPlugin.PluginInterface.Data.GetExcelSheet<Addon>();
|
|
||||||
var ret = new Dictionary<EquipSlot, string>(12)
|
|
||||||
{
|
|
||||||
[EquipSlot.MainHand] = sheet.GetRow(738)?.Text.ToString() ?? "Main Hand",
|
|
||||||
[EquipSlot.OffHand] = sheet.GetRow(739)?.Text.ToString() ?? "Off Hand",
|
|
||||||
[EquipSlot.Head] = sheet.GetRow(740)?.Text.ToString() ?? "Head",
|
|
||||||
[EquipSlot.Body] = sheet.GetRow(741)?.Text.ToString() ?? "Body",
|
|
||||||
[EquipSlot.Hands] = sheet.GetRow(750)?.Text.ToString() ?? "Hands",
|
|
||||||
[EquipSlot.Legs] = sheet.GetRow(742)?.Text.ToString() ?? "Legs",
|
|
||||||
[EquipSlot.Feet] = sheet.GetRow(744)?.Text.ToString() ?? "Feet",
|
|
||||||
[EquipSlot.Ears] = sheet.GetRow(745)?.Text.ToString() ?? "Ears",
|
|
||||||
[EquipSlot.Neck] = sheet.GetRow(746)?.Text.ToString() ?? "Neck",
|
|
||||||
[EquipSlot.Wrists] = sheet.GetRow(747)?.Text.ToString() ?? "Wrists",
|
|
||||||
[EquipSlot.RFinger] = sheet.GetRow(748)?.Text.ToString() ?? "Right Ring",
|
|
||||||
[EquipSlot.LFinger] = sheet.GetRow(749)?.Text.ToString() ?? "Left Ring",
|
|
||||||
};
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Interface()
|
|
||||||
{
|
{
|
||||||
|
_plugin = plugin;
|
||||||
|
_designs = plugin.Designs;
|
||||||
_glamourerHeader = GlamourerPlugin.Version.Length > 0
|
_glamourerHeader = GlamourerPlugin.Version.Length > 0
|
||||||
? $"{PluginName} v{GlamourerPlugin.Version}###{PluginName}Main"
|
? $"{PluginName} v{GlamourerPlugin.Version}###{PluginName}Main"
|
||||||
: $"{PluginName}###{PluginName}Main";
|
: $"{PluginName}###{PluginName}Main";
|
||||||
|
GlamourerPlugin.PluginInterface.UiBuilder.DisableGposeUiHide = true;
|
||||||
GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi += Draw;
|
GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi += Draw;
|
||||||
GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi += ToggleVisibility;
|
GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi += ToggleVisibility;
|
||||||
|
|
||||||
_equipSlotNames = GetEquipSlotNames();
|
_equipSlotNames = GetEquipSlotNames();
|
||||||
|
|
||||||
_stains = GameData.Stains(GlamourerPlugin.PluginInterface);
|
_stains = GameData.Stains(GlamourerPlugin.PluginInterface);
|
||||||
_equip = GameData.ItemsBySlot(GlamourerPlugin.PluginInterface);
|
|
||||||
_identifier = Penumbra.GameData.GameData.GetIdentifier(GlamourerPlugin.PluginInterface);
|
_identifier = Penumbra.GameData.GameData.GetIdentifier(GlamourerPlugin.PluginInterface);
|
||||||
_actors = GlamourerPlugin.PluginInterface.ClientState.Actors;
|
_actors = GlamourerPlugin.PluginInterface.ClientState.Actors;
|
||||||
_playerWatcher = PlayerWatchFactory.Create(GlamourerPlugin.PluginInterface);
|
|
||||||
|
|
||||||
var stainCombo = CreateDefaultStainCombo(_stains.Values.ToArray());
|
var stainCombo = CreateDefaultStainCombo(_stains.Values.ToArray());
|
||||||
|
|
||||||
_combos = _equip.ToDictionary(kvp => kvp.Key, kvp => CreateCombos(kvp.Key, kvp.Value, stainCombo));
|
var equip = GameData.ItemsBySlot(GlamourerPlugin.PluginInterface);
|
||||||
|
_combos = equip.ToDictionary(kvp => kvp.Key, kvp => CreateCombos(kvp.Key, kvp.Value, stainCombo));
|
||||||
_legacyTattooIcon = GetLegacyTattooIcon();
|
_legacyTattooIcon = GetLegacyTattooIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,464 +60,27 @@ namespace Glamourer.Gui
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_legacyTattooIcon?.Dispose();
|
_legacyTattooIcon?.Dispose();
|
||||||
_playerWatcher?.Dispose();
|
|
||||||
GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi -= Draw;
|
GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi -= Draw;
|
||||||
GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi -= ToggleVisibility;
|
GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi -= ToggleVisibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _currentActorName = "";
|
|
||||||
|
|
||||||
private SubRace _currentSubRace = SubRace.Midlander;
|
|
||||||
private Gender _currentGender = Gender.Male;
|
|
||||||
|
|
||||||
private bool DrawListSelector(string label, string tooltip, LazyCustomization customization, CustomizationId id,
|
|
||||||
CustomizationSet set)
|
|
||||||
{
|
|
||||||
using var bigGroup = ImGuiRaii.NewGroup();
|
|
||||||
var ret = false;
|
|
||||||
int current = customization.Value[id];
|
|
||||||
var count = set.Count(id);
|
|
||||||
|
|
||||||
ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale);
|
|
||||||
if (ImGui.BeginCombo($"##combo_{id}", $"{set.Option(id)} #{current + 1}"))
|
|
||||||
{
|
|
||||||
for (var i = 0; i < count; ++i)
|
|
||||||
{
|
|
||||||
if (ImGui.Selectable($"{set.Option(id)} #{i + 1}##combo", i == current) && i != current)
|
|
||||||
{
|
|
||||||
customization.Value[id] = (byte) i;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndCombo();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (InputInt($"##text_{id}", ref current, 1, count))
|
|
||||||
{
|
|
||||||
customization.Value[id] = set.Data(id, current).Value;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Text(label);
|
|
||||||
if (tooltip.Any() && ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(tooltip);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly Vector4 NoColor = new(1f, 1f, 1f, 1f);
|
|
||||||
private static readonly Vector4 RedColor = new(0.6f, 0.3f, 0.3f, 1f);
|
|
||||||
|
|
||||||
private bool DrawMultiSelector(LazyCustomization customization, CustomizationSet set)
|
|
||||||
{
|
|
||||||
using var bigGroup = ImGuiRaii.NewGroup();
|
|
||||||
var ret = false;
|
|
||||||
var count = set.Count(CustomizationId.FacialFeaturesTattoos);
|
|
||||||
using (var raii = ImGuiRaii.NewGroup())
|
|
||||||
{
|
|
||||||
for (var i = 0; i < count; ++i)
|
|
||||||
{
|
|
||||||
var enabled = customization.Value.FacialFeature(i);
|
|
||||||
var feature = set.FacialFeature(set.Race == Race.Hrothgar ? customization.Value.Hairstyle : customization.Value.Face, i);
|
|
||||||
var icon = i == count - 1
|
|
||||||
? _legacyTattooIcon ?? GlamourerPlugin.Customization.GetIcon(feature.IconId)
|
|
||||||
: GlamourerPlugin.Customization.GetIcon(feature.IconId);
|
|
||||||
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int) ImGui.GetStyle().FramePadding.X,
|
|
||||||
Vector4.Zero,
|
|
||||||
enabled ? NoColor : RedColor))
|
|
||||||
{
|
|
||||||
ret = true;
|
|
||||||
customization.Value.FacialFeature(i, !enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
using var tt = ImGuiRaii.NewTooltip();
|
|
||||||
ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i % 4 != 3)
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
using var group = ImGuiRaii.NewGroup();
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() + 3 * ImGui.GetStyle().ItemSpacing.Y / 2);
|
|
||||||
int value = customization.Value[CustomizationId.FacialFeaturesTattoos];
|
|
||||||
if (InputInt($"##{CustomizationId.FacialFeaturesTattoos}", ref value, 1, 256))
|
|
||||||
{
|
|
||||||
customization.Value[CustomizationId.FacialFeaturesTattoos] = (byte) value;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Text(set.Option(CustomizationId.FacialFeaturesTattoos));
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawIconPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value)
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
if (!ImGui.BeginPopup(label, ImGuiWindowFlags.AlwaysAutoResize))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var ret = false;
|
|
||||||
var count = set.Count(id);
|
|
||||||
using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
|
||||||
.PushStyle(ImGuiStyleVar.FrameRounding, 0);
|
|
||||||
for (var i = 0; i < count; ++i)
|
|
||||||
{
|
|
||||||
var custom = set.Data(id, i);
|
|
||||||
var icon = GlamourerPlugin.Customization.GetIcon(custom.IconId);
|
|
||||||
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
|
|
||||||
{
|
|
||||||
value = custom;
|
|
||||||
ret = true;
|
|
||||||
ImGui.CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
using var tt = ImGuiRaii.NewTooltip();
|
|
||||||
ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i % 8 != 7)
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndPopup();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawIconSelector(string label, string tooltip, LazyCustomization customization, CustomizationId id,
|
|
||||||
CustomizationSet set)
|
|
||||||
{
|
|
||||||
using var bigGroup = ImGuiRaii.NewGroup();
|
|
||||||
var ret = false;
|
|
||||||
var count = set.Count(id);
|
|
||||||
|
|
||||||
var current = set.DataByValue(id, customization.Value[id], out var custom);
|
|
||||||
if (current < 0)
|
|
||||||
{
|
|
||||||
PluginLog.Warning($"Read invalid customization value {customization.Value[id]} for {id}.");
|
|
||||||
current = 0;
|
|
||||||
custom = set.Data(id, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var popupName = $"Style Picker##{id}";
|
|
||||||
var icon = GlamourerPlugin.Customization.GetIcon(custom!.Value.IconId);
|
|
||||||
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
|
|
||||||
ImGui.OpenPopup(popupName);
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
using var tt = ImGuiRaii.NewTooltip();
|
|
||||||
ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
using var group = ImGuiRaii.NewGroup();
|
|
||||||
if (InputInt($"##text_{id}", ref current, 1, count))
|
|
||||||
{
|
|
||||||
customization.Value[id] = set.Data(id, current).Value;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DrawIconPickerPopup(popupName, set, id, out var newCustom))
|
|
||||||
{
|
|
||||||
customization.Value[id] = newCustom.Value;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Text(label);
|
|
||||||
if (tooltip.Any() && ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(tooltip);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private bool DrawPercentageSelector(string label, string tooltip, LazyCustomization customization, CustomizationId id,
|
|
||||||
CustomizationSet set)
|
|
||||||
{
|
|
||||||
using var bigGroup = ImGuiRaii.NewGroup();
|
|
||||||
var ret = false;
|
|
||||||
int value = customization.Value[id];
|
|
||||||
var count = set.Count(id);
|
|
||||||
ImGui.SetNextItemWidth(_percentageSize * ImGui.GetIO().FontGlobalScale);
|
|
||||||
if (ImGui.SliderInt($"##slider_{id}", ref value, 0, count - 1, "") && value != customization.Value[id])
|
|
||||||
{
|
|
||||||
customization.Value[id] = (byte) value;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
--value;
|
|
||||||
if (InputInt($"##input_{id}", ref value, 0, count - 1))
|
|
||||||
{
|
|
||||||
customization.Value[id] = (byte) (value + 1);
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Text(label);
|
|
||||||
if (tooltip.Any() && ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(tooltip);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ClanName(SubRace race, Gender gender)
|
|
||||||
{
|
|
||||||
if (gender == Gender.Female)
|
|
||||||
return race switch
|
|
||||||
{
|
|
||||||
SubRace.Midlander => GlamourerPlugin.Customization.GetName(CustomName.MidlanderM),
|
|
||||||
SubRace.Highlander => GlamourerPlugin.Customization.GetName(CustomName.HighlanderM),
|
|
||||||
SubRace.Wildwood => GlamourerPlugin.Customization.GetName(CustomName.WildwoodM),
|
|
||||||
SubRace.Duskwight => GlamourerPlugin.Customization.GetName(CustomName.DuskwightM),
|
|
||||||
SubRace.Plainsfolk => GlamourerPlugin.Customization.GetName(CustomName.PlainsfolkM),
|
|
||||||
SubRace.Dunesfolk => GlamourerPlugin.Customization.GetName(CustomName.DunesfolkM),
|
|
||||||
SubRace.SeekerOfTheSun => GlamourerPlugin.Customization.GetName(CustomName.SeekerOfTheSunM),
|
|
||||||
SubRace.KeeperOfTheMoon => GlamourerPlugin.Customization.GetName(CustomName.KeeperOfTheMoonM),
|
|
||||||
SubRace.Seawolf => GlamourerPlugin.Customization.GetName(CustomName.SeawolfM),
|
|
||||||
SubRace.Hellsguard => GlamourerPlugin.Customization.GetName(CustomName.HellsguardM),
|
|
||||||
SubRace.Raen => GlamourerPlugin.Customization.GetName(CustomName.RaenM),
|
|
||||||
SubRace.Xaela => GlamourerPlugin.Customization.GetName(CustomName.XaelaM),
|
|
||||||
SubRace.Helion => GlamourerPlugin.Customization.GetName(CustomName.HelionM),
|
|
||||||
SubRace.Lost => GlamourerPlugin.Customization.GetName(CustomName.LostM),
|
|
||||||
SubRace.Rava => GlamourerPlugin.Customization.GetName(CustomName.RavaF),
|
|
||||||
SubRace.Veena => GlamourerPlugin.Customization.GetName(CustomName.VeenaF),
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
|
|
||||||
};
|
|
||||||
|
|
||||||
return race switch
|
|
||||||
{
|
|
||||||
SubRace.Midlander => GlamourerPlugin.Customization.GetName(CustomName.MidlanderF),
|
|
||||||
SubRace.Highlander => GlamourerPlugin.Customization.GetName(CustomName.HighlanderF),
|
|
||||||
SubRace.Wildwood => GlamourerPlugin.Customization.GetName(CustomName.WildwoodF),
|
|
||||||
SubRace.Duskwight => GlamourerPlugin.Customization.GetName(CustomName.DuskwightF),
|
|
||||||
SubRace.Plainsfolk => GlamourerPlugin.Customization.GetName(CustomName.PlainsfolkF),
|
|
||||||
SubRace.Dunesfolk => GlamourerPlugin.Customization.GetName(CustomName.DunesfolkF),
|
|
||||||
SubRace.SeekerOfTheSun => GlamourerPlugin.Customization.GetName(CustomName.SeekerOfTheSunF),
|
|
||||||
SubRace.KeeperOfTheMoon => GlamourerPlugin.Customization.GetName(CustomName.KeeperOfTheMoonF),
|
|
||||||
SubRace.Seawolf => GlamourerPlugin.Customization.GetName(CustomName.SeawolfF),
|
|
||||||
SubRace.Hellsguard => GlamourerPlugin.Customization.GetName(CustomName.HellsguardF),
|
|
||||||
SubRace.Raen => GlamourerPlugin.Customization.GetName(CustomName.RaenF),
|
|
||||||
SubRace.Xaela => GlamourerPlugin.Customization.GetName(CustomName.XaelaF),
|
|
||||||
SubRace.Helion => GlamourerPlugin.Customization.GetName(CustomName.HelionM),
|
|
||||||
SubRace.Lost => GlamourerPlugin.Customization.GetName(CustomName.LostM),
|
|
||||||
SubRace.Rava => GlamourerPlugin.Customization.GetName(CustomName.RavaF),
|
|
||||||
SubRace.Veena => GlamourerPlugin.Customization.GetName(CustomName.VeenaF),
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawRaceSelector(LazyCustomization customization)
|
|
||||||
{
|
|
||||||
using var group = ImGuiRaii.NewGroup();
|
|
||||||
var ret = false;
|
|
||||||
_currentSubRace = customization.Value.Clan;
|
|
||||||
ImGui.SetNextItemWidth(_raceSelectorWidth);
|
|
||||||
if (ImGui.BeginCombo("##subRaceCombo", ClanName(_currentSubRace, customization.Value.Gender)))
|
|
||||||
{
|
|
||||||
for (var i = 0; i < (int) SubRace.Veena; ++i)
|
|
||||||
{
|
|
||||||
if (ImGui.Selectable(ClanName((SubRace) i + 1, customization.Value.Gender), (int) _currentSubRace == i + 1))
|
|
||||||
{
|
|
||||||
_currentSubRace = (SubRace) i + 1;
|
|
||||||
ret |= ChangeRace(customization, _currentSubRace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndCombo();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Text(
|
|
||||||
$"{GlamourerPlugin.Customization.GetName(CustomName.Gender)} & {GlamourerPlugin.Customization.GetName(CustomName.Clan)}");
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawGenderSelector(LazyCustomization customization)
|
|
||||||
{
|
|
||||||
var ret = false;
|
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
|
||||||
var icon = _currentGender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus;
|
|
||||||
var restricted = false;
|
|
||||||
if (customization.Value.Race == Race.Viera)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.25f);
|
|
||||||
icon = FontAwesomeIcon.VenusDouble;
|
|
||||||
restricted = true;
|
|
||||||
}
|
|
||||||
else if (customization.Value.Race == Race.Hrothgar)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.25f);
|
|
||||||
icon = FontAwesomeIcon.MarsDouble;
|
|
||||||
restricted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.Button(icon.ToIconString(), _actualIconSize) && !restricted)
|
|
||||||
{
|
|
||||||
_currentGender = _currentGender == Gender.Male ? Gender.Female : Gender.Male;
|
|
||||||
ret = ChangeGender(customization, _currentGender);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (restricted)
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
ImGui.PopFont();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawPicker(CustomizationSet set, CustomizationId id, LazyCustomization customization)
|
|
||||||
{
|
|
||||||
if (!set.IsAvailable(id))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
switch (set.Type(id))
|
|
||||||
{
|
|
||||||
case CharaMakeParams.MenuType.ColorPicker: return DrawColorPicker(set.OptionName[(int) id], "", customization, id, set);
|
|
||||||
case CharaMakeParams.MenuType.ListSelector: return DrawListSelector(set.OptionName[(int) id], "", customization, id, set);
|
|
||||||
case CharaMakeParams.MenuType.IconSelector: return DrawIconSelector(set.OptionName[(int) id], "", customization, id, set);
|
|
||||||
case CharaMakeParams.MenuType.MultiIconSelector: return DrawMultiSelector(customization, set);
|
|
||||||
case CharaMakeParams.MenuType.Percentage: return DrawPercentageSelector(set.OptionName[(int) id], "", customization, id, set);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly CustomizationId[] AllCustomizations = (CustomizationId[]) Enum.GetValues(typeof(CustomizationId));
|
|
||||||
|
|
||||||
private bool DrawStuff(LazyCustomization x)
|
|
||||||
{
|
|
||||||
_currentSubRace = x.Value.Clan;
|
|
||||||
_currentGender = x.Value.Gender;
|
|
||||||
var ret = DrawGenderSelector(x);
|
|
||||||
ImGui.SameLine();
|
|
||||||
ret |= DrawRaceSelector(x);
|
|
||||||
|
|
||||||
var set = GlamourerPlugin.Customization.GetList(_currentSubRace, _currentGender);
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.Percentage))
|
|
||||||
ret |= DrawPicker(set, id, x);
|
|
||||||
|
|
||||||
var odd = true;
|
|
||||||
foreach (var id in AllCustomizations.Where((c, i) => set.Type(c) == CharaMakeParams.MenuType.IconSelector))
|
|
||||||
{
|
|
||||||
ret |= DrawPicker(set, id, x);
|
|
||||||
if (odd)
|
|
||||||
ImGui.SameLine();
|
|
||||||
odd = !odd;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!odd)
|
|
||||||
ImGui.NewLine();
|
|
||||||
|
|
||||||
ret |= DrawPicker(set, CustomizationId.FacialFeaturesTattoos, x);
|
|
||||||
|
|
||||||
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.ListSelector))
|
|
||||||
ret |= DrawPicker(set, id, x);
|
|
||||||
|
|
||||||
odd = true;
|
|
||||||
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.ColorPicker))
|
|
||||||
{
|
|
||||||
ret |= DrawPicker(set, id, x);
|
|
||||||
if (odd)
|
|
||||||
ImGui.SameLine();
|
|
||||||
odd = !odd;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!odd)
|
|
||||||
ImGui.NewLine();
|
|
||||||
|
|
||||||
var tmp = x.Value.HighlightsOn;
|
|
||||||
if (ImGui.Checkbox(set.Option(CustomizationId.HighlightsOnFlag), ref tmp) && tmp != x.Value.HighlightsOn)
|
|
||||||
{
|
|
||||||
x.Value.HighlightsOn = tmp;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var xPos = _inputIntSize + _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
|
|
||||||
ImGui.SameLine(xPos);
|
|
||||||
tmp = x.Value.FacePaintReversed;
|
|
||||||
if (ImGui.Checkbox($"{GlamourerPlugin.Customization.GetName(CustomName.Reverse)} {set.Option(CustomizationId.FacePaint)}", ref tmp)
|
|
||||||
&& tmp != x.Value.FacePaintReversed)
|
|
||||||
{
|
|
||||||
x.Value.FacePaintReversed = tmp;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp = x.Value.SmallIris;
|
|
||||||
if (ImGui.Checkbox($"{GlamourerPlugin.Customization.GetName(CustomName.IrisSmall)} {set.Option(CustomizationId.EyeColorL)}",
|
|
||||||
ref tmp)
|
|
||||||
&& tmp != x.Value.SmallIris)
|
|
||||||
{
|
|
||||||
x.Value.SmallIris = tmp;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x.Value.Race != Race.Hrothgar)
|
|
||||||
{
|
|
||||||
tmp = x.Value.Lipstick;
|
|
||||||
ImGui.SameLine(xPos);
|
|
||||||
if (ImGui.Checkbox(set.Option(CustomizationId.LipColor), ref tmp) && tmp != x.Value.Lipstick)
|
|
||||||
{
|
|
||||||
x.Value.Lipstick = tmp;
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void Draw()
|
private void Draw()
|
||||||
{
|
{
|
||||||
ImGui.SetNextWindowSizeConstraints(Vector2.One * 450 * ImGui.GetIO().FontGlobalScale,
|
if (!_visible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui.SetNextWindowSizeConstraints(Vector2.One * MinWindowWidth * ImGui.GetIO().FontGlobalScale,
|
||||||
Vector2.One * 5000 * ImGui.GetIO().FontGlobalScale);
|
Vector2.One * 5000 * ImGui.GetIO().FontGlobalScale);
|
||||||
if (!_visible || !ImGui.Begin(_glamourerHeader, ref _visible))
|
if (!ImGui.Begin(_glamourerHeader, ref _visible))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var inCombo = ImGui.BeginCombo("Actor", _currentActorName);
|
using var raii = new ImGuiRaii();
|
||||||
var idx = 0;
|
if (!raii.Begin(() => ImGui.BeginTabBar("##tabBar"), ImGui.EndTabBar))
|
||||||
_player = null;
|
return;
|
||||||
foreach (var actor in _actors.Where(a => a.ObjectKind == ObjectKind.Player))
|
|
||||||
{
|
|
||||||
if (_currentActorName == actor.Name)
|
|
||||||
_player = actor;
|
|
||||||
|
|
||||||
if (inCombo && ImGui.Selectable($"{actor.Name}##{idx++}"))
|
_inGPose = _actors[GPoseActorId] != null;
|
||||||
_currentActorName = actor.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_player == null)
|
|
||||||
{
|
|
||||||
_player = _actors[0];
|
|
||||||
_currentActorName = _player?.Name ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inCombo)
|
|
||||||
ImGui.EndCombo();
|
|
||||||
|
|
||||||
if (_player == _actors[0] && _actors[GPoseActorId] != null)
|
|
||||||
_player = _actors[GPoseActorId];
|
|
||||||
if (_player == null || !GlamourerPlugin.PluginInterface.ClientState.Condition.Any())
|
|
||||||
{
|
|
||||||
ImGui.TextColored(new Vector4(0.4f, 0.1f, 0.1f, 1f),
|
|
||||||
"No player character available.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var equip = new ActorEquipment(_player);
|
|
||||||
_iconSize = Vector2.One * ImGui.GetTextLineHeightWithSpacing() * 2;
|
_iconSize = Vector2.One * ImGui.GetTextLineHeightWithSpacing() * 2;
|
||||||
_actualIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding;
|
_actualIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding;
|
||||||
_comboSelectorSize = 4 * _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
|
_comboSelectorSize = 4 * _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
|
@ -867,59 +88,9 @@ namespace Glamourer.Gui
|
||||||
_inputIntSize = 2 * _actualIconSize.X + ImGui.GetStyle().ItemSpacing.X;
|
_inputIntSize = 2 * _actualIconSize.X + ImGui.GetStyle().ItemSpacing.X;
|
||||||
_raceSelectorWidth = _inputIntSize + _percentageSize - _actualIconSize.X;
|
_raceSelectorWidth = _inputIntSize + _percentageSize - _actualIconSize.X;
|
||||||
_itemComboWidth = 6 * _actualIconSize.X + 4 * ImGui.GetStyle().ItemSpacing.X - ColorButtonWidth + 1;
|
_itemComboWidth = 6 * _actualIconSize.X + 4 * ImGui.GetStyle().ItemSpacing.X - ColorButtonWidth + 1;
|
||||||
var changes = false;
|
|
||||||
|
|
||||||
if (ImGui.CollapsingHeader("Character Equipment"))
|
DrawActorTab();
|
||||||
{
|
DrawSaves();
|
||||||
changes |= DrawWeapon(EquipSlot.MainHand, equip.MainHand);
|
|
||||||
changes |= DrawWeapon(EquipSlot.OffHand, equip.OffHand);
|
|
||||||
changes |= DrawEquip(EquipSlot.Head, equip.Head);
|
|
||||||
changes |= DrawEquip(EquipSlot.Body, equip.Body);
|
|
||||||
changes |= DrawEquip(EquipSlot.Hands, equip.Hands);
|
|
||||||
changes |= DrawEquip(EquipSlot.Legs, equip.Legs);
|
|
||||||
changes |= DrawEquip(EquipSlot.Feet, equip.Feet);
|
|
||||||
changes |= DrawEquip(EquipSlot.Ears, equip.Ears);
|
|
||||||
changes |= DrawEquip(EquipSlot.Neck, equip.Neck);
|
|
||||||
changes |= DrawEquip(EquipSlot.Wrists, equip.Wrists);
|
|
||||||
changes |= DrawEquip(EquipSlot.RFinger, equip.RFinger);
|
|
||||||
changes |= DrawEquip(EquipSlot.LFinger, equip.LFinger);
|
|
||||||
}
|
|
||||||
|
|
||||||
var x = new LazyCustomization(_player!.Address);
|
|
||||||
if (ImGui.CollapsingHeader("Character Customization"))
|
|
||||||
changes |= DrawStuff(x);
|
|
||||||
|
|
||||||
if (ImGui.Button("Copy to Clipboard"))
|
|
||||||
{
|
|
||||||
var save = new CharacterSave();
|
|
||||||
save.Load(x.Value);
|
|
||||||
save.Load(equip);
|
|
||||||
Clipboard.SetText(save.ToBase64());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGui.Button("Apply from Clipboard"))
|
|
||||||
{
|
|
||||||
var text = Clipboard.GetText();
|
|
||||||
if (text.Any())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var save = CharacterSave.FromString(text);
|
|
||||||
save.Customizations.Write(_player.Address);
|
|
||||||
save.Equipment.Write(_player.Address);
|
|
||||||
changes = true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLog.Information($"{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changes)
|
|
||||||
UpdateActors(_player);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
||||||
188
Glamourer/Gui/InterfaceActorPanel.cs
Normal file
188
Glamourer/Gui/InterfaceActorPanel.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Glamourer.Designs;
|
||||||
|
using Glamourer.FileSystem;
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui
|
||||||
|
{
|
||||||
|
internal partial class Interface
|
||||||
|
{
|
||||||
|
private readonly CharacterSave _currentSave = new();
|
||||||
|
private string _newDesignName = string.Empty;
|
||||||
|
private bool _keyboardFocus = false;
|
||||||
|
private const string DesignNamePopupLabel = "Save Design As...";
|
||||||
|
private const uint RedHeaderColor = 0xFF1818C0;
|
||||||
|
private const uint GreenHeaderColor = 0xFF18C018;
|
||||||
|
|
||||||
|
private void DrawActorHeader()
|
||||||
|
{
|
||||||
|
var color = _player == null ? RedHeaderColor : GreenHeaderColor;
|
||||||
|
var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
|
||||||
|
using var raii = new ImGuiRaii()
|
||||||
|
.PushColor(ImGuiCol.Text, color)
|
||||||
|
.PushColor(ImGuiCol.Button, buttonColor)
|
||||||
|
.PushColor(ImGuiCol.ButtonHovered, buttonColor)
|
||||||
|
.PushColor(ImGuiCol.ButtonActive, buttonColor)
|
||||||
|
.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||||
|
.PushStyle(ImGuiStyleVar.FrameRounding, 0);
|
||||||
|
ImGui.Button($"{_currentActorName}##actorHeader", -Vector2.UnitX * 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawCopyClipboardButton(CharacterSave save)
|
||||||
|
{
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.Clipboard.ToIconString()))
|
||||||
|
Clipboard.SetText(save.ToBase64());
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Copy customization code to clipboard.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawApplyClipboardButton()
|
||||||
|
{
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
var applyButton = ImGui.Button(FontAwesomeIcon.Paste.ToIconString()) && _player != null;
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Apply customization code from clipboard.");
|
||||||
|
|
||||||
|
if (!applyButton)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var text = Clipboard.GetText();
|
||||||
|
if (!text.Any())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var save = CharacterSave.FromString(text);
|
||||||
|
save.Apply(_player!);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Information($"{e}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSaveDesignButton()
|
||||||
|
{
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.Save.ToIconString()))
|
||||||
|
OpenDesignNamePopup(DesignNameUse.SaveCurrent);
|
||||||
|
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Save the current design.");
|
||||||
|
|
||||||
|
DrawDesignNamePopup(DesignNameUse.SaveCurrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTargetPlayerButton()
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Target Player"))
|
||||||
|
GlamourerPlugin.PluginInterface.ClientState.Targets.SetCurrentTarget(_player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawApplyToPlayerButton(CharacterSave save)
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Apply to Self"))
|
||||||
|
{
|
||||||
|
var player = _inGPose
|
||||||
|
? GlamourerPlugin.PluginInterface.ClientState.Actors[GPoseActorId]
|
||||||
|
: GlamourerPlugin.PluginInterface.ClientState.LocalPlayer;
|
||||||
|
var fallback = _inGPose ? GlamourerPlugin.PluginInterface.ClientState.LocalPlayer : null;
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
save.Apply(player);
|
||||||
|
if (_inGPose)
|
||||||
|
save.Apply(fallback!);
|
||||||
|
_plugin.UpdateActors(player, fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawApplyToTargetButton(CharacterSave save)
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Apply to Target"))
|
||||||
|
{
|
||||||
|
var player = GlamourerPlugin.PluginInterface.ClientState.Targets.CurrentTarget;
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
var fallBackActor = _playerNames[player.Name];
|
||||||
|
save.Apply(player);
|
||||||
|
if (fallBackActor != null)
|
||||||
|
save.Apply(fallBackActor);
|
||||||
|
_plugin.UpdateActors(player, fallBackActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveNewDesign(CharacterSave save)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (folder, name) = _designs.FileSystem.CreateAllFolders(_newDesignName);
|
||||||
|
if (name.Any())
|
||||||
|
{
|
||||||
|
var newDesign = new Design(folder, name) { Data = save };
|
||||||
|
folder.AddChild(newDesign);
|
||||||
|
_designs.Designs[newDesign.FullName()] = save;
|
||||||
|
_designs.SaveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Could not save new design {_newDesignName}:\n{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawActorPanel()
|
||||||
|
{
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
DrawActorHeader();
|
||||||
|
if (!ImGui.BeginChild("##actorData", -Vector2.One, true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
DrawCopyClipboardButton(_currentSave);
|
||||||
|
ImGui.SameLine();
|
||||||
|
var changes = DrawApplyClipboardButton();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawSaveDesignButton();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawApplyToPlayerButton(_currentSave);
|
||||||
|
if (!_inGPose)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawApplyToTargetButton(_currentSave);
|
||||||
|
if (_player != null)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawTargetPlayerButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (DrawCustomization(ref _currentSave.Customizations) && _player != null)
|
||||||
|
{
|
||||||
|
_currentSave.Customizations.Write(_player.Address);
|
||||||
|
changes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
changes |= DrawEquip(_currentSave.Equipment);
|
||||||
|
changes |= DrawMiscellaneous(_currentSave, _player);
|
||||||
|
|
||||||
|
if (_player != null && changes)
|
||||||
|
_plugin.UpdateActors(_player);
|
||||||
|
ImGui.EndChild();
|
||||||
|
ImGui.EndGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
Glamourer/Gui/InterfaceActorSelector.cs
Normal file
151
Glamourer/Gui/InterfaceActorSelector.cs
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Game.ClientState.Actors;
|
||||||
|
using Dalamud.Game.ClientState.Actors.Types;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui
|
||||||
|
{
|
||||||
|
internal partial class Interface
|
||||||
|
{
|
||||||
|
private Actor? _player;
|
||||||
|
private string _currentActorName = string.Empty;
|
||||||
|
private string _actorFilter = string.Empty;
|
||||||
|
private string _actorFilterLower = string.Empty;
|
||||||
|
private readonly Dictionary<string, Actor?> _playerNames = new(400);
|
||||||
|
|
||||||
|
private void DrawActorFilter()
|
||||||
|
{
|
||||||
|
using var raii = new ImGuiRaii()
|
||||||
|
.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||||
|
.PushStyle(ImGuiStyleVar.FrameRounding, 0);
|
||||||
|
ImGui.SetNextItemWidth(SelectorWidth * ImGui.GetIO().FontGlobalScale);
|
||||||
|
if (ImGui.InputTextWithHint("##actorFilter", "Filter Players...", ref _actorFilter, 32))
|
||||||
|
_actorFilterLower = _actorFilter.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawActorSelectable(Actor actor, bool gPose)
|
||||||
|
{
|
||||||
|
var actorName = actor.Name;
|
||||||
|
if (!actorName.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_playerNames.ContainsKey(actorName))
|
||||||
|
{
|
||||||
|
_playerNames[actorName] = actor;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_playerNames.Add(actorName, null);
|
||||||
|
|
||||||
|
var label = gPose ? $"{actorName} (GPose)" : actorName;
|
||||||
|
if (!_actorFilterLower.Any() || actorName.ToLowerInvariant().Contains(_actorFilterLower))
|
||||||
|
if (ImGui.Selectable(label, _currentActorName == actorName))
|
||||||
|
{
|
||||||
|
_currentActorName = actorName;
|
||||||
|
_currentSave.LoadActor(actor);
|
||||||
|
_player = actor;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentActorName == actor.Name)
|
||||||
|
{
|
||||||
|
_currentSave.LoadActor(actor);
|
||||||
|
_player = actor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSelectionButtons()
|
||||||
|
{
|
||||||
|
using var raii = new ImGuiRaii()
|
||||||
|
.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||||
|
.PushStyle(ImGuiStyleVar.FrameRounding, 0)
|
||||||
|
.PushFont(UiBuilder.IconFont);
|
||||||
|
Actor? select = null;
|
||||||
|
var buttonWidth = Vector2.UnitX * SelectorWidth / 2;
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth))
|
||||||
|
select = GlamourerPlugin.PluginInterface.ClientState.LocalPlayer;
|
||||||
|
raii.PopFonts();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Select the local player character.");
|
||||||
|
ImGui.SameLine();
|
||||||
|
raii.PushFont(UiBuilder.IconFont);
|
||||||
|
if (_inGPose)
|
||||||
|
{
|
||||||
|
raii.PushStyle(ImGuiStyleVar.Alpha, 0.5f);
|
||||||
|
ImGui.Button(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth);
|
||||||
|
raii.PopStyles();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth))
|
||||||
|
select = GlamourerPlugin.PluginInterface.ClientState.Targets.CurrentTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
raii.PopFonts();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Select the current target, if it is a player actor.");
|
||||||
|
|
||||||
|
if (select == null || select.ObjectKind != ObjectKind.Player)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_player = select;
|
||||||
|
_currentActorName = _player.Name;
|
||||||
|
_currentSave.LoadActor(_player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawActorSelector()
|
||||||
|
{
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
DrawActorFilter();
|
||||||
|
if (!ImGui.BeginChild("##actorSelector",
|
||||||
|
new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_playerNames.Clear();
|
||||||
|
for (var i = GPoseActorId; i < GPoseActorId + 48; ++i)
|
||||||
|
{
|
||||||
|
var actor = _actors[i];
|
||||||
|
if (actor == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (actor.ObjectKind == ObjectKind.Player)
|
||||||
|
DrawActorSelectable(actor, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < GPoseActorId; i += 2)
|
||||||
|
{
|
||||||
|
var actor = _actors[i];
|
||||||
|
if (actor != null && actor.ObjectKind == ObjectKind.Player)
|
||||||
|
DrawActorSelectable(actor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
using (var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
|
||||||
|
{
|
||||||
|
ImGui.EndChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawSelectionButtons();
|
||||||
|
ImGui.EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawActorTab()
|
||||||
|
{
|
||||||
|
using var raii = new ImGuiRaii();
|
||||||
|
if (!raii.Begin(() => ImGui.BeginTabItem("Current Players"), ImGui.EndTabItem))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_player = null;
|
||||||
|
DrawActorSelector();
|
||||||
|
|
||||||
|
if (!_currentActorName.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawActorPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
480
Glamourer/Gui/InterfaceCustomization.cs
Normal file
480
Glamourer/Gui/InterfaceCustomization.cs
Normal file
|
|
@ -0,0 +1,480 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Glamourer.Customization;
|
||||||
|
using ImGuiNET;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui
|
||||||
|
{
|
||||||
|
internal partial class Interface
|
||||||
|
{
|
||||||
|
private static bool DrawColorPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
if (!ImGui.BeginPopup(label, ImGuiWindowFlags.AlwaysAutoResize))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var ret = false;
|
||||||
|
var count = set.Count(id);
|
||||||
|
using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||||
|
.PushStyle(ImGuiStyleVar.FrameRounding, 0);
|
||||||
|
for (var i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
var custom = set.Data(id, i);
|
||||||
|
if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)))
|
||||||
|
{
|
||||||
|
value = custom;
|
||||||
|
ret = true;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i % 8 != 7)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndPopup();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 _iconSize = Vector2.Zero;
|
||||||
|
private Vector2 _actualIconSize = Vector2.Zero;
|
||||||
|
private float _raceSelectorWidth = 0;
|
||||||
|
private float _inputIntSize = 0;
|
||||||
|
private float _comboSelectorSize = 0;
|
||||||
|
private float _percentageSize = 0;
|
||||||
|
private float _itemComboWidth = 0;
|
||||||
|
|
||||||
|
private bool InputInt(string label, ref int value, int minValue, int maxValue)
|
||||||
|
{
|
||||||
|
var ret = false;
|
||||||
|
var tmp = value + 1;
|
||||||
|
ImGui.SetNextItemWidth(_inputIntSize);
|
||||||
|
if (ImGui.InputInt(label, ref tmp, 1) && tmp != value + 1 && tmp >= minValue && tmp <= maxValue)
|
||||||
|
{
|
||||||
|
value = tmp - 1;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Input Range: [{minValue}, {maxValue}]");
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int, Customization.Customization) GetCurrentCustomization(ref ActorCustomization customization, CustomizationId id,
|
||||||
|
CustomizationSet set)
|
||||||
|
{
|
||||||
|
var current = set.DataByValue(id, customization[id], out var custom);
|
||||||
|
if (set.IsAvailable(id) && current < 0)
|
||||||
|
{
|
||||||
|
PluginLog.Warning($"Read invalid customization value {customization[id]} for {id}.");
|
||||||
|
current = 0;
|
||||||
|
custom = set.Data(id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (current, custom!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawColorPicker(string label, string tooltip, ref ActorCustomization customization, CustomizationId id,
|
||||||
|
CustomizationSet set)
|
||||||
|
{
|
||||||
|
var ret = false;
|
||||||
|
var count = set.Count(id);
|
||||||
|
|
||||||
|
var (current, custom) = GetCurrentCustomization(ref customization, id, set);
|
||||||
|
|
||||||
|
var popupName = $"Color Picker##{id}";
|
||||||
|
if (ImGui.ColorButton($"{current + 1}##color_{id}", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None,
|
||||||
|
_actualIconSize))
|
||||||
|
ImGui.OpenPopup(popupName);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
using (var group = ImGuiRaii.NewGroup())
|
||||||
|
{
|
||||||
|
if (InputInt($"##text_{id}", ref current, 1, count))
|
||||||
|
{
|
||||||
|
customization[id] = set.Data(id, current - 1).Value;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ImGui.Text(label);
|
||||||
|
if (tooltip.Any() && ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DrawColorPickerPopup(popupName, set, id, out var newCustom))
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
customization[id] = newCustom.Value;
|
||||||
|
ret = true;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawListSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id,
|
||||||
|
CustomizationSet set)
|
||||||
|
{
|
||||||
|
using var bigGroup = ImGuiRaii.NewGroup();
|
||||||
|
var ret = false;
|
||||||
|
int current = customization[id];
|
||||||
|
var count = set.Count(id);
|
||||||
|
|
||||||
|
ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale);
|
||||||
|
if (ImGui.BeginCombo($"##combo_{id}", $"{set.Option(id)} #{current + 1}"))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
if (ImGui.Selectable($"{set.Option(id)} #{i + 1}##combo", i == current) && i != current)
|
||||||
|
{
|
||||||
|
customization[id] = (byte) i;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (InputInt($"##text_{id}", ref current, 1, count))
|
||||||
|
{
|
||||||
|
customization[id] = set.Data(id, current).Value;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.Text(label);
|
||||||
|
if (tooltip.Any() && ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(tooltip);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly Vector4 NoColor = new(1f, 1f, 1f, 1f);
|
||||||
|
private static readonly Vector4 RedColor = new(0.6f, 0.3f, 0.3f, 1f);
|
||||||
|
|
||||||
|
private bool DrawMultiSelector(ref ActorCustomization customization, CustomizationSet set)
|
||||||
|
{
|
||||||
|
using var bigGroup = ImGuiRaii.NewGroup();
|
||||||
|
var ret = false;
|
||||||
|
var count = set.Count(CustomizationId.FacialFeaturesTattoos);
|
||||||
|
using (var _ = ImGuiRaii.NewGroup())
|
||||||
|
{
|
||||||
|
for (var i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
var enabled = customization.FacialFeature(i);
|
||||||
|
var feature = set.FacialFeature(set.Race == Race.Hrothgar ? customization.Hairstyle : customization.Face, i);
|
||||||
|
var icon = i == count - 1
|
||||||
|
? _legacyTattooIcon ?? GlamourerPlugin.Customization.GetIcon(feature.IconId)
|
||||||
|
: GlamourerPlugin.Customization.GetIcon(feature.IconId);
|
||||||
|
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int) ImGui.GetStyle().FramePadding.X,
|
||||||
|
Vector4.Zero,
|
||||||
|
enabled ? NoColor : RedColor))
|
||||||
|
{
|
||||||
|
ret = true;
|
||||||
|
customization.FacialFeature(i, !enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
using var tt = ImGuiRaii.NewTooltip();
|
||||||
|
ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i % 4 != 3)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
using var group = ImGuiRaii.NewGroup();
|
||||||
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() + 3 * ImGui.GetStyle().ItemSpacing.Y / 2);
|
||||||
|
int value = customization[CustomizationId.FacialFeaturesTattoos];
|
||||||
|
if (InputInt($"##{CustomizationId.FacialFeaturesTattoos}", ref value, 1, 256))
|
||||||
|
{
|
||||||
|
customization[CustomizationId.FacialFeaturesTattoos] = (byte) value;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Text(set.Option(CustomizationId.FacialFeaturesTattoos));
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private bool DrawIconPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
if (!ImGui.BeginPopup(label, ImGuiWindowFlags.AlwaysAutoResize))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var ret = false;
|
||||||
|
var count = set.Count(id);
|
||||||
|
using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||||
|
.PushStyle(ImGuiStyleVar.FrameRounding, 0);
|
||||||
|
for (var i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
var custom = set.Data(id, i);
|
||||||
|
var icon = GlamourerPlugin.Customization.GetIcon(custom.IconId);
|
||||||
|
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
|
||||||
|
{
|
||||||
|
value = custom;
|
||||||
|
ret = true;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
using var tt = ImGuiRaii.NewTooltip();
|
||||||
|
ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i % 8 != 7)
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndPopup();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawIconSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id,
|
||||||
|
CustomizationSet set)
|
||||||
|
{
|
||||||
|
using var bigGroup = ImGuiRaii.NewGroup();
|
||||||
|
var ret = false;
|
||||||
|
var count = set.Count(id);
|
||||||
|
|
||||||
|
var current = set.DataByValue(id, customization[id], out var custom);
|
||||||
|
if (current < 0)
|
||||||
|
{
|
||||||
|
PluginLog.Warning($"Read invalid customization value {customization[id]} for {id}.");
|
||||||
|
current = 0;
|
||||||
|
custom = set.Data(id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var popupName = $"Style Picker##{id}";
|
||||||
|
var icon = GlamourerPlugin.Customization.GetIcon(custom!.Value.IconId);
|
||||||
|
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
|
||||||
|
ImGui.OpenPopup(popupName);
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
using var tt = ImGuiRaii.NewTooltip();
|
||||||
|
ImGui.Image(icon.ImGuiHandle, new Vector2(icon.Width, icon.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
using var group = ImGuiRaii.NewGroup();
|
||||||
|
if (InputInt($"##text_{id}", ref current, 1, count))
|
||||||
|
{
|
||||||
|
customization[id] = set.Data(id, current).Value;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DrawIconPickerPopup(popupName, set, id, out var newCustom))
|
||||||
|
{
|
||||||
|
customization[id] = newCustom.Value;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Text(label);
|
||||||
|
if (tooltip.Any() && ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(tooltip);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private bool DrawPercentageSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id,
|
||||||
|
CustomizationSet set)
|
||||||
|
{
|
||||||
|
using var bigGroup = ImGuiRaii.NewGroup();
|
||||||
|
var ret = false;
|
||||||
|
int value = customization[id];
|
||||||
|
var count = set.Count(id);
|
||||||
|
ImGui.SetNextItemWidth(_percentageSize * ImGui.GetIO().FontGlobalScale);
|
||||||
|
if (ImGui.SliderInt($"##slider_{id}", ref value, 0, count - 1, "") && value != customization[id])
|
||||||
|
{
|
||||||
|
customization[id] = (byte) value;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
--value;
|
||||||
|
if (InputInt($"##input_{id}", ref value, 0, count - 1))
|
||||||
|
{
|
||||||
|
customization[id] = (byte) (value + 1);
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.Text(label);
|
||||||
|
if (tooltip.Any() && ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(tooltip);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawRaceSelector(ref ActorCustomization customization)
|
||||||
|
{
|
||||||
|
using var group = ImGuiRaii.NewGroup();
|
||||||
|
var ret = false;
|
||||||
|
ImGui.SetNextItemWidth(_raceSelectorWidth);
|
||||||
|
if (ImGui.BeginCombo("##subRaceCombo", ClanName(customization.Clan, customization.Gender)))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < (int)SubRace.Veena; ++i)
|
||||||
|
{
|
||||||
|
if (ImGui.Selectable(ClanName((SubRace)i + 1, customization.Gender), (int)customization.Clan == i + 1))
|
||||||
|
{
|
||||||
|
var race = (SubRace)i + 1;
|
||||||
|
ret |= ChangeRace(ref customization, race);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Text(
|
||||||
|
$"{GlamourerPlugin.Customization.GetName(CustomName.Gender)} & {GlamourerPlugin.Customization.GetName(CustomName.Clan)}");
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawGenderSelector(ref ActorCustomization customization)
|
||||||
|
{
|
||||||
|
var ret = false;
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
var icon = customization.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus;
|
||||||
|
var restricted = false;
|
||||||
|
if (customization.Race == Race.Viera)
|
||||||
|
{
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.25f);
|
||||||
|
icon = FontAwesomeIcon.VenusDouble;
|
||||||
|
restricted = true;
|
||||||
|
}
|
||||||
|
else if (customization.Race == Race.Hrothgar)
|
||||||
|
{
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.25f);
|
||||||
|
icon = FontAwesomeIcon.MarsDouble;
|
||||||
|
restricted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.Button(icon.ToIconString(), _actualIconSize) && !restricted)
|
||||||
|
{
|
||||||
|
var gender = customization.Gender == Gender.Male ? Gender.Female : Gender.Male;
|
||||||
|
ret = ChangeGender(ref customization, gender);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restricted)
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
ImGui.PopFont();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawPicker(CustomizationSet set, CustomizationId id, ref ActorCustomization customization)
|
||||||
|
{
|
||||||
|
if (!set.IsAvailable(id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (set.Type(id))
|
||||||
|
{
|
||||||
|
case CharaMakeParams.MenuType.ColorPicker: return DrawColorPicker(set.OptionName[(int)id], "", ref customization, id, set);
|
||||||
|
case CharaMakeParams.MenuType.ListSelector: return DrawListSelector(set.OptionName[(int)id], "", ref customization, id, set);
|
||||||
|
case CharaMakeParams.MenuType.IconSelector: return DrawIconSelector(set.OptionName[(int)id], "", ref customization, id, set);
|
||||||
|
case CharaMakeParams.MenuType.MultiIconSelector: return DrawMultiSelector(ref customization, set);
|
||||||
|
case CharaMakeParams.MenuType.Percentage: return DrawPercentageSelector(set.OptionName[(int)id], "", ref customization, id, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly CustomizationId[] AllCustomizations = (CustomizationId[])Enum.GetValues(typeof(CustomizationId));
|
||||||
|
private bool DrawCustomization(ref ActorCustomization custom)
|
||||||
|
{
|
||||||
|
if (!ImGui.CollapsingHeader("Character Customization"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var ret = DrawGenderSelector(ref custom);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ret |= DrawRaceSelector(ref custom);
|
||||||
|
|
||||||
|
var set = GlamourerPlugin.Customization.GetList(custom.Clan, custom.Gender);
|
||||||
|
|
||||||
|
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.Percentage))
|
||||||
|
ret |= DrawPicker(set, id, ref custom);
|
||||||
|
|
||||||
|
var odd = true;
|
||||||
|
foreach (var id in AllCustomizations.Where((c, _) => set.Type(c) == CharaMakeParams.MenuType.IconSelector))
|
||||||
|
{
|
||||||
|
ret |= DrawPicker(set, id, ref custom);
|
||||||
|
if (odd)
|
||||||
|
ImGui.SameLine();
|
||||||
|
odd = !odd;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!odd)
|
||||||
|
ImGui.NewLine();
|
||||||
|
|
||||||
|
ret |= DrawPicker(set, CustomizationId.FacialFeaturesTattoos, ref custom);
|
||||||
|
|
||||||
|
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.ListSelector))
|
||||||
|
ret |= DrawPicker(set, id, ref custom);
|
||||||
|
|
||||||
|
odd = true;
|
||||||
|
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.ColorPicker))
|
||||||
|
{
|
||||||
|
ret |= DrawPicker(set, id, ref custom);
|
||||||
|
if (odd)
|
||||||
|
ImGui.SameLine();
|
||||||
|
odd = !odd;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!odd)
|
||||||
|
ImGui.NewLine();
|
||||||
|
|
||||||
|
var tmp = custom.HighlightsOn;
|
||||||
|
if (ImGui.Checkbox(set.Option(CustomizationId.HighlightsOnFlag), ref tmp) && tmp != custom.HighlightsOn)
|
||||||
|
{
|
||||||
|
custom.HighlightsOn = tmp;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xPos = _inputIntSize + _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
ImGui.SameLine(xPos);
|
||||||
|
tmp = custom.FacePaintReversed;
|
||||||
|
if (ImGui.Checkbox($"{GlamourerPlugin.Customization.GetName(CustomName.Reverse)} {set.Option(CustomizationId.FacePaint)}", ref tmp)
|
||||||
|
&& tmp != custom.FacePaintReversed)
|
||||||
|
{
|
||||||
|
custom.FacePaintReversed = tmp;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp = custom.SmallIris;
|
||||||
|
if (ImGui.Checkbox($"{GlamourerPlugin.Customization.GetName(CustomName.IrisSmall)} {set.Option(CustomizationId.EyeColorL)}",
|
||||||
|
ref tmp)
|
||||||
|
&& tmp != custom.SmallIris)
|
||||||
|
{
|
||||||
|
custom.SmallIris = tmp;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (custom.Race != Race.Hrothgar)
|
||||||
|
{
|
||||||
|
tmp = custom.Lipstick;
|
||||||
|
ImGui.SameLine(xPos);
|
||||||
|
if (ImGui.Checkbox(set.Option(CustomizationId.LipColor), ref tmp) && tmp != custom.Lipstick)
|
||||||
|
{
|
||||||
|
custom.Lipstick = tmp;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
343
Glamourer/Gui/InterfaceDesigns.cs
Normal file
343
Glamourer/Gui/InterfaceDesigns.cs
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Glamourer.Designs;
|
||||||
|
using Glamourer.FileSystem;
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui
|
||||||
|
{
|
||||||
|
internal partial class Interface
|
||||||
|
{
|
||||||
|
private int _totalObject = 0;
|
||||||
|
|
||||||
|
private Design? _selection = null;
|
||||||
|
private string _newChildName = string.Empty;
|
||||||
|
|
||||||
|
private void DrawDesignSelector()
|
||||||
|
{
|
||||||
|
_totalObject = 0;
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
if (ImGui.BeginChild("##selector", new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, - ImGui.GetFrameHeight() - 1) , true))
|
||||||
|
{
|
||||||
|
DrawFolderContent(_designs.FileSystem.Root, SortMode.FoldersFirst);
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||||
|
ImGui.EndChild();
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
}
|
||||||
|
DrawDesignSelectorButtons();
|
||||||
|
ImGui.EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawPasteClipboardButton()
|
||||||
|
{
|
||||||
|
if (_selection!.Data.WriteProtected)
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
|
||||||
|
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
var applyButton = ImGui.Button(FontAwesomeIcon.Paste.ToIconString());
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (_selection!.Data.WriteProtected)
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Overwrite with customization code from clipboard.");
|
||||||
|
|
||||||
|
if (_selection!.Data.WriteProtected || !applyButton)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var text = Clipboard.GetText();
|
||||||
|
if (!text.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_selection!.Data = CharacterSave.FromString(text);
|
||||||
|
_designs.SaveToFile();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Information($"{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNewFolderButton()
|
||||||
|
{
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.FolderPlus.ToIconString(), Vector2.UnitX * SelectorWidth / 5))
|
||||||
|
OpenDesignNamePopup(DesignNameUse.NewFolder);
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Create a new, empty Folder.");
|
||||||
|
|
||||||
|
DrawDesignNamePopup(DesignNameUse.NewFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNewDesignButton()
|
||||||
|
{
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), Vector2.UnitX * SelectorWidth / 5))
|
||||||
|
OpenDesignNamePopup(DesignNameUse.NewDesign);
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Create a new, empty Design.");
|
||||||
|
|
||||||
|
DrawDesignNamePopup(DesignNameUse.NewDesign);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawClipboardDesignButton()
|
||||||
|
{
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.Paste.ToIconString(), Vector2.UnitX * SelectorWidth / 5))
|
||||||
|
OpenDesignNamePopup(DesignNameUse.FromClipboard);
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Create a new design from the customization string in your clipboard.");
|
||||||
|
|
||||||
|
DrawDesignNamePopup(DesignNameUse.FromClipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDeleteDesignButton()
|
||||||
|
{
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
var style = _selection == null;
|
||||||
|
if (style)
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString(), Vector2.UnitX * SelectorWidth / 5) && _selection != null)
|
||||||
|
{
|
||||||
|
_designs.DeleteAllChildren(_selection, false);
|
||||||
|
_selection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (style)
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Delete the currently selected Design.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDuplicateDesignButton()
|
||||||
|
{
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
if (_selection == null)
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.5f);
|
||||||
|
if (ImGui.Button(FontAwesomeIcon.Clone.ToIconString(), Vector2.UnitX * SelectorWidth / 5) && _selection != null)
|
||||||
|
OpenDesignNamePopup(DesignNameUse.DuplicateDesign);
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (_selection == null)
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Clone the currently selected Design.");
|
||||||
|
|
||||||
|
DrawDesignNamePopup(DesignNameUse.DuplicateDesign);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDesignSelectorButtons()
|
||||||
|
{
|
||||||
|
using var raii = new ImGuiRaii()
|
||||||
|
.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||||
|
.PushStyle(ImGuiStyleVar.FrameRounding, 0f);
|
||||||
|
|
||||||
|
DrawNewFolderButton();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawNewDesignButton();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawClipboardDesignButton();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawDuplicateDesignButton();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawDeleteDesignButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDesignHeaderButtons()
|
||||||
|
{
|
||||||
|
DrawCopyClipboardButton(_selection!.Data);
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawPasteClipboardButton();
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawApplyToPlayerButton(_selection!.Data);
|
||||||
|
if (!_inGPose)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawApplyToTargetButton(_selection!.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawCheckbox("Write Protected", _selection!.Data.WriteProtected, v => _selection!.Data.WriteProtected = v, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDesignPanel()
|
||||||
|
{
|
||||||
|
if (ImGui.BeginChild("##details", -Vector2.One * 0.001f, true))
|
||||||
|
{
|
||||||
|
DrawDesignHeaderButtons();
|
||||||
|
var data = _selection!.Data;
|
||||||
|
var prot = _selection!.Data.WriteProtected;
|
||||||
|
if (prot)
|
||||||
|
{
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.8f);
|
||||||
|
data = data.Copy();
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawGeneralSettings(data, prot);
|
||||||
|
var mask = data.WriteEquipment;
|
||||||
|
if (DrawEquip(data.Equipment, ref mask) && !prot)
|
||||||
|
{
|
||||||
|
data.WriteEquipment = mask;
|
||||||
|
_designs.SaveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DrawCustomization(ref data.Customizations) && !prot)
|
||||||
|
_designs.SaveToFile();
|
||||||
|
|
||||||
|
if (DrawMiscellaneous(data, null) && !prot)
|
||||||
|
_designs.SaveToFile();
|
||||||
|
|
||||||
|
if (prot)
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
|
||||||
|
ImGui.EndChild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawSaves()
|
||||||
|
{
|
||||||
|
using var raii = new ImGuiRaii();
|
||||||
|
raii.PushStyle(ImGuiStyleVar.IndentSpacing, 25f);
|
||||||
|
if (!raii.Begin(() => ImGui.BeginTabItem("Saves"), ImGui.EndTabItem))
|
||||||
|
return;
|
||||||
|
|
||||||
|
DrawDesignSelector();
|
||||||
|
|
||||||
|
if (_selection != null)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
DrawDesignPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCheckbox(string label, bool value, Action<bool> setter, bool prot)
|
||||||
|
{
|
||||||
|
var tmp = value;
|
||||||
|
if (ImGui.Checkbox(label, ref tmp) && tmp != value)
|
||||||
|
{
|
||||||
|
setter(tmp);
|
||||||
|
if (!prot)
|
||||||
|
_designs.SaveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGeneralSettings(CharacterSave data, bool prot)
|
||||||
|
{
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
DrawCheckbox("Apply Customizations", data.WriteCustomizations, v => data.WriteCustomizations = v, prot);
|
||||||
|
DrawCheckbox("Write Weapon State", data.SetWeaponState, v => data.SetWeaponState = v, prot);
|
||||||
|
ImGui.EndGroup();
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
DrawCheckbox("Write Hat State", data.SetHatState, v => data.SetHatState = v, prot);
|
||||||
|
DrawCheckbox("Write Visor State", data.SetVisorState, v => data.SetVisorState = v, prot);
|
||||||
|
ImGui.EndGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenameChildInput(IFileSystemBase child)
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(150);
|
||||||
|
if (!ImGui.InputTextWithHint("##fsNewName", "Rename...", ref _newChildName, 64,
|
||||||
|
ImGuiInputTextFlags.EnterReturnsTrue))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_newChildName.Any() && _newChildName != child.Name)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var oldPath = child.FullName();
|
||||||
|
if (_designs.FileSystem.Rename(child, _newChildName))
|
||||||
|
_designs.UpdateAllChildren(oldPath, child);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Could not rename {child.Name} to {_newChildName}:\n{e}");
|
||||||
|
}
|
||||||
|
else if (child is Folder f)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var oldPath = child.FullName();
|
||||||
|
if (_designs.FileSystem.Merge(f, f.Parent, true))
|
||||||
|
_designs.UpdateAllChildren(oldPath, f.Parent);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Error($"Could not merge folder {child.Name} into parent:\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_newChildName = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ContextMenu(IFileSystemBase child)
|
||||||
|
{
|
||||||
|
var label = $"##fsPopup{child.FullName()}";
|
||||||
|
var renameLabel = $"{label}_rename";
|
||||||
|
if (ImGui.BeginPopup(label))
|
||||||
|
{
|
||||||
|
if (ImGui.MenuItem("Delete"))
|
||||||
|
_designs.DeleteAllChildren(child, false);
|
||||||
|
|
||||||
|
RenameChildInput(child);
|
||||||
|
|
||||||
|
if (child is Design d && ImGui.MenuItem("Copy to Clipboard"))
|
||||||
|
Clipboard.SetText(d.Data.ToBase64());
|
||||||
|
|
||||||
|
ImGui.EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
_newChildName = child.Name;
|
||||||
|
ImGui.OpenPopup(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawFolderContent(Folder folder, SortMode mode)
|
||||||
|
{
|
||||||
|
foreach (var child in folder.AllChildren(mode).ToArray())
|
||||||
|
{
|
||||||
|
if (child.IsFolder(out var subFolder))
|
||||||
|
{
|
||||||
|
var treeNode = ImGui.TreeNodeEx($"{subFolder.Name}##{_totalObject}");
|
||||||
|
DrawOrnaments(child);
|
||||||
|
|
||||||
|
if (treeNode)
|
||||||
|
{
|
||||||
|
DrawFolderContent(subFolder, mode);
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_totalObject += subFolder.TotalDescendantLeaves();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++_totalObject;
|
||||||
|
var selected = ImGui.Selectable($"{child.Name}##{_totalObject}", ReferenceEquals(child, _selection));
|
||||||
|
DrawOrnaments(child);
|
||||||
|
|
||||||
|
if (selected)
|
||||||
|
if (child is Design d)
|
||||||
|
_selection = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawOrnaments(IFileSystemBase child)
|
||||||
|
{
|
||||||
|
FileSystemImGui.DragDropSource(child);
|
||||||
|
if (FileSystemImGui.DragDropTarget(_designs.FileSystem, child, out var oldPath, out var draggedFolder))
|
||||||
|
_designs.UpdateAllChildren(oldPath, draggedFolder!);
|
||||||
|
ContextMenu(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Glamourer/Gui/InterfaceEquipment.cs
Normal file
138
Glamourer/Gui/InterfaceEquipment.cs
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
using ImGuiNET;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui
|
||||||
|
{
|
||||||
|
internal partial class Interface
|
||||||
|
{
|
||||||
|
private bool DrawStainSelector(ComboWithFilter<Stain> stainCombo, EquipSlot slot, StainId stainIdx)
|
||||||
|
{
|
||||||
|
stainCombo.PostPreview = null;
|
||||||
|
if (_stains.TryGetValue((byte) stainIdx, out var stain))
|
||||||
|
{
|
||||||
|
var previewPush = PushColor(stain, ImGuiCol.FrameBg);
|
||||||
|
stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stainCombo.Draw(string.Empty, out var newStain) && _player != null && !newStain.RowIndex.Equals(stainIdx))
|
||||||
|
{
|
||||||
|
newStain.Write(_player.Address, slot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawItemSelector(ComboWithFilter<Item> equipCombo, Lumina.Excel.GeneratedSheets.Item? item)
|
||||||
|
{
|
||||||
|
var currentName = item?.Name.ToString() ?? "Nothing";
|
||||||
|
if (equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && _player != null && newItem.Base.RowId != item?.RowId)
|
||||||
|
{
|
||||||
|
newItem.Write(_player.Address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DrawCheckbox(ActorEquipMask flag, ref ActorEquipMask mask)
|
||||||
|
{
|
||||||
|
var tmp = (uint) mask;
|
||||||
|
var ret = false;
|
||||||
|
if (ImGui.CheckboxFlags($"##flag_{(uint) flag}", ref tmp, (uint) flag) && tmp != (uint) mask)
|
||||||
|
{
|
||||||
|
mask = (ActorEquipMask) tmp;
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Enable writing this slot in this save.");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawEquipSlot(EquipSlot slot, ActorArmor equip)
|
||||||
|
{
|
||||||
|
var (equipCombo, stainCombo) = _combos[slot];
|
||||||
|
|
||||||
|
var ret = DrawStainSelector(stainCombo, slot, equip.Stain);
|
||||||
|
ImGui.SameLine();
|
||||||
|
var item = _identifier.Identify(equip.Set, new WeaponType(), equip.Variant, slot);
|
||||||
|
ret |= DrawItemSelector(equipCombo, item);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawEquipSlotWithCheck(EquipSlot slot, ActorArmor equip, ActorEquipMask flag, ref ActorEquipMask mask)
|
||||||
|
{
|
||||||
|
var ret = DrawCheckbox(flag, ref mask);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ret |= DrawEquipSlot(slot, equip);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawWeapon(EquipSlot slot, ActorWeapon weapon)
|
||||||
|
{
|
||||||
|
var (equipCombo, stainCombo) = _combos[slot];
|
||||||
|
|
||||||
|
var ret = DrawStainSelector(stainCombo, slot, weapon.Stain);
|
||||||
|
ImGui.SameLine();
|
||||||
|
var item = _identifier.Identify(weapon.Set, weapon.Type, weapon.Variant, slot);
|
||||||
|
ret |= DrawItemSelector(equipCombo, item);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawWeaponWithCheck(EquipSlot slot, ActorWeapon weapon, ActorEquipMask flag, ref ActorEquipMask mask)
|
||||||
|
{
|
||||||
|
var ret = DrawCheckbox(flag, ref mask);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ret |= DrawWeapon(slot, weapon);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawEquip(ActorEquipment equip)
|
||||||
|
{
|
||||||
|
var ret = false;
|
||||||
|
if (ImGui.CollapsingHeader("Character Equipment"))
|
||||||
|
{
|
||||||
|
ret |= DrawWeapon(EquipSlot.MainHand, equip.MainHand);
|
||||||
|
ret |= DrawWeapon(EquipSlot.OffHand, equip.OffHand);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.Head, equip.Head);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.Body, equip.Body);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.Hands, equip.Hands);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.Legs, equip.Legs);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.Feet, equip.Feet);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.Ears, equip.Ears);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.Neck, equip.Neck);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.Wrists, equip.Wrists);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.RFinger, equip.RFinger);
|
||||||
|
ret |= DrawEquipSlot(EquipSlot.LFinger, equip.LFinger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawEquip(ActorEquipment equip, ref ActorEquipMask mask)
|
||||||
|
{
|
||||||
|
var ret = false;
|
||||||
|
if (ImGui.CollapsingHeader("Character Equipment"))
|
||||||
|
{
|
||||||
|
ret |= DrawWeaponWithCheck(EquipSlot.MainHand, equip.MainHand, ActorEquipMask.MainHand, ref mask);
|
||||||
|
ret |= DrawWeaponWithCheck(EquipSlot.OffHand, equip.OffHand, ActorEquipMask.OffHand, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.Head, equip.Head, ActorEquipMask.Head, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.Body, equip.Body, ActorEquipMask.Body, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.Hands, equip.Hands, ActorEquipMask.Hands, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.Legs, equip.Legs, ActorEquipMask.Legs, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.Feet, equip.Feet, ActorEquipMask.Feet, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.Ears, equip.Ears, ActorEquipMask.Ears, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.Neck, equip.Neck, ActorEquipMask.Neck, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.Wrists, equip.Wrists, ActorEquipMask.Wrists, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.RFinger, equip.RFinger, ActorEquipMask.RFinger, ref mask);
|
||||||
|
ret |= DrawEquipSlotWithCheck(EquipSlot.LFinger, equip.LFinger, ActorEquipMask.LFinger, ref mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
208
Glamourer/Gui/InterfaceHelpers.cs
Normal file
208
Glamourer/Gui/InterfaceHelpers.cs
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Dalamud.Game.ClientState.Actors.Types;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Glamourer.Customization;
|
||||||
|
using ImGuiNET;
|
||||||
|
using Penumbra.Api;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui
|
||||||
|
{
|
||||||
|
internal partial class Interface
|
||||||
|
{
|
||||||
|
// Push the stain color to type and if it is too bright, turn the text color black.
|
||||||
|
// Return number of pushed styles.
|
||||||
|
private static int PushColor(Stain stain, ImGuiCol type = ImGuiCol.Button)
|
||||||
|
{
|
||||||
|
ImGui.PushStyleColor(type, stain.RgbaColor);
|
||||||
|
if (stain.Intensity > 127)
|
||||||
|
{
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, 0xFF101010);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through a whole customization struct and fix up all settings that need fixing.
|
||||||
|
private static void FixUpAttributes(ref ActorCustomization customization)
|
||||||
|
{
|
||||||
|
var set = GlamourerPlugin.Customization.GetList(customization.Clan, customization.Gender);
|
||||||
|
foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId)))
|
||||||
|
{
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
case CustomizationId.Race: break;
|
||||||
|
case CustomizationId.Clan: break;
|
||||||
|
case CustomizationId.BodyType: break;
|
||||||
|
case CustomizationId.Gender: break;
|
||||||
|
case CustomizationId.FacialFeaturesTattoos: break;
|
||||||
|
case CustomizationId.HighlightsOnFlag: break;
|
||||||
|
case CustomizationId.Face:
|
||||||
|
if (customization.Race != Race.Hrothgar)
|
||||||
|
goto default;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
var count = set.Count(id);
|
||||||
|
if (set.DataByValue(id, customization[id], out var value) < 0)
|
||||||
|
if (count == 0)
|
||||||
|
customization[id] = 0;
|
||||||
|
else
|
||||||
|
customization[id] = set.Data(id, 0).Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change a race and fix up all required customizations afterwards.
|
||||||
|
private static bool ChangeRace(ref ActorCustomization customization, SubRace clan)
|
||||||
|
{
|
||||||
|
if (clan == customization.Clan)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var race = clan.ToRace();
|
||||||
|
customization.Race = race;
|
||||||
|
customization.Clan = clan;
|
||||||
|
|
||||||
|
customization.Gender = race switch
|
||||||
|
{
|
||||||
|
Race.Hrothgar => Gender.Male,
|
||||||
|
Race.Viera => Gender.Female,
|
||||||
|
_ => customization.Gender,
|
||||||
|
};
|
||||||
|
|
||||||
|
FixUpAttributes(ref customization);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change a gender and fix up all required customizations afterwards.
|
||||||
|
private static bool ChangeGender(ref ActorCustomization customization, Gender gender)
|
||||||
|
{
|
||||||
|
if (gender == customization.Gender)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
customization.Gender = gender;
|
||||||
|
FixUpAttributes(ref customization);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ClanName(SubRace race, Gender gender)
|
||||||
|
{
|
||||||
|
if (gender == Gender.Female)
|
||||||
|
return race switch
|
||||||
|
{
|
||||||
|
SubRace.Midlander => GlamourerPlugin.Customization.GetName(CustomName.MidlanderM),
|
||||||
|
SubRace.Highlander => GlamourerPlugin.Customization.GetName(CustomName.HighlanderM),
|
||||||
|
SubRace.Wildwood => GlamourerPlugin.Customization.GetName(CustomName.WildwoodM),
|
||||||
|
SubRace.Duskwight => GlamourerPlugin.Customization.GetName(CustomName.DuskwightM),
|
||||||
|
SubRace.Plainsfolk => GlamourerPlugin.Customization.GetName(CustomName.PlainsfolkM),
|
||||||
|
SubRace.Dunesfolk => GlamourerPlugin.Customization.GetName(CustomName.DunesfolkM),
|
||||||
|
SubRace.SeekerOfTheSun => GlamourerPlugin.Customization.GetName(CustomName.SeekerOfTheSunM),
|
||||||
|
SubRace.KeeperOfTheMoon => GlamourerPlugin.Customization.GetName(CustomName.KeeperOfTheMoonM),
|
||||||
|
SubRace.Seawolf => GlamourerPlugin.Customization.GetName(CustomName.SeawolfM),
|
||||||
|
SubRace.Hellsguard => GlamourerPlugin.Customization.GetName(CustomName.HellsguardM),
|
||||||
|
SubRace.Raen => GlamourerPlugin.Customization.GetName(CustomName.RaenM),
|
||||||
|
SubRace.Xaela => GlamourerPlugin.Customization.GetName(CustomName.XaelaM),
|
||||||
|
SubRace.Helion => GlamourerPlugin.Customization.GetName(CustomName.HelionM),
|
||||||
|
SubRace.Lost => GlamourerPlugin.Customization.GetName(CustomName.LostM),
|
||||||
|
SubRace.Rava => GlamourerPlugin.Customization.GetName(CustomName.RavaF),
|
||||||
|
SubRace.Veena => GlamourerPlugin.Customization.GetName(CustomName.VeenaF),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
return race switch
|
||||||
|
{
|
||||||
|
SubRace.Midlander => GlamourerPlugin.Customization.GetName(CustomName.MidlanderF),
|
||||||
|
SubRace.Highlander => GlamourerPlugin.Customization.GetName(CustomName.HighlanderF),
|
||||||
|
SubRace.Wildwood => GlamourerPlugin.Customization.GetName(CustomName.WildwoodF),
|
||||||
|
SubRace.Duskwight => GlamourerPlugin.Customization.GetName(CustomName.DuskwightF),
|
||||||
|
SubRace.Plainsfolk => GlamourerPlugin.Customization.GetName(CustomName.PlainsfolkF),
|
||||||
|
SubRace.Dunesfolk => GlamourerPlugin.Customization.GetName(CustomName.DunesfolkF),
|
||||||
|
SubRace.SeekerOfTheSun => GlamourerPlugin.Customization.GetName(CustomName.SeekerOfTheSunF),
|
||||||
|
SubRace.KeeperOfTheMoon => GlamourerPlugin.Customization.GetName(CustomName.KeeperOfTheMoonF),
|
||||||
|
SubRace.Seawolf => GlamourerPlugin.Customization.GetName(CustomName.SeawolfF),
|
||||||
|
SubRace.Hellsguard => GlamourerPlugin.Customization.GetName(CustomName.HellsguardF),
|
||||||
|
SubRace.Raen => GlamourerPlugin.Customization.GetName(CustomName.RaenF),
|
||||||
|
SubRace.Xaela => GlamourerPlugin.Customization.GetName(CustomName.XaelaF),
|
||||||
|
SubRace.Helion => GlamourerPlugin.Customization.GetName(CustomName.HelionM),
|
||||||
|
SubRace.Lost => GlamourerPlugin.Customization.GetName(CustomName.LostM),
|
||||||
|
SubRace.Rava => GlamourerPlugin.Customization.GetName(CustomName.RavaF),
|
||||||
|
SubRace.Veena => GlamourerPlugin.Customization.GetName(CustomName.VeenaF),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum DesignNameUse
|
||||||
|
{
|
||||||
|
SaveCurrent,
|
||||||
|
NewDesign,
|
||||||
|
DuplicateDesign,
|
||||||
|
NewFolder,
|
||||||
|
FromClipboard,
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDesignNamePopup(DesignNameUse use)
|
||||||
|
{
|
||||||
|
if (ImGui.BeginPopup($"{DesignNamePopupLabel}{use}"))
|
||||||
|
{
|
||||||
|
if (ImGui.InputText("##designName", ref _newDesignName, 64, ImGuiInputTextFlags.EnterReturnsTrue)
|
||||||
|
&& _newDesignName.Any())
|
||||||
|
{
|
||||||
|
switch (use)
|
||||||
|
{
|
||||||
|
case DesignNameUse.SaveCurrent:
|
||||||
|
SaveNewDesign(_currentSave);
|
||||||
|
break;
|
||||||
|
case DesignNameUse.NewDesign:
|
||||||
|
var empty = new CharacterSave();
|
||||||
|
empty.Load(ActorCustomization.Default);
|
||||||
|
empty.WriteCustomizations = false;
|
||||||
|
SaveNewDesign(empty);
|
||||||
|
break;
|
||||||
|
case DesignNameUse.DuplicateDesign:
|
||||||
|
SaveNewDesign(_selection!.Data.Copy());
|
||||||
|
break;
|
||||||
|
case DesignNameUse.NewFolder:
|
||||||
|
_designs.FileSystem.CreateAllFolders($"{_newDesignName}/a"); // Filename is just ignored, but all folders are created.
|
||||||
|
break;
|
||||||
|
case DesignNameUse.FromClipboard:
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var text = Clipboard.GetText();
|
||||||
|
var save = CharacterSave.FromString(text);
|
||||||
|
SaveNewDesign(save);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginLog.Information($"Could not save new Design from Clipboard:\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_newDesignName = string.Empty;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_keyboardFocus)
|
||||||
|
{
|
||||||
|
ImGui.SetKeyboardFocusHere();
|
||||||
|
_keyboardFocus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenDesignNamePopup(DesignNameUse use)
|
||||||
|
{
|
||||||
|
_newDesignName = string.Empty;
|
||||||
|
_keyboardFocus = true;
|
||||||
|
ImGui.OpenPopup($"{DesignNamePopupLabel}{use}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
Glamourer/Gui/InterfaceInitialization.cs
Normal file
83
Glamourer/Gui/InterfaceInitialization.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
|
using ImGuiNET;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Lumina.Excel.GeneratedSheets;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui
|
||||||
|
{
|
||||||
|
internal partial class Interface
|
||||||
|
{
|
||||||
|
private const float ColorButtonWidth = 22.5f;
|
||||||
|
private const float ColorComboWidth = 140f;
|
||||||
|
private const float ItemComboWidth = 300f;
|
||||||
|
|
||||||
|
private static ComboWithFilter<Stain> CreateDefaultStainCombo(IReadOnlyList<Stain> stains)
|
||||||
|
=> new("##StainCombo", ColorComboWidth, ColorButtonWidth, stains,
|
||||||
|
s => s.Name.ToString())
|
||||||
|
{
|
||||||
|
Flags = ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge,
|
||||||
|
PreList = () =>
|
||||||
|
{
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
|
||||||
|
},
|
||||||
|
PostList = () => { ImGui.PopStyleVar(3); },
|
||||||
|
CreateSelectable = s =>
|
||||||
|
{
|
||||||
|
var push = PushColor(s);
|
||||||
|
var ret = ImGui.Button($"{s.Name}##Stain{(byte) s.RowIndex}",
|
||||||
|
Vector2.UnitX * (ColorComboWidth - ImGui.GetStyle().ScrollbarSize));
|
||||||
|
ImGui.PopStyleColor(push);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
ItemsAtOnce = 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
private ComboWithFilter<Item> CreateItemCombo(EquipSlot slot, IReadOnlyList<Item> items)
|
||||||
|
=> new($"{_equipSlotNames[slot]}##Equip", ItemComboWidth, ItemComboWidth, items, i => i.Name)
|
||||||
|
{
|
||||||
|
Flags = ImGuiComboFlags.HeightLarge,
|
||||||
|
};
|
||||||
|
|
||||||
|
private (ComboWithFilter<Item>, ComboWithFilter<Stain>) CreateCombos(EquipSlot slot, IReadOnlyList<Item> items,
|
||||||
|
ComboWithFilter<Stain> defaultStain)
|
||||||
|
=> (CreateItemCombo(slot, items), new ComboWithFilter<Stain>($"##{slot}Stain", defaultStain));
|
||||||
|
|
||||||
|
private static ImGuiScene.TextureWrap? GetLegacyTattooIcon()
|
||||||
|
{
|
||||||
|
using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw");
|
||||||
|
if (resource != null)
|
||||||
|
{
|
||||||
|
var rawImage = new byte[resource.Length];
|
||||||
|
resource.Read(rawImage, 0, (int) resource.Length);
|
||||||
|
return GlamourerPlugin.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<EquipSlot, string> GetEquipSlotNames()
|
||||||
|
{
|
||||||
|
var sheet = GlamourerPlugin.PluginInterface.Data.GetExcelSheet<Addon>();
|
||||||
|
var ret = new Dictionary<EquipSlot, string>(12)
|
||||||
|
{
|
||||||
|
[EquipSlot.MainHand] = sheet.GetRow(738)?.Text.ToString() ?? "Main Hand",
|
||||||
|
[EquipSlot.OffHand] = sheet.GetRow(739)?.Text.ToString() ?? "Off Hand",
|
||||||
|
[EquipSlot.Head] = sheet.GetRow(740)?.Text.ToString() ?? "Head",
|
||||||
|
[EquipSlot.Body] = sheet.GetRow(741)?.Text.ToString() ?? "Body",
|
||||||
|
[EquipSlot.Hands] = sheet.GetRow(742)?.Text.ToString() ?? "Hands",
|
||||||
|
[EquipSlot.Legs] = sheet.GetRow(744)?.Text.ToString() ?? "Legs",
|
||||||
|
[EquipSlot.Feet] = sheet.GetRow(745)?.Text.ToString() ?? "Feet",
|
||||||
|
[EquipSlot.Ears] = sheet.GetRow(746)?.Text.ToString() ?? "Ears",
|
||||||
|
[EquipSlot.Neck] = sheet.GetRow(747)?.Text.ToString() ?? "Neck",
|
||||||
|
[EquipSlot.Wrists] = sheet.GetRow(748)?.Text.ToString() ?? "Wrists",
|
||||||
|
[EquipSlot.RFinger] = sheet.GetRow(749)?.Text.ToString() ?? "Right Ring",
|
||||||
|
[EquipSlot.LFinger] = sheet.GetRow(750)?.Text.ToString() ?? "Left Ring",
|
||||||
|
};
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Glamourer/Gui/InterfaceMiscellaneous.cs
Normal file
64
Glamourer/Gui/InterfaceMiscellaneous.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
using System;
|
||||||
|
using Dalamud.Game.ClientState.Actors.Types;
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui
|
||||||
|
{
|
||||||
|
internal partial class Interface
|
||||||
|
{
|
||||||
|
private static bool DrawCheckMark(string label, bool value, Action<bool> setter)
|
||||||
|
{
|
||||||
|
var startValue = value;
|
||||||
|
if (ImGui.Checkbox(label, ref startValue) && startValue != value)
|
||||||
|
{
|
||||||
|
setter(startValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DrawMiscellaneous(CharacterSave save, Actor? player)
|
||||||
|
{
|
||||||
|
var ret = false;
|
||||||
|
if (!ImGui.CollapsingHeader("Miscellaneous"))
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
ret |= DrawCheckMark("Hat Visible", save.HatState, v =>
|
||||||
|
{
|
||||||
|
save.HatState = v;
|
||||||
|
player?.SetHatHidden(!v);
|
||||||
|
});
|
||||||
|
|
||||||
|
ret |= DrawCheckMark("Weapon Visible", save.WeaponState, v =>
|
||||||
|
{
|
||||||
|
save.WeaponState = v;
|
||||||
|
player?.SetWeaponHidden(!v);
|
||||||
|
});
|
||||||
|
|
||||||
|
ret |= DrawCheckMark("Visor Toggled", save.VisorState, v =>
|
||||||
|
{
|
||||||
|
save.VisorState = v;
|
||||||
|
player?.SetVisorToggled(v);
|
||||||
|
});
|
||||||
|
|
||||||
|
ret |= DrawCheckMark("Is Wet", save.IsWet, v =>
|
||||||
|
{
|
||||||
|
save.IsWet = v;
|
||||||
|
player?.SetWetness(v);
|
||||||
|
});
|
||||||
|
|
||||||
|
var alpha = save.Alpha;
|
||||||
|
if (ImGui.DragFloat("Alpha", ref alpha, 0.01f, 0f, 1f, "%.2f") && alpha != save.Alpha)
|
||||||
|
{
|
||||||
|
alpha = (float) Math.Round(alpha > 1 ? 1 : alpha < 0 ? 0 : alpha, 2);
|
||||||
|
save.Alpha = alpha;
|
||||||
|
ret = true;
|
||||||
|
if (player != null)
|
||||||
|
player.Alpha() = alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,144 +1,36 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Windows.Forms;
|
||||||
using System.Runtime.InteropServices;
|
using Dalamud.Game.ClientState.Actors.Types;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Dalamud.Game.Command;
|
using Dalamud.Game.Command;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Glamourer.Customization;
|
using Glamourer.Customization;
|
||||||
|
using Glamourer.Designs;
|
||||||
|
using Glamourer.FileSystem;
|
||||||
using Glamourer.Gui;
|
using Glamourer.Gui;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
using Lumina.Data;
|
||||||
using Penumbra.Api;
|
using Penumbra.Api;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.PlayerWatch;
|
||||||
using CommandManager = Glamourer.Managers.CommandManager;
|
|
||||||
|
|
||||||
namespace Glamourer
|
namespace Glamourer
|
||||||
{
|
{
|
||||||
public class CharacterSave
|
|
||||||
{
|
|
||||||
public const byte CurrentVersion = 1;
|
|
||||||
public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + ActorCustomization.CustomizationBytes;
|
|
||||||
|
|
||||||
public const byte TotalSize = TotalSizeVersion1;
|
|
||||||
|
|
||||||
private readonly byte[] _bytes = new byte[TotalSize];
|
|
||||||
|
|
||||||
public CharacterSave()
|
|
||||||
=> _bytes[0] = CurrentVersion;
|
|
||||||
|
|
||||||
public byte Version
|
|
||||||
=> _bytes[0];
|
|
||||||
|
|
||||||
public bool WriteCustomizations
|
|
||||||
{
|
|
||||||
get => _bytes[1] != 0;
|
|
||||||
set => _bytes[1] = (byte) (value ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ActorEquipMask WriteEquipment
|
|
||||||
{
|
|
||||||
get => (ActorEquipMask) ((ushort) _bytes[2] | ((ushort) _bytes[3] << 8));
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_bytes[2] = (byte) (((ushort) value) & 0xFF);
|
|
||||||
_bytes[3] = (byte) (((ushort) value) >> 8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Load(ActorCustomization customization)
|
|
||||||
{
|
|
||||||
WriteCustomizations = true;
|
|
||||||
customization.WriteBytes(_bytes, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Load(ActorEquipment equipment, ActorEquipMask mask = ActorEquipMask.All)
|
|
||||||
{
|
|
||||||
WriteEquipment = mask;
|
|
||||||
equipment.WriteBytes(_bytes, 4 + ActorCustomization.CustomizationBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToBase64()
|
|
||||||
=> System.Convert.ToBase64String(_bytes);
|
|
||||||
|
|
||||||
public void Load(string base64)
|
|
||||||
{
|
|
||||||
var bytes = System.Convert.FromBase64String(base64);
|
|
||||||
switch (bytes[0])
|
|
||||||
{
|
|
||||||
case 1:
|
|
||||||
if (bytes.Length != TotalSizeVersion1)
|
|
||||||
throw new Exception(
|
|
||||||
$"Can not parse Base64 string into CharacterSave:\n\tInvalid size {bytes.Length} instead of {TotalSizeVersion1}.");
|
|
||||||
if (bytes[1] != 0 && bytes[1] != 1)
|
|
||||||
throw new Exception(
|
|
||||||
$"Can not parse Base64 string into CharacterSave:\n\tInvalid value {bytes[1]} in byte 2, should be either 0 or 1.");
|
|
||||||
|
|
||||||
var mask = (ActorEquipMask) ((ushort) bytes[2] | ((ushort) bytes[3] << 8));
|
|
||||||
if (!Enum.IsDefined(typeof(ActorEquipMask), mask))
|
|
||||||
throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid value {mask} in byte 3 and 4.");
|
|
||||||
bytes.CopyTo(_bytes, 0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid Version {bytes[0]}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CharacterSave FromString(string base64)
|
|
||||||
{
|
|
||||||
var ret = new CharacterSave();
|
|
||||||
ret.Load(base64);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe ActorCustomization Customizations
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var ret = new ActorCustomization();
|
|
||||||
fixed (byte* ptr = _bytes)
|
|
||||||
{
|
|
||||||
ret.Read(new IntPtr(ptr) + 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ActorEquipment Equipment
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var ret = new ActorEquipment();
|
|
||||||
ret.FromBytes(_bytes, 4 + ActorCustomization.CustomizationBytes);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class Glamourer
|
|
||||||
{
|
|
||||||
private readonly DalamudPluginInterface _pluginInterface;
|
|
||||||
private readonly CommandManager _commands;
|
|
||||||
|
|
||||||
public Glamourer(DalamudPluginInterface pi)
|
|
||||||
{
|
|
||||||
_pluginInterface = pi;
|
|
||||||
_commands = new CommandManager(_pluginInterface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GlamourerPlugin : IDalamudPlugin
|
public class GlamourerPlugin : IDalamudPlugin
|
||||||
{
|
{
|
||||||
public const int RequiredPenumbraShareVersion = 1;
|
public const int RequiredPenumbraShareVersion = 1;
|
||||||
|
|
||||||
|
private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],<Name for Save>";
|
||||||
|
|
||||||
public string Name
|
public string Name
|
||||||
=> "Glamourer";
|
=> "Glamourer";
|
||||||
|
|
||||||
public static DalamudPluginInterface PluginInterface = null!;
|
public static DalamudPluginInterface PluginInterface = null!;
|
||||||
private Glamourer _glamourer = null!;
|
|
||||||
private Interface _interface = null!;
|
private Interface _interface = null!;
|
||||||
public static ICustomizationManager Customization = null!;
|
public static ICustomizationManager Customization = null!;
|
||||||
|
public DesignManager Designs = null!;
|
||||||
|
public IPlayerWatcher PlayerWatcher = null!;
|
||||||
|
|
||||||
public static string Version = string.Empty;
|
public static string Version = string.Empty;
|
||||||
|
|
||||||
|
|
@ -167,12 +59,18 @@ namespace Glamourer
|
||||||
if (button == MouseButton.Right && it is Lumina.Excel.GeneratedSheets.Item item)
|
if (button == MouseButton.Right && it is Lumina.Excel.GeneratedSheets.Item item)
|
||||||
{
|
{
|
||||||
var actors = PluginInterface.ClientState.Actors;
|
var actors = PluginInterface.ClientState.Actors;
|
||||||
var player = actors[Interface.GPoseActorId] ?? actors[0];
|
var gPose = actors[Interface.GPoseActorId];
|
||||||
if (player != null)
|
var player = actors[0];
|
||||||
{
|
|
||||||
var writeItem = new Item(item, string.Empty);
|
var writeItem = new Item(item, string.Empty);
|
||||||
|
if (gPose != null)
|
||||||
|
{
|
||||||
|
writeItem.Write(gPose.Address);
|
||||||
|
UpdateActors(gPose, player);
|
||||||
|
}
|
||||||
|
else if (player != null)
|
||||||
|
{
|
||||||
writeItem.Write(player.Address);
|
writeItem.Write(player.Address);
|
||||||
_interface.UpdateActors(player);
|
UpdateActors(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,32 +142,187 @@ namespace Glamourer
|
||||||
Customization = CustomizationManager.Create(PluginInterface);
|
Customization = CustomizationManager.Create(PluginInterface);
|
||||||
SetDalamud(PluginInterface);
|
SetDalamud(PluginInterface);
|
||||||
SetPlugins(PluginInterface);
|
SetPlugins(PluginInterface);
|
||||||
|
Designs = new DesignManager(PluginInterface);
|
||||||
GetPenumbra();
|
GetPenumbra();
|
||||||
|
PlayerWatcher = PlayerWatchFactory.Create(PluginInterface);
|
||||||
|
|
||||||
PluginInterface.CommandManager.AddHandler("/glamour", new CommandInfo(OnCommand)
|
PluginInterface.CommandManager.AddHandler("/glamourer", new CommandInfo(OnGlamourer)
|
||||||
{
|
{
|
||||||
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
|
HelpMessage = "Open or close the Glamourer window.",
|
||||||
|
});
|
||||||
|
PluginInterface.CommandManager.AddHandler("/glamour", new CommandInfo(OnGlamour)
|
||||||
|
{
|
||||||
|
HelpMessage = $"Use Glamourer Functions: {HelpString}",
|
||||||
});
|
});
|
||||||
|
|
||||||
_glamourer = new Glamourer(PluginInterface);
|
_interface = new Interface(this);
|
||||||
_interface = new Interface();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnCommand(string command, string arguments)
|
public void OnGlamourer(string command, string arguments)
|
||||||
|
=> _interface?.ToggleVisibility(null!, null!);
|
||||||
|
|
||||||
|
private Actor? GetActor(string name)
|
||||||
{
|
{
|
||||||
if (GetPenumbra())
|
var lowerName = name.ToLowerInvariant();
|
||||||
Penumbra!.RedrawAll(RedrawType.WithSettings);
|
return lowerName switch
|
||||||
else
|
{
|
||||||
PluginLog.Information("Could not get Penumbra.");
|
"" => null,
|
||||||
|
"<me>" => PluginInterface.ClientState.Actors[Interface.GPoseActorId] ?? PluginInterface.ClientState.LocalPlayer,
|
||||||
|
"self" => PluginInterface.ClientState.Actors[Interface.GPoseActorId] ?? PluginInterface.ClientState.LocalPlayer,
|
||||||
|
"<t>" => PluginInterface.ClientState.Targets.CurrentTarget,
|
||||||
|
"target" => PluginInterface.ClientState.Targets.CurrentTarget,
|
||||||
|
"<f>" => PluginInterface.ClientState.Targets.FocusTarget,
|
||||||
|
"focus" => PluginInterface.ClientState.Targets.FocusTarget,
|
||||||
|
"<mo>" => PluginInterface.ClientState.Targets.MouseOverTarget,
|
||||||
|
"mouseover" => PluginInterface.ClientState.Targets.MouseOverTarget,
|
||||||
|
_ => PluginInterface.ClientState.Actors.LastOrDefault(
|
||||||
|
a => string.Equals(a.Name, lowerName, StringComparison.InvariantCultureIgnoreCase)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void CopyToClipboard(Actor actor)
|
||||||
|
{
|
||||||
|
var save = new CharacterSave();
|
||||||
|
save.LoadActor(actor);
|
||||||
|
Clipboard.SetText(save.ToBase64());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyCommand(Actor actor, string target)
|
||||||
|
{
|
||||||
|
CharacterSave? save = null;
|
||||||
|
if (target.ToLowerInvariant() == "clipboard")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
save = CharacterSave.FromString(Clipboard.GetText());
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
PluginInterface.Framework.Gui.Chat.PrintError("Clipboard does not contain a valid customization string.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d)
|
||||||
|
{
|
||||||
|
PluginInterface.Framework.Gui.Chat.PrintError("The given path to a saved design does not exist or does not point to a design.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
save = d.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
save?.Apply(actor);
|
||||||
|
UpdateActors(actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveCommand(Actor actor, string path)
|
||||||
|
{
|
||||||
|
var save = new CharacterSave();
|
||||||
|
save.LoadActor(actor);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (folder, name) = Designs.FileSystem.CreateAllFolders(path);
|
||||||
|
var design = new Design(folder, name) { Data = save };
|
||||||
|
folder.FindOrAddChild(design);
|
||||||
|
Designs.Designs.Add(design.FullName(), design.Data);
|
||||||
|
Designs.SaveToFile();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
PluginInterface.Framework.Gui.Chat.PrintError("Could not save file:");
|
||||||
|
PluginInterface.Framework.Gui.Chat.PrintError($" {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGlamour(string command, string arguments)
|
||||||
|
{
|
||||||
|
static void PrintHelp()
|
||||||
|
{
|
||||||
|
PluginInterface.Framework.Gui.Chat.Print("Usage:");
|
||||||
|
PluginInterface.Framework.Gui.Chat.Print($" {HelpString}");
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments = arguments.Trim();
|
||||||
|
if (!arguments.Any())
|
||||||
|
{
|
||||||
|
PrintHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var split = arguments.Split(new[]
|
||||||
|
{
|
||||||
|
',',
|
||||||
|
}, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
if (split.Length < 2)
|
||||||
|
{
|
||||||
|
PrintHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actor = GetActor(split[1]);
|
||||||
|
if (actor == null)
|
||||||
|
{
|
||||||
|
PluginInterface.Framework.Gui.Chat.Print($"Could not find actor for {split[1]}.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (split[0].ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "copy":
|
||||||
|
CopyToClipboard(actor);
|
||||||
|
return;
|
||||||
|
case "apply":
|
||||||
|
{
|
||||||
|
if (split.Length < 3)
|
||||||
|
{
|
||||||
|
PluginInterface.Framework.Gui.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ApplyCommand(actor, split[2]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case "save":
|
||||||
|
{
|
||||||
|
if (split.Length < 3)
|
||||||
|
{
|
||||||
|
PluginInterface.Framework.Gui.Chat.Print("Saving requires a name for the save.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SaveCommand(actor, split[2]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
PrintHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
PlayerWatcher?.Dispose();
|
||||||
UnregisterFunctions();
|
UnregisterFunctions();
|
||||||
_interface?.Dispose();
|
_interface?.Dispose();
|
||||||
PluginInterface.CommandManager.RemoveHandler("/glamour");
|
PluginInterface.CommandManager.RemoveHandler("/glamour");
|
||||||
|
PluginInterface.CommandManager.RemoveHandler("/glamourer");
|
||||||
PluginInterface.Dispose();
|
PluginInterface.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update actors without triggering PlayerWatcher Events,
|
||||||
|
// then manually redraw using Penumbra.
|
||||||
|
public void UpdateActors(Actor actor, Actor? gPoseOriginalActor = null)
|
||||||
|
{
|
||||||
|
var newEquip = PlayerWatcher.UpdateActorWithoutEvent(actor);
|
||||||
|
Penumbra?.RedrawActor(actor, RedrawType.WithSettings);
|
||||||
|
|
||||||
|
// Special case for carrying over changes to the gPose actor to the regular player actor, too.
|
||||||
|
if (gPoseOriginalActor != null)
|
||||||
|
{
|
||||||
|
newEquip.Write(gPoseOriginalActor.Address);
|
||||||
|
PlayerWatcher.UpdateActorWithoutEvent(gPoseOriginalActor);
|
||||||
|
Penumbra?.RedrawActor(gPoseOriginalActor, RedrawType.AfterGPoseWithSettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Glamourer.SeFunctions;
|
|
||||||
|
|
||||||
namespace Glamourer.Managers
|
|
||||||
{
|
|
||||||
public class CommandManager
|
|
||||||
{
|
|
||||||
private readonly ProcessChatBox _processChatBox;
|
|
||||||
private readonly Dalamud.Game.Command.CommandManager _dalamudCommands;
|
|
||||||
|
|
||||||
private readonly IntPtr _uiModulePtr;
|
|
||||||
|
|
||||||
public CommandManager(DalamudPluginInterface pi, BaseUiObject baseUiObject, GetUiModule getUiModule, ProcessChatBox processChatBox)
|
|
||||||
{
|
|
||||||
_dalamudCommands = pi.CommandManager;
|
|
||||||
_processChatBox = processChatBox;
|
|
||||||
_uiModulePtr = getUiModule.Invoke(Marshal.ReadIntPtr(baseUiObject.Address));
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandManager(DalamudPluginInterface pi)
|
|
||||||
: this(pi, new BaseUiObject(pi.TargetModuleScanner), new GetUiModule(pi.TargetModuleScanner),
|
|
||||||
new ProcessChatBox(pi.TargetModuleScanner))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public bool Execute(string message)
|
|
||||||
{
|
|
||||||
// First try to process the command through Dalamud.
|
|
||||||
if (_dalamudCommands.ProcessCommand(message))
|
|
||||||
{
|
|
||||||
PluginLog.Verbose("Executed Dalamud command \"{Message:l}\".", message);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_uiModulePtr == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
PluginLog.Error("Can not execute \"{Message:l}\" because no uiModulePtr is available.", message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then prepare a string to send to the game itself.
|
|
||||||
var (text, length) = PrepareString(message);
|
|
||||||
var payload = PrepareContainer(text, length);
|
|
||||||
|
|
||||||
_processChatBox.Invoke(_uiModulePtr, payload, IntPtr.Zero, (byte) 0);
|
|
||||||
|
|
||||||
Marshal.FreeHGlobal(payload);
|
|
||||||
Marshal.FreeHGlobal(text);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (IntPtr, long) PrepareString(string message)
|
|
||||||
{
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(message);
|
|
||||||
var mem = Marshal.AllocHGlobal(bytes.Length + 30);
|
|
||||||
Marshal.Copy(bytes, 0, mem, bytes.Length);
|
|
||||||
Marshal.WriteByte(mem + bytes.Length, 0);
|
|
||||||
return (mem, bytes.Length + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IntPtr PrepareContainer(IntPtr message, long length)
|
|
||||||
{
|
|
||||||
var mem = Marshal.AllocHGlobal(400);
|
|
||||||
Marshal.WriteInt64(mem, message.ToInt64());
|
|
||||||
Marshal.WriteInt64(mem + 0x8, 64);
|
|
||||||
Marshal.WriteInt64(mem + 0x10, length);
|
|
||||||
Marshal.WriteInt64(mem + 0x18, 0);
|
|
||||||
return mem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
using Dalamud.Game;
|
|
||||||
|
|
||||||
namespace Glamourer.SeFunctions
|
|
||||||
{
|
|
||||||
public sealed class BaseUiObject : SeAddressBase
|
|
||||||
{
|
|
||||||
public BaseUiObject(SigScanner sigScanner)
|
|
||||||
: base(sigScanner, "48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 48 83 C1 10 E8")
|
|
||||||
{ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
using Dalamud.Game;
|
|
||||||
|
|
||||||
namespace Glamourer.SeFunctions
|
|
||||||
{
|
|
||||||
public delegate IntPtr GetUiModuleDelegate(IntPtr baseUiObj);
|
|
||||||
|
|
||||||
public sealed class GetUiModule : SeFunctionBase<GetUiModuleDelegate>
|
|
||||||
{
|
|
||||||
public GetUiModule(SigScanner sigScanner)
|
|
||||||
: base(sigScanner, "E8 ?? ?? ?? ?? 48 83 7F ?? 00 48 8B F0")
|
|
||||||
{ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
using Dalamud.Game;
|
|
||||||
|
|
||||||
namespace Glamourer.SeFunctions
|
|
||||||
{
|
|
||||||
public delegate IntPtr ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unk1, byte unk2);
|
|
||||||
|
|
||||||
public sealed class ProcessChatBox : SeFunctionBase<ProcessChatBoxDelegate>
|
|
||||||
{
|
|
||||||
public ProcessChatBox(SigScanner sigScanner)
|
|
||||||
: base(sigScanner, "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9")
|
|
||||||
{ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
using System;
|
|
||||||
using Dalamud.Game;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
|
|
||||||
namespace Glamourer.SeFunctions
|
|
||||||
{
|
|
||||||
public class SeAddressBase
|
|
||||||
{
|
|
||||||
public readonly IntPtr Address;
|
|
||||||
|
|
||||||
public SeAddressBase(SigScanner sigScanner, string signature, int offset = 0)
|
|
||||||
{
|
|
||||||
Address = sigScanner.GetStaticAddressFromSig(signature);
|
|
||||||
if (Address != IntPtr.Zero)
|
|
||||||
Address += offset;
|
|
||||||
var baseOffset = (ulong) Address.ToInt64() - (ulong) sigScanner.Module.BaseAddress.ToInt64();
|
|
||||||
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{baseOffset:X16}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Dalamud.Game;
|
|
||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Plugin;
|
|
||||||
|
|
||||||
namespace Glamourer.SeFunctions
|
|
||||||
{
|
|
||||||
public class SeFunctionBase<T> where T : Delegate
|
|
||||||
{
|
|
||||||
public IntPtr Address;
|
|
||||||
protected T? FuncDelegate;
|
|
||||||
|
|
||||||
public SeFunctionBase(SigScanner sigScanner, int offset)
|
|
||||||
{
|
|
||||||
Address = sigScanner.Module.BaseAddress + offset;
|
|
||||||
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{offset:X16}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public SeFunctionBase(SigScanner sigScanner, string signature, int offset = 0)
|
|
||||||
{
|
|
||||||
Address = sigScanner.ScanText(signature);
|
|
||||||
if (Address != IntPtr.Zero)
|
|
||||||
Address += offset;
|
|
||||||
var baseOffset = (ulong) Address.ToInt64() - (ulong) sigScanner.Module.BaseAddress.ToInt64();
|
|
||||||
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{baseOffset:X16}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public T? Delegate()
|
|
||||||
{
|
|
||||||
if (FuncDelegate != null)
|
|
||||||
return FuncDelegate;
|
|
||||||
|
|
||||||
if (Address != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
FuncDelegate = Marshal.GetDelegateForFunctionPointer<T>(Address);
|
|
||||||
return FuncDelegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLog.Error($"Trying to generate delegate for {GetType().Name}, but no pointer available.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public dynamic? Invoke(params dynamic[] parameters)
|
|
||||||
{
|
|
||||||
if (FuncDelegate != null)
|
|
||||||
return FuncDelegate.DynamicInvoke(parameters);
|
|
||||||
|
|
||||||
if (Address != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
FuncDelegate = Marshal.GetDelegateForFunctionPointer<T>(Address);
|
|
||||||
return FuncDelegate!.DynamicInvoke(parameters);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PluginLog.Error($"Trying to call {GetType().Name}, but no pointer available.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Hook<T>? CreateHook(T detour)
|
|
||||||
{
|
|
||||||
if (Address != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
var hook = new Hook<T>(Address, detour);
|
|
||||||
hook.Enable();
|
|
||||||
PluginLog.Debug($"Hooked onto {GetType().Name} at address 0x{Address.ToInt64():X16}.");
|
|
||||||
return hook;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginLog.Error($"Trying to create Hook for {GetType().Name}, but no pointer available.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
"Name": "Glamourer",
|
"Name": "Glamourer",
|
||||||
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
|
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
|
||||||
"InternalName": "Glamourer",
|
"InternalName": "Glamourer",
|
||||||
"AssemblyVersion": "0.0.2.0",
|
"AssemblyVersion": "0.0.3.0",
|
||||||
"TestingAssemblyVersion": "0.0.2.0",
|
"TestingAssemblyVersion": "0.0.3.0",
|
||||||
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
|
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"DalamudApiLevel": 3,
|
"DalamudApiLevel": 3,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue