Merge branch 'api4' into main

This commit is contained in:
Ottermandias 2021-10-08 18:29:41 +02:00
commit b65658ef63
44 changed files with 1822 additions and 978 deletions

View file

@ -1,168 +0,0 @@
using System;
using System.ComponentModel;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer
{
public static class WriteExtensions
{
private static unsafe void Write(IntPtr actorPtr, EquipSlot slot, SetId? id, WeaponType? type, ushort? variant, StainId? stain)
{
void WriteWeapon(int offset)
{
var address = (byte*) actorPtr + offset;
if (id.HasValue)
*(ushort*) address = (ushort) id.Value;
if (type.HasValue)
*(ushort*) (address + 2) = (ushort) type.Value;
if (variant.HasValue)
*(ushort*) (address + 4) = variant.Value;
if (stain.HasValue)
*(address + 6) = (byte) stain.Value;
}
void WriteEquip(int offset)
{
var address = (byte*) actorPtr + offset;
if (id.HasValue)
*(ushort*) address = (ushort) id.Value;
if (variant < byte.MaxValue)
*(address + 2) = (byte) variant.Value;
if (stain.HasValue)
*(address + 3) = (byte) stain.Value;
}
switch (slot)
{
case EquipSlot.MainHand:
WriteWeapon(ActorEquipment.MainWeaponOffset);
break;
case EquipSlot.OffHand:
WriteWeapon(ActorEquipment.OffWeaponOffset);
break;
case EquipSlot.Head:
WriteEquip(ActorEquipment.EquipmentOffset);
break;
case EquipSlot.Body:
WriteEquip(ActorEquipment.EquipmentOffset + 4);
break;
case EquipSlot.Hands:
WriteEquip(ActorEquipment.EquipmentOffset + 8);
break;
case EquipSlot.Legs:
WriteEquip(ActorEquipment.EquipmentOffset + 12);
break;
case EquipSlot.Feet:
WriteEquip(ActorEquipment.EquipmentOffset + 16);
break;
case EquipSlot.Ears:
WriteEquip(ActorEquipment.EquipmentOffset + 20);
break;
case EquipSlot.Neck:
WriteEquip(ActorEquipment.EquipmentOffset + 24);
break;
case EquipSlot.Wrists:
WriteEquip(ActorEquipment.EquipmentOffset + 28);
break;
case EquipSlot.RFinger:
WriteEquip(ActorEquipment.EquipmentOffset + 32);
break;
case EquipSlot.LFinger:
WriteEquip(ActorEquipment.EquipmentOffset + 36);
break;
default: throw new InvalidEnumArgumentException();
}
}
public static void Write(this Stain stain, IntPtr actorPtr, EquipSlot slot)
=> Write(actorPtr, slot, null, null, null, stain.RowIndex);
public static void Write(this Item item, IntPtr actorAddress)
{
var (id, type, variant) = item.MainModel;
Write(actorAddress, item.EquippableTo, id, type, variant, null);
if (item.EquippableTo == EquipSlot.MainHand && item.HasSubModel)
{
var (subId, subType, subVariant) = item.SubModel;
Write(actorAddress, EquipSlot.OffHand, subId, subType, subVariant, null);
}
}
public static void Write(this ActorArmor armor, IntPtr actorAddress, EquipSlot slot)
=> Write(actorAddress, slot, armor.Set, null, armor.Variant, armor.Stain);
public static void Write(this ActorWeapon weapon, IntPtr actorAddress, EquipSlot slot)
=> Write(actorAddress, slot, weapon.Set, weapon.Type, weapon.Variant, weapon.Stain);
public static unsafe void Write(this ActorEquipment equip, IntPtr actorAddress)
{
if (equip.IsSet == 0)
return;
Write(actorAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, equip.MainHand.Stain);
Write(actorAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, equip.OffHand.Stain);
fixed (ActorArmor* equipment = &equip.Head)
{
Buffer.MemoryCopy(equipment, (byte*) actorAddress + ActorEquipment.EquipmentOffset,
ActorEquipment.EquipmentSlots * sizeof(ActorArmor), ActorEquipment.EquipmentSlots * sizeof(ActorArmor));
}
}
public static void Write(this ActorEquipment equip, IntPtr actorAddress, ActorEquipMask models, ActorEquipMask stains)
{
if (models == ActorEquipMask.All && stains == ActorEquipMask.All)
{
equip.Write(actorAddress);
return;
}
if (models.HasFlag(ActorEquipMask.MainHand))
Write(actorAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, null);
if (stains.HasFlag(ActorEquipMask.MainHand))
Write(actorAddress, EquipSlot.MainHand, null, null, null, equip.MainHand.Stain);
if (models.HasFlag(ActorEquipMask.OffHand))
Write(actorAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, null);
if (stains.HasFlag(ActorEquipMask.OffHand))
Write(actorAddress, EquipSlot.OffHand, null, null, null, equip.OffHand.Stain);
if (models.HasFlag(ActorEquipMask.Head))
Write(actorAddress, EquipSlot.Head, equip.Head.Set, null, equip.Head.Variant, null);
if (stains.HasFlag(ActorEquipMask.Head))
Write(actorAddress, EquipSlot.Head, null, null, null, equip.Head.Stain);
if (models.HasFlag(ActorEquipMask.Body))
Write(actorAddress, EquipSlot.Body, equip.Body.Set, null, equip.Body.Variant, null);
if (stains.HasFlag(ActorEquipMask.Body))
Write(actorAddress, EquipSlot.Body, null, null, null, equip.Body.Stain);
if (models.HasFlag(ActorEquipMask.Hands))
Write(actorAddress, EquipSlot.Hands, equip.Hands.Set, null, equip.Hands.Variant, null);
if (stains.HasFlag(ActorEquipMask.Hands))
Write(actorAddress, EquipSlot.Hands, null, null, null, equip.Hands.Stain);
if (models.HasFlag(ActorEquipMask.Legs))
Write(actorAddress, EquipSlot.Legs, equip.Legs.Set, null, equip.Legs.Variant, null);
if (stains.HasFlag(ActorEquipMask.Legs))
Write(actorAddress, EquipSlot.Legs, null, null, null, equip.Legs.Stain);
if (models.HasFlag(ActorEquipMask.Feet))
Write(actorAddress, EquipSlot.Feet, equip.Feet.Set, null, equip.Feet.Variant, null);
if (stains.HasFlag(ActorEquipMask.Feet))
Write(actorAddress, EquipSlot.Feet, null, null, null, equip.Feet.Stain);
if (models.HasFlag(ActorEquipMask.Ears))
Write(actorAddress, EquipSlot.Ears, equip.Ears.Set, null, equip.Ears.Variant, null);
if (models.HasFlag(ActorEquipMask.Neck))
Write(actorAddress, EquipSlot.Neck, equip.Neck.Set, null, equip.Neck.Variant, null);
if (models.HasFlag(ActorEquipMask.Wrists))
Write(actorAddress, EquipSlot.Wrists, equip.Wrists.Set, null, equip.Wrists.Variant, null);
if (models.HasFlag(ActorEquipMask.LFinger))
Write(actorAddress, EquipSlot.LFinger, equip.LFinger.Set, null, equip.LFinger.Variant, null);
if (models.HasFlag(ActorEquipMask.RFinger))
Write(actorAddress, EquipSlot.RFinger, equip.RFinger.Set, null, equip.RFinger.Variant, null);
}
}
}

View file

@ -0,0 +1,168 @@
using System;
using System.ComponentModel;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer
{
public static class WriteExtensions
{
private static unsafe void Write(IntPtr characterPtr, EquipSlot slot, SetId? id, WeaponType? type, ushort? variant, StainId? stain)
{
void WriteWeapon(int offset)
{
var address = (byte*) characterPtr + offset;
if (id.HasValue)
*(ushort*) address = (ushort) id.Value;
if (type.HasValue)
*(ushort*) (address + 2) = (ushort) type.Value;
if (variant.HasValue)
*(ushort*) (address + 4) = variant.Value;
if (stain.HasValue)
*(address + 6) = (byte) stain.Value;
}
void WriteEquip(int offset)
{
var address = (byte*) characterPtr + offset;
if (id.HasValue)
*(ushort*) address = (ushort) id.Value;
if (variant < byte.MaxValue)
*(address + 2) = (byte) variant.Value;
if (stain.HasValue)
*(address + 3) = (byte) stain.Value;
}
switch (slot)
{
case EquipSlot.MainHand:
WriteWeapon(CharacterEquipment.MainWeaponOffset);
break;
case EquipSlot.OffHand:
WriteWeapon(CharacterEquipment.OffWeaponOffset);
break;
case EquipSlot.Head:
WriteEquip(CharacterEquipment.EquipmentOffset);
break;
case EquipSlot.Body:
WriteEquip(CharacterEquipment.EquipmentOffset + 4);
break;
case EquipSlot.Hands:
WriteEquip(CharacterEquipment.EquipmentOffset + 8);
break;
case EquipSlot.Legs:
WriteEquip(CharacterEquipment.EquipmentOffset + 12);
break;
case EquipSlot.Feet:
WriteEquip(CharacterEquipment.EquipmentOffset + 16);
break;
case EquipSlot.Ears:
WriteEquip(CharacterEquipment.EquipmentOffset + 20);
break;
case EquipSlot.Neck:
WriteEquip(CharacterEquipment.EquipmentOffset + 24);
break;
case EquipSlot.Wrists:
WriteEquip(CharacterEquipment.EquipmentOffset + 28);
break;
case EquipSlot.RFinger:
WriteEquip(CharacterEquipment.EquipmentOffset + 32);
break;
case EquipSlot.LFinger:
WriteEquip(CharacterEquipment.EquipmentOffset + 36);
break;
default: throw new InvalidEnumArgumentException();
}
}
public static void Write(this Stain stain, IntPtr characterPtr, EquipSlot slot)
=> Write(characterPtr, slot, null, null, null, stain.RowIndex);
public static void Write(this Item item, IntPtr characterAddress)
{
var (id, type, variant) = item.MainModel;
Write(characterAddress, item.EquippableTo, id, type, variant, null);
if (item.EquippableTo == EquipSlot.MainHand && item.HasSubModel)
{
var (subId, subType, subVariant) = item.SubModel;
Write(characterAddress, EquipSlot.OffHand, subId, subType, subVariant, null);
}
}
public static void Write(this CharacterArmor armor, IntPtr characterAddress, EquipSlot slot)
=> Write(characterAddress, slot, armor.Set, null, armor.Variant, armor.Stain);
public static void Write(this CharacterWeapon weapon, IntPtr characterAddress, EquipSlot slot)
=> Write(characterAddress, slot, weapon.Set, weapon.Type, weapon.Variant, weapon.Stain);
public static unsafe void Write(this CharacterEquipment equip, IntPtr characterAddress)
{
if (equip.IsSet == 0)
return;
Write(characterAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, equip.MainHand.Stain);
Write(characterAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, equip.OffHand.Stain);
fixed (CharacterArmor* equipment = &equip.Head)
{
Buffer.MemoryCopy(equipment, (byte*) characterAddress + CharacterEquipment.EquipmentOffset,
CharacterEquipment.EquipmentSlots * sizeof(CharacterArmor), CharacterEquipment.EquipmentSlots * sizeof(CharacterArmor));
}
}
public static void Write(this CharacterEquipment equip, IntPtr characterAddress, CharacterEquipMask models, CharacterEquipMask stains)
{
if (models == CharacterEquipMask.All && stains == CharacterEquipMask.All)
{
equip.Write(characterAddress);
return;
}
if (models.HasFlag(CharacterEquipMask.MainHand))
Write(characterAddress, EquipSlot.MainHand, equip.MainHand.Set, equip.MainHand.Type, equip.MainHand.Variant, null);
if (stains.HasFlag(CharacterEquipMask.MainHand))
Write(characterAddress, EquipSlot.MainHand, null, null, null, equip.MainHand.Stain);
if (models.HasFlag(CharacterEquipMask.OffHand))
Write(characterAddress, EquipSlot.OffHand, equip.OffHand.Set, equip.OffHand.Type, equip.OffHand.Variant, null);
if (stains.HasFlag(CharacterEquipMask.OffHand))
Write(characterAddress, EquipSlot.OffHand, null, null, null, equip.OffHand.Stain);
if (models.HasFlag(CharacterEquipMask.Head))
Write(characterAddress, EquipSlot.Head, equip.Head.Set, null, equip.Head.Variant, null);
if (stains.HasFlag(CharacterEquipMask.Head))
Write(characterAddress, EquipSlot.Head, null, null, null, equip.Head.Stain);
if (models.HasFlag(CharacterEquipMask.Body))
Write(characterAddress, EquipSlot.Body, equip.Body.Set, null, equip.Body.Variant, null);
if (stains.HasFlag(CharacterEquipMask.Body))
Write(characterAddress, EquipSlot.Body, null, null, null, equip.Body.Stain);
if (models.HasFlag(CharacterEquipMask.Hands))
Write(characterAddress, EquipSlot.Hands, equip.Hands.Set, null, equip.Hands.Variant, null);
if (stains.HasFlag(CharacterEquipMask.Hands))
Write(characterAddress, EquipSlot.Hands, null, null, null, equip.Hands.Stain);
if (models.HasFlag(CharacterEquipMask.Legs))
Write(characterAddress, EquipSlot.Legs, equip.Legs.Set, null, equip.Legs.Variant, null);
if (stains.HasFlag(CharacterEquipMask.Legs))
Write(characterAddress, EquipSlot.Legs, null, null, null, equip.Legs.Stain);
if (models.HasFlag(CharacterEquipMask.Feet))
Write(characterAddress, EquipSlot.Feet, equip.Feet.Set, null, equip.Feet.Variant, null);
if (stains.HasFlag(CharacterEquipMask.Feet))
Write(characterAddress, EquipSlot.Feet, null, null, null, equip.Feet.Stain);
if (models.HasFlag(CharacterEquipMask.Ears))
Write(characterAddress, EquipSlot.Ears, equip.Ears.Set, null, equip.Ears.Variant, null);
if (models.HasFlag(CharacterEquipMask.Neck))
Write(characterAddress, EquipSlot.Neck, equip.Neck.Set, null, equip.Neck.Variant, null);
if (models.HasFlag(CharacterEquipMask.Wrists))
Write(characterAddress, EquipSlot.Wrists, equip.Wrists.Set, null, equip.Wrists.Variant, null);
if (models.HasFlag(CharacterEquipMask.LFinger))
Write(characterAddress, EquipSlot.LFinger, equip.LFinger.Set, null, equip.LFinger.Variant, null);
if (models.HasFlag(CharacterEquipMask.RFinger))
Write(characterAddress, EquipSlot.RFinger, equip.RFinger.Set, null, equip.RFinger.Variant, null);
}
}
}

View file

@ -3,7 +3,7 @@ using System;
namespace Glamourer
{
[Flags]
public enum ActorEquipMask : ushort
public enum CharacterEquipMask : ushort
{
None = 0,
MainHand = 0b000000000001,

View file

@ -1,32 +1,32 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Objects.Types;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
{
public unsafe struct LazyCustomization
{
public ActorCustomization* Address;
public CharacterCustomization* Address;
public LazyCustomization(IntPtr actorPtr)
=> Address = (ActorCustomization*) (actorPtr + ActorCustomization.CustomizationOffset);
public LazyCustomization(IntPtr characterPtr)
=> Address = (CharacterCustomization*) (characterPtr + CharacterCustomization.CustomizationOffset);
public ref ActorCustomization Value
public ref CharacterCustomization Value
=> ref *Address;
public LazyCustomization(ActorCustomization data)
public LazyCustomization(CharacterCustomization data)
=> Address = &data;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ActorCustomization
public struct CharacterCustomization
{
public const int CustomizationOffset = 0x1898;
public const int CustomizationBytes = 26;
public static ActorCustomization Default = new()
public static CharacterCustomization Default = new()
{
Race = Race.Hyur,
Gender = Gender.Male,
@ -150,13 +150,13 @@ namespace Glamourer.Customization
}
}
public void Read(Actor actor)
=> Read(actor.Address + CustomizationOffset);
public void Read(Character character)
=> Read(character.Address + CustomizationOffset);
public ActorCustomization(Actor actor)
public CharacterCustomization(Character character)
: this()
{
Read(actor.Address + CustomizationOffset);
Read(character.Address + CustomizationOffset);
}
public byte this[CustomizationId id]
@ -278,11 +278,11 @@ namespace Glamourer.Customization
}
}
public unsafe void Write(IntPtr actorAddress)
public unsafe void Write(IntPtr characterAddress)
{
fixed (Race* ptr = &Race)
{
Buffer.MemoryCopy(ptr, (byte*) actorAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes);
Buffer.MemoryCopy(ptr, (byte*) characterAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes);
}
}

View file

@ -1,4 +1,5 @@
using Dalamud.Plugin;
using Dalamud.Data;
using Dalamud.Plugin;
namespace Glamourer
{
@ -7,9 +8,9 @@ namespace Glamourer
public readonly Lumina.Data.FileResource File;
public readonly uint[] RgbaColors;
public CmpFile(DalamudPluginInterface pi)
public CmpFile(DataManager gameData)
{
File = pi.Data.GetFile("chara/xls/charamake/human.cmp");
File = gameData.GetFile("chara/xls/charamake/human.cmp")!;
RgbaColors = new uint[File.Data.Length >> 2];
for (var i = 0; i < File.Data.Length; i += 4)
{

View file

@ -8,6 +8,7 @@
OddEyes,
IrisSmall,
IrisLarge,
IrisSize,
MidlanderM,
HighlanderM,
WildwoodM,

View file

@ -1,4 +1,6 @@
using System.Collections.Generic;
using Dalamud;
using Dalamud.Data;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
@ -11,9 +13,9 @@ namespace Glamourer.Customization
private CustomizationManager()
{ }
public static ICustomizationManager Create(DalamudPluginInterface pi)
public static ICustomizationManager Create(DalamudPluginInterface pi, DataManager gameData, ClientLanguage language)
{
_options ??= new CustomizationOptions(pi);
_options ??= new CustomizationOptions(pi, gameData, language);
return new CustomizationManager();
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud;
using Dalamud.Data;
using Dalamud.Plugin;
using Glamourer.Util;
using Lumina.Data;
@ -50,7 +51,7 @@ namespace Glamourer.Customization
private Customization[] GetHairStyles(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint) race - 1) * 2 - 1 + (uint) gender);
var row = _hairSheet.GetRow(((uint) race - 1) * 2 - 1 + (uint) gender)!;
var hairList = new List<Customization>(row.Unknown30);
for (var i = 0; i < row.Unknown30; ++i)
{
@ -129,7 +130,7 @@ namespace Glamourer.Customization
private CustomizationSet GetSet(SubRace race, Gender gender)
{
var (skin, hair) = GetColors(race, gender);
var row = _listSheet.GetRow(((uint) race - 1) * 2 - 1 + (uint) gender);
var row = _listSheet.GetRow(((uint) race - 1) * 2 - 1 + (uint) gender)!;
var set = new CustomizationSet(race, gender)
{
HairStyles = race.ToRace() == Race.Hrothgar ? HrothgarFaces(row) : GetHairStyles(race, gender),
@ -190,7 +191,7 @@ namespace Glamourer.Customization
set.FeaturesTattoos = featureDict;
set.OptionName = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c =>
var nameArray = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
@ -210,14 +211,19 @@ namespace Glamourer.Customization
var textRow = _lobby.GetRow(menu.Value.Id);
return textRow?.Text.ToString() ?? c.ToDefaultName();
}).ToArray();
nameArray[(int) CustomizationId.EyeColorL] = nameArray[(int) CustomizationId.EyeColorR];
nameArray[(int) CustomizationId.EyeColorR] = GetName(CustomName.OddEyes);
set.OptionName = nameArray;
set._types = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c =>
set.Types = ((CustomizationId[]) Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
if (c == CustomizationId.HighlightColor)
return CharaMakeParams.MenuType.ColorPicker;
if (c == CustomizationId.EyeColorL)
return CharaMakeParams.MenuType.ColorPicker;
switch (c)
{
case CustomizationId.HighlightColor:
case CustomizationId.EyeColorL:
case CustomizationId.EyeColorR:
return CharaMakeParams.MenuType.ColorPicker;
}
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
@ -255,21 +261,21 @@ namespace Glamourer.Customization
_ => Language.English,
};
internal CustomizationOptions(DalamudPluginInterface pi)
internal CustomizationOptions(DalamudPluginInterface pi, DataManager gameData, ClientLanguage language)
{
_cmpFile = new CmpFile(pi);
_customizeSheet = pi.Data.GetExcelSheet<CharaMakeCustomize>();
_lobby = pi.Data.GetExcelSheet<Lobby>();
var tmp = pi.Data.Excel.GetType()!.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(typeof(CharaMakeParams))!.Invoke(pi.Data.Excel, new object?[]
_cmpFile = new CmpFile(gameData);
_customizeSheet = gameData.GetExcelSheet<CharaMakeCustomize>()!;
_lobby = gameData.GetExcelSheet<Lobby>()!;
var tmp = gameData.Excel.GetType()!.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(typeof(CharaMakeParams))!.Invoke(gameData.Excel, new object?[]
{
"charamaketype",
FromClientLanguage(pi.ClientState.ClientLanguage),
FromClientLanguage(language),
null,
}) as ExcelSheet<CharaMakeParams>;
_listSheet = tmp!;
_hairSheet = pi.Data.GetExcelSheet<HairMakeType>();
SetNames(pi);
_hairSheet = gameData.GetExcelSheet<HairMakeType>()!;
SetNames(gameData);
_highlightPicker = CreateColorPicker(CustomizationId.HighlightColor, 256, 192);
_lipColorPickerDark = CreateColorPicker(CustomizationId.LipColor, 512, 96);
@ -279,7 +285,7 @@ namespace Glamourer.Customization
_facePaintColorPickerLight = CreateColorPicker(CustomizationId.FacePaintColor, 1152, 96, true);
_tattooColorPicker = CreateColorPicker(CustomizationId.TattooColor, 0, 192);
_icons = new IconStorage(pi, _list.Length * 50);
_icons = new IconStorage(pi, gameData, _list.Length * 50);
foreach (var race in Clans)
{
foreach (var gender in Genders)
@ -287,15 +293,16 @@ namespace Glamourer.Customization
}
}
public void SetNames(DalamudPluginInterface pi)
private void SetNames(DataManager gameData)
{
var subRace = pi.Data.GetExcelSheet<Tribe>();
var subRace = gameData.GetExcelSheet<Tribe>()!;
_names[(int) CustomName.Clan] = _lobby.GetRow(102)?.Text ?? "Clan";
_names[(int) CustomName.Gender] = _lobby.GetRow(103)?.Text ?? "Gender";
_names[(int) CustomName.Reverse] = _lobby.GetRow(2135)?.Text ?? "Reverse";
_names[(int) CustomName.OddEyes] = _lobby.GetRow(2125)?.Text ?? "Odd Eyes";
_names[(int) CustomName.IrisSmall] = _lobby.GetRow(1076)?.Text ?? "Small";
_names[(int) CustomName.IrisLarge] = _lobby.GetRow(1075)?.Text ?? "Large";
_names[(int) CustomName.IrisSize] = _lobby.GetRow(244)?.Text ?? "Iris Size";
_names[(int) CustomName.MidlanderM] = subRace.GetRow((int) SubRace.Midlander)?.Masculine.ToString() ?? SubRace.Midlander.ToName();
_names[(int) CustomName.MidlanderF] = subRace.GetRow((int) SubRace.Midlander)?.Feminine.ToString() ?? SubRace.Midlander.ToName();
_names[(int) CustomName.HighlanderM] =

View file

@ -69,7 +69,7 @@ namespace Glamourer.Customization
public IReadOnlyList<Customization> LipColorsLight { get; internal set; } = null!;
public IReadOnlyList<Customization> LipColorsDark { get; internal set; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> _types { get; internal set; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public string Option(CustomizationId id)
=> OptionName[(int) id];
@ -154,7 +154,7 @@ namespace Glamourer.Customization
}
public CharaMakeParams.MenuType Type(CustomizationId id)
=> _types[(int) id];
=> Types[(int) id];
public int Count(CustomizationId id)

View file

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
namespace Glamourer
@ -10,23 +11,37 @@ namespace Glamourer
{
private static Dictionary<byte, Stain>? _stains;
private static Dictionary<EquipSlot, List<Item>>? _itemsBySlot;
private static SortedList<uint, ModelChara>? _models;
public static IReadOnlyDictionary<byte, Stain> Stains(DalamudPluginInterface pi)
public static IReadOnlyDictionary<uint, ModelChara> Models(DataManager dataManager)
{
if (_models != null)
return _models;
var sheet = dataManager.GetExcelSheet<ModelChara>()!;
_models = new SortedList<uint, ModelChara>((int) sheet.RowCount);
foreach (var model in sheet.Where(m => m.Type != 0))
_models.Add(model.RowId, model);
return _models;
}
public static IReadOnlyDictionary<byte, Stain> Stains(DataManager dataManager)
{
if (_stains != null)
return _stains;
var sheet = pi.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.Stain>();
var sheet = dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Stain>()!;
_stains = sheet.Where(s => s.Color != 0).ToDictionary(s => (byte) s.RowId, s => new Stain((byte) s.RowId, s));
return _stains;
}
public static IReadOnlyDictionary<EquipSlot, List<Item>> ItemsBySlot(DalamudPluginInterface pi)
public static IReadOnlyDictionary<EquipSlot, List<Item>> ItemsBySlot(DataManager dataManager)
{
if (_itemsBySlot != null)
return _itemsBySlot;
var sheet = pi.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.Item>();
var sheet = dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Item>()!;
Item EmptySlot(EquipSlot slot)
=> new(sheet.First(), "Nothing", slot);
@ -40,7 +55,7 @@ namespace Glamourer
[EquipSlot.Feet] = new(200) { EmptySlot(EquipSlot.Feet) },
[EquipSlot.RFinger] = new(200) { EmptySlot(EquipSlot.RFinger) },
[EquipSlot.Neck] = new(200) { EmptySlot(EquipSlot.Neck) },
[EquipSlot.MainHand] = new(200) { EmptySlot(EquipSlot.MainHand) },
[EquipSlot.MainHand] = new(1000) { EmptySlot(EquipSlot.MainHand) },
[EquipSlot.OffHand] = new(200) { EmptySlot(EquipSlot.OffHand) },
[EquipSlot.Wrists] = new(200) { EmptySlot(EquipSlot.Wrists) },
[EquipSlot.Ears] = new(200) { EmptySlot(EquipSlot.Ears) },

View file

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>preview</LangVersion>
<TargetFramework>net5.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer.GameData</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
@ -55,7 +56,7 @@
<Private>False</Private>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\$(TargetFramework)\Penumbra.GameData.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using Dalamud.Data.LuminaExtensions;
using Dalamud.Data;
using Dalamud.Plugin;
using Dalamud.Utility;
using ImGuiScene;
using Lumina.Data.Files;
@ -9,33 +10,35 @@ namespace Glamourer.Util
{
public class IconStorage : IDisposable
{
private readonly DalamudPluginInterface _pi;
private readonly Dictionary<int, TextureWrap> _icons;
private readonly DalamudPluginInterface _pi;
private readonly DataManager _gameData;
private readonly Dictionary<uint, TextureWrap> _icons;
public IconStorage(DalamudPluginInterface pi, int size = 0)
public IconStorage(DalamudPluginInterface pi, DataManager gameData, int size = 0)
{
_pi = pi;
_icons = new Dictionary<int, TextureWrap>(size);
_pi = pi;
_gameData = gameData;
_icons = new Dictionary<uint, TextureWrap>(size);
}
public TextureWrap this[int id]
=> LoadIcon(id);
private TexFile? LoadIconHq(int id)
private TexFile? LoadIconHq(uint id)
{
var path = $"ui/icon/{id / 1000 * 1000:000000}/{id:000000}_hr1.tex";
return _pi.Data.GetFile<TexFile>(path);
return _gameData.GetFile<TexFile>(path);
}
public TextureWrap LoadIcon(uint id)
=> LoadIcon((int) id);
public TextureWrap LoadIcon(int id)
=> LoadIcon((uint) id);
public TextureWrap LoadIcon(uint id)
{
if (_icons.TryGetValue(id, out var ret))
return ret;
var icon = LoadIconHq(id) ?? _pi.Data.GetIcon(id);
var icon = LoadIconHq(id) ?? _gameData.GetIcon(id)!;
var iconData = icon.GetRgbaImageData();
ret = _pi.UiBuilder.LoadImageRaw(iconData, icon.Header.Width, icon.Header.Height, 4);

Binary file not shown.

View file

@ -1,8 +1,8 @@
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Objects.Types;
namespace Glamourer
{
public static class ActorExtensions
public static class CharacterExtensions
{
public const int WetnessOffset = 0x19A5;
public const byte WetnessFlag = 0x10;
@ -13,10 +13,10 @@ namespace Glamourer
public const int WeaponHiddenOffset = 0xF64;
public const byte WeaponHiddenFlag = 0x02;
public static unsafe bool IsWet(this Actor a)
public static unsafe bool IsWet(this Character a)
=> (*((byte*) a.Address + WetnessOffset) & WetnessFlag) != 0;
public static unsafe bool SetWetness(this Actor a, bool value)
public static unsafe bool SetWetness(this Character a, bool value)
{
var current = a.IsWet();
if (current == value)
@ -29,10 +29,10 @@ namespace Glamourer
return true;
}
public static unsafe ref byte StateFlags(this Actor a)
public static unsafe ref byte StateFlags(this Character a)
=> ref *((byte*) a.Address + StateFlagsOffset);
public static bool SetStateFlag(this Actor a, bool value, byte flag)
public static bool SetStateFlag(this Character a, bool value, byte flag)
{
var current = a.StateFlags();
var previousValue = (current & flag) != 0;
@ -46,20 +46,20 @@ namespace Glamourer
return true;
}
public static bool IsHatHidden(this Actor a)
public static bool IsHatHidden(this Character a)
=> (a.StateFlags() & HatHiddenFlag) != 0;
public static unsafe bool IsWeaponHidden(this Actor a)
public static unsafe bool IsWeaponHidden(this Character a)
=> (a.StateFlags() & WeaponHiddenFlag) != 0
&& (*((byte*) a.Address + WeaponHiddenOffset) & WeaponHiddenFlag) != 0;
public static bool IsVisorToggled(this Actor a)
public static bool IsVisorToggled(this Character a)
=> (a.StateFlags() & VisorToggledFlag) != 0;
public static bool SetHatHidden(this Actor a, bool value)
public static bool SetHatHidden(this Character a, bool value)
=> SetStateFlag(a, value, HatHiddenFlag);
public static unsafe bool SetWeaponHidden(this Actor a, bool value)
public static unsafe bool SetWeaponHidden(this Character a, bool value)
{
var ret = SetStateFlag(a, value, WeaponHiddenFlag);
var val = *((byte*) a.Address + WeaponHiddenOffset);
@ -70,10 +70,10 @@ namespace Glamourer
return ret || (val & WeaponHiddenFlag) != 0 != value;
}
public static bool SetVisorToggled(this Actor a, bool value)
public static bool SetVisorToggled(this Character a, bool value)
=> SetStateFlag(a, value, VisorToggledFlag);
public static unsafe ref float Alpha(this Actor a)
public static unsafe ref float Alpha(this Character a)
=> ref *(float*) ((byte*) a.Address + AlphaOffset);
}
}

View file

@ -1,8 +1,11 @@
using System;
using Dalamud.Game.ClientState.Actors.Types;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.Customization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer
@ -36,8 +39,8 @@ namespace Glamourer
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 TotalSizeVersion1 = 1 + 1 + 2 + 56 + CharacterCustomization.CustomizationBytes;
public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + CharacterCustomization.CustomizationBytes + 4 + 1;
public const byte TotalSize = TotalSizeVersion2;
@ -97,8 +100,8 @@ namespace Glamourer
public byte StateFlags
{
get => _bytes[64 + ActorCustomization.CustomizationBytes];
set => _bytes[64 + ActorCustomization.CustomizationBytes] = value;
get => _bytes[64 + CharacterCustomization.CustomizationBytes];
set => _bytes[64 + CharacterCustomization.CustomizationBytes] = value;
}
public bool HatState
@ -119,9 +122,9 @@ namespace Glamourer
set => StateFlags = (byte) (value ? StateFlags & ~0x02 : StateFlags | 0x02);
}
public ActorEquipMask WriteEquipment
public CharacterEquipMask WriteEquipment
{
get => (ActorEquipMask) ((ushort) _bytes[2] | ((ushort) _bytes[3] << 8));
get => (CharacterEquipMask) (_bytes[2] | (_bytes[3] << 8));
set
{
_bytes[2] = (byte) ((ushort) value & 0xFF);
@ -129,11 +132,104 @@ namespace Glamourer
}
}
private static Dictionary<EquipSlot, (int, int, bool)> Offsets()
{
var stainOffsetWeapon = (int) Marshal.OffsetOf<CharacterWeapon>("Stain");
var stainOffsetEquip = (int) Marshal.OffsetOf<CharacterArmor>("Stain");
(int, int, bool) ToOffsets(IntPtr offset, bool weapon)
{
var off = 4 + CharacterCustomization.CustomizationBytes + (int) offset;
return weapon ? (off, off + stainOffsetWeapon, weapon) : (off, off + stainOffsetEquip, weapon);
}
return new Dictionary<EquipSlot, (int, int, bool)>(12)
{
[EquipSlot.MainHand] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("MainHand"), true),
[EquipSlot.OffHand] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("OffHand"), true),
[EquipSlot.Head] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("Head"), false),
[EquipSlot.Body] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("Body"), false),
[EquipSlot.Hands] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("Hands"), false),
[EquipSlot.Legs] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("Legs"), false),
[EquipSlot.Feet] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("Feet"), false),
[EquipSlot.Ears] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("Ears"), false),
[EquipSlot.Neck] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("Neck"), false),
[EquipSlot.Wrists] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("Wrists"), false),
[EquipSlot.RFinger] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("RFinger"), false),
[EquipSlot.LFinger] = ToOffsets(Marshal.OffsetOf<CharacterEquipment>("LFinger"), false),
};
}
private static readonly IReadOnlyDictionary<EquipSlot, (int, int, bool)> FieldOffsets = Offsets();
public bool WriteStain(EquipSlot slot, StainId stainId)
{
if (WriteProtected)
return false;
var (_, stainOffset, _) = FieldOffsets[slot];
if (_bytes[stainOffset] == (byte) stainId)
return false;
_bytes[stainOffset] = stainId.Value;
return true;
}
private bool WriteItem(int offset, SetId id, WeaponType type, ushort variant, bool weapon)
{
var idBytes = BitConverter.GetBytes(id.Value);
static bool WriteIfDifferent(ref byte x, byte y)
{
if (x == y)
return false;
x = y;
return true;
}
var ret = WriteIfDifferent(ref _bytes[offset], idBytes[0]);
ret |= WriteIfDifferent(ref _bytes[offset + 1], idBytes[1]);
if (weapon)
{
var typeBytes = BitConverter.GetBytes(type.Value);
var variantBytes = BitConverter.GetBytes(variant);
ret |= WriteIfDifferent(ref _bytes[offset + 2], typeBytes[0]);
ret |= WriteIfDifferent(ref _bytes[offset + 3], typeBytes[1]);
ret |= WriteIfDifferent(ref _bytes[offset + 4], variantBytes[0]);
ret |= WriteIfDifferent(ref _bytes[offset + 5], variantBytes[1]);
}
else
{
ret |= WriteIfDifferent(ref _bytes[offset + 2], (byte) variant);
}
return ret;
}
public bool WriteItem(Item item)
{
if (WriteProtected)
return false;
var (itemOffset, _, isWeapon) = FieldOffsets[item.EquippableTo];
var (id, type, variant) = item.MainModel;
var ret = WriteItem(itemOffset, id, type, variant, isWeapon);
if (item.EquippableTo == EquipSlot.MainHand && item.HasSubModel)
{
var (subOffset, _, _) = FieldOffsets[EquipSlot.OffHand];
var (subId, subType, subVariant) = item.SubModel;
ret |= WriteItem(subOffset, subId, subType, subVariant, true);
}
return ret;
}
public unsafe float Alpha
{
get
{
fixed (byte* ptr = &_bytes[60 + ActorCustomization.CustomizationBytes])
fixed (byte* ptr = &_bytes[60 + CharacterCustomization.CustomizationBytes])
{
return *(float*) ptr;
}
@ -142,24 +238,24 @@ namespace Glamourer
{
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);
*(ptr + 60 + CharacterCustomization.CustomizationBytes + 0) = *((byte*) &value + 0);
*(ptr + 60 + CharacterCustomization.CustomizationBytes + 1) = *((byte*) &value + 1);
*(ptr + 60 + CharacterCustomization.CustomizationBytes + 2) = *((byte*) &value + 2);
*(ptr + 60 + CharacterCustomization.CustomizationBytes + 3) = *((byte*) &value + 3);
}
}
}
public void Load(ActorCustomization customization)
public void Load(CharacterCustomization customization)
{
WriteCustomizations = true;
customization.WriteBytes(_bytes, 4);
}
public void Load(ActorEquipment equipment, ActorEquipMask mask = ActorEquipMask.All)
public void Load(CharacterEquipment equipment, CharacterEquipMask mask = CharacterEquipMask.All)
{
WriteEquipment = mask;
equipment.WriteBytes(_bytes, 4 + ActorCustomization.CustomizationBytes);
equipment.WriteBytes(_bytes, 4 + CharacterCustomization.CustomizationBytes);
}
public string ToBase64()
@ -179,19 +275,19 @@ namespace Glamourer
$"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)
private static void CheckCharacterMask(byte val1, byte val2)
{
var mask = (ActorEquipMask) ((ushort) val1 | ((ushort) val2 << 8));
if (mask > ActorEquipMask.All)
var mask = (CharacterEquipMask) (val1 | (val2 << 8));
if (mask > CharacterEquipMask.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)
public void LoadCharacter(Character a)
{
WriteCustomizations = true;
Load(new ActorCustomization(a));
Load(new CharacterCustomization(a));
Load(new ActorEquipment(a), ActorEquipMask.All);
Load(new CharacterEquipment(a));
SetHatState = true;
SetVisorState = true;
@ -202,11 +298,13 @@ namespace Glamourer
Alpha = a.Alpha();
}
public void Apply(Actor a)
public void Apply(Character a)
{
Glamourer.RevertableDesigns.Add(a);
if (WriteCustomizations)
Customizations.Write(a.Address);
if (WriteEquipment != ActorEquipMask.None)
if (WriteEquipment != CharacterEquipMask.None)
Equipment.Write(a.Address, WriteEquipment, WriteEquipment);
a.SetWetness(IsWet);
a.Alpha() = Alpha;
@ -243,7 +341,7 @@ namespace Glamourer
default: throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid Version {bytes[0]}.");
}
CheckActorMask(bytes[2], bytes[3]);
CheckCharacterMask(bytes[2], bytes[3]);
bytes.CopyTo(_bytes, 0);
}
@ -254,23 +352,23 @@ namespace Glamourer
return ret;
}
public unsafe ref ActorCustomization Customizations
public unsafe ref CharacterCustomization Customizations
{
get
{
fixed (byte* ptr = _bytes)
{
return ref *(ActorCustomization*) (ptr + 4);
return ref *(CharacterCustomization*) (ptr + 4);
}
}
}
public ActorEquipment Equipment
public CharacterEquipment Equipment
{
get
{
var ret = new ActorEquipment();
ret.FromBytes(_bytes, 4 + ActorCustomization.CustomizationBytes);
var ret = new CharacterEquipment();
ret.FromBytes(_bytes, 4 + CharacterCustomization.CustomizationBytes);
return ret;
}
}

View file

@ -1,23 +0,0 @@
using Dalamud.Plugin;
namespace Glamourer
{
public class CmpFile
{
public readonly Lumina.Data.FileResource File;
public readonly uint[] RgbaColors;
public CmpFile(DalamudPluginInterface pi)
{
File = pi.Data.GetFile("chara/xls/charamake/human.cmp");
RgbaColors = new uint[File.Data.Length >> 2];
for (var i = 0; i < File.Data.Length; i += 4)
{
RgbaColors[i >> 2] = File.Data[i]
| (uint) (File.Data[i + 1] << 8)
| (uint) (File.Data[i + 2] << 16)
| (uint) (File.Data[i + 3] << 24);
}
}
}
}

56
Glamourer/Dalamud.cs Normal file
View file

@ -0,0 +1,56 @@
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Buddy;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Fates;
using Dalamud.Game.ClientState.JobGauge;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Party;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.FlyText;
using Dalamud.Game.Gui.PartyFinder;
using Dalamud.Game.Gui.Toast;
using Dalamud.Game.Libc;
using Dalamud.Game.Network;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC;
using Dalamud.Plugin;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
namespace Glamourer
{
public class Dalamud
{
public static void Initialize(DalamudPluginInterface pluginInterface)
=> pluginInterface.Create<Dalamud>();
// @formatter:off
[PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static SeStringManager SeStrings { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static ChatHandlers ChatHandlers { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static GameNetwork Network { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static KeyState Keys { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static FlyTextGui FlyTexts { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static ToastGui Toasts { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static JobGauges Gauges { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static PartyFinderGui PartyFinder { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static BuddyList Buddies { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static PartyList Party { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static FateTable Fates { get; private set; } = null!;
//[PluginService][RequiredVersion("1.0")] public static LibcFunction LibC { get; private set; } = null!;
// @formatter:on
}
}

View file

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Dalamud.Logging;
using Glamourer.FileSystem;
using Newtonsoft.Json;
@ -16,9 +16,9 @@ namespace Glamourer.Designs
public SortedList<string, CharacterSave> Designs = null!;
public FileSystem.FileSystem FileSystem { get; } = new();
public DesignManager(DalamudPluginInterface pi)
public DesignManager()
{
var saveFolder = new DirectoryInfo(pi.GetPluginConfigDirectory());
var saveFolder = new DirectoryInfo(Dalamud.PluginInterface.GetPluginConfigDirectory());
if (!saveFolder.Exists)
Directory.CreateDirectory(saveFolder.FullName);
@ -31,24 +31,21 @@ namespace Glamourer.Designs
{
FileSystem.Clear();
var anyChanges = false;
foreach (var kvp in Designs.ToArray())
foreach (var (path, save) 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}.");
}
if (string.Equals(fixedPath, path, StringComparison.InvariantCultureIgnoreCase))
continue;
Designs.Remove(path);
Designs[fixedPath] = save;
anyChanges = true;
PluginLog.Debug($"Problem loading saved designs, {path} was renamed to {fixedPath}.");
}
catch (Exception e)
{

View file

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
using Glamourer.FileSystem;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs
{
public class FixedDesigns : IDisposable
{
public class FixedDesign
{
public string Name;
public Design Design;
public bool Enabled;
public GlamourerConfig.FixedDesign ToSave()
=> new()
{
Name = Name,
Path = Design.FullName(),
Enabled = Enabled,
};
public FixedDesign(string name, Design design, bool enabled)
{
Name = name;
Design = design;
Enabled = enabled;
}
}
public List<FixedDesign> Data;
public Dictionary<string, FixedDesign> EnabledDesigns;
public bool EnableDesign(FixedDesign design)
{
var changes = !design.Enabled;
if (EnabledDesigns.TryGetValue(design.Name, out var oldDesign))
{
oldDesign.Enabled = false;
changes = true;
}
else
{
Glamourer.PlayerWatcher.AddPlayerToWatch(design.Name);
}
EnabledDesigns[design.Name] = design;
design.Enabled = true;
if (Dalamud.Objects.FirstOrDefault(o => o.ObjectKind == ObjectKind.Player && o.Name.ToString() == design.Name)
is Character character)
OnPlayerChange(character);
return changes;
}
public bool DisableDesign(FixedDesign design)
{
if (!design.Enabled)
return false;
design.Enabled = false;
EnabledDesigns.Remove(design.Name);
Glamourer.PlayerWatcher.RemovePlayerFromWatch(design.Name);
return true;
}
public FixedDesigns(DesignManager designs)
{
Data = new List<FixedDesign>(Glamourer.Config.FixedDesigns.Count);
EnabledDesigns = new Dictionary<string, FixedDesign>(Glamourer.Config.FixedDesigns.Count);
Glamourer.PlayerWatcher.PlayerChanged += OnPlayerChange;
var changes = false;
for (var i = 0; i < Glamourer.Config.FixedDesigns.Count; ++i)
{
var save = Glamourer.Config.FixedDesigns[i];
if (designs.FileSystem.Find(save.Path, out var d) && d is Design design)
{
Data.Add(new FixedDesign(save.Name, design, save.Enabled));
if (save.Enabled)
changes |= EnableDesign(Data.Last());
}
else
{
PluginLog.Warning($"{save.Path} does not exist anymore, removing {save.Name} from fixed designs.");
Glamourer.Config.FixedDesigns.RemoveAt(i--);
changes = true;
}
}
if (changes)
Glamourer.Config.Save();
}
private void OnPlayerChange(Character character)
{
var name = character.Name.ToString();
if (EnabledDesigns.TryGetValue(name, out var design))
{
PluginLog.Debug("Redrawing {CharacterName} with {DesignName}.", name, design.Design.FullName());
design.Design.Data.Apply(character);
Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(character);
Glamourer.Penumbra.RedrawObject(character, RedrawType.WithSettings, false);
}
}
public void Add(string name, Design design, bool enabled = false)
{
Data.Add(new FixedDesign(name, design, enabled));
Glamourer.Config.FixedDesigns.Add(Data.Last().ToSave());
if (enabled)
EnableDesign(Data.Last());
Glamourer.Config.Save();
}
public void Remove(FixedDesign design)
{
var idx = Data.IndexOf(design);
if (idx < 0)
return;
Data.RemoveAt(idx);
Glamourer.Config.FixedDesigns.RemoveAt(idx);
if (design.Enabled)
{
EnabledDesigns.Remove(design.Name);
Glamourer.PlayerWatcher.RemovePlayerFromWatch(design.Name);
}
Glamourer.Config.Save();
}
public void Move(FixedDesign design, int newIdx)
{
if (newIdx < 0)
newIdx = 0;
if (newIdx >= Data.Count)
newIdx = Data.Count - 1;
var idx = Data.IndexOf(design);
if (idx < 0 || idx == newIdx)
return;
Data.RemoveAt(idx);
Data.Insert(newIdx, design);
Glamourer.Config.FixedDesigns.RemoveAt(idx);
Glamourer.Config.FixedDesigns.Insert(newIdx, design.ToSave());
Glamourer.Config.Save();
}
public void Dispose()
{
Glamourer.Config.FixedDesigns = Data.Select(d => d.ToSave()).ToList();
Glamourer.Config.Save();
}
}
}

View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types;
namespace Glamourer.Designs
{
public class RevertableDesigns
{
public readonly Dictionary<string, CharacterSave> Saves = new();
public bool Add(Character actor)
{
var name = actor.Name.ToString();
if (Saves.TryGetValue(name, out var save))
return false;
save = new CharacterSave();
save.LoadCharacter(actor);
Saves[name] = save;
return true;
}
public bool Revert(Character actor)
{
if (!Saves.TryGetValue(actor.Name.ToString(), out var save))
return false;
save.Apply(actor);
return true;
}
}
}

View file

@ -1,6 +1,5 @@
using System;
using System.Linq;
using Dalamud.Plugin;
using Dalamud.Logging;
using ImGuiNET;
namespace Glamourer.FileSystem
@ -12,7 +11,7 @@ namespace Glamourer.FileSystem
private static unsafe bool IsDropping(string name)
=> ImGui.AcceptDragDropPayload(name).NativePtr != null;
private static IFileSystemBase? _draggedObject = null;
private static IFileSystemBase? _draggedObject;
public static bool DragDropTarget(FileSystem fs, IFileSystemBase child, out string oldPath, out IFileSystemBase? draggedChild)
{

View file

@ -1,17 +1,16 @@
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 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)
public int Compare(IFileSystemBase? x, IFileSystemBase? y)
=> Cmp(x, y);
internal static readonly FolderStructureComparer Default = new();

207
Glamourer/Glamourer.cs Normal file
View file

@ -0,0 +1,207 @@
using System;
using System.Linq;
using System.Reflection;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Command;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.FileSystem;
using Glamourer.Gui;
using ImGuiNET;
using Penumbra.PlayerWatch;
namespace Glamourer
{
public class Glamourer : IDalamudPlugin
{
private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],<Name for Save>";
public string Name
=> "Glamourer";
public static GlamourerConfig Config = null!;
public static IPlayerWatcher PlayerWatcher = null!;
public static ICustomizationManager Customization = null!;
private readonly Interface _interface;
public readonly DesignManager Designs;
public readonly FixedDesigns FixedDesigns;
public static RevertableDesigns RevertableDesigns = new();
public static string Version = string.Empty;
public static PenumbraAttach Penumbra = null!;
public Glamourer(DalamudPluginInterface pluginInterface)
{
Dalamud.Initialize(pluginInterface);
Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "";
Config = GlamourerConfig.Load();
Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage);
Designs = new DesignManager();
Penumbra = new PenumbraAttach(Config.AttachToPenumbra);
PlayerWatcher = PlayerWatchFactory.Create(Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects);
FixedDesigns = new FixedDesigns(Designs);
if (Config.ApplyFixedDesigns)
PlayerWatcher.Enable();
Dalamud.Commands.AddHandler("/glamourer", new CommandInfo(OnGlamourer)
{
HelpMessage = "Open or close the Glamourer window.",
});
Dalamud.Commands.AddHandler("/glamour", new CommandInfo(OnGlamour)
{
HelpMessage = $"Use Glamourer Functions: {HelpString}",
});
_interface = new Interface(this);
}
public void OnGlamourer(string command, string arguments)
=> _interface.ToggleVisibility();
private static GameObject? GetPlayer(string name)
{
var lowerName = name.ToLowerInvariant();
return lowerName switch
{
"" => null,
"<me>" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer,
"self" => Dalamud.Objects[Interface.GPoseObjectId] ?? Dalamud.ClientState.LocalPlayer,
"<t>" => Dalamud.Targets.Target,
"target" => Dalamud.Targets.Target,
"<f>" => Dalamud.Targets.FocusTarget,
"focus" => Dalamud.Targets.FocusTarget,
"<mo>" => Dalamud.Targets.MouseOverTarget,
"mouseover" => Dalamud.Targets.MouseOverTarget,
_ => Dalamud.Objects.LastOrDefault(
a => string.Equals(a.Name.ToString(), lowerName, StringComparison.InvariantCultureIgnoreCase)),
};
}
public void CopyToClipboard(Character player)
{
var save = new CharacterSave();
save.LoadCharacter(player);
ImGui.SetClipboardText(save.ToBase64());
}
public void ApplyCommand(Character player, string target)
{
CharacterSave? save = null;
if (target.ToLowerInvariant() == "clipboard")
try
{
save = CharacterSave.FromString(ImGui.GetClipboardText());
}
catch (Exception)
{
Dalamud.Chat.PrintError("Clipboard does not contain a valid customization string.");
}
else if (!Designs.FileSystem.Find(target, out var child) || child is not Design d)
Dalamud.Chat.PrintError("The given path to a saved design does not exist or does not point to a design.");
else
save = d.Data;
save?.Apply(player);
Penumbra.UpdateCharacters(player);
}
public void SaveCommand(Character player, string path)
{
var save = new CharacterSave();
save.LoadCharacter(player);
try
{
var (folder, name) = Designs.FileSystem.CreateAllFolders(path);
var design = new Design(folder, name) { Data = save };
folder.FindOrAddChild(design);
Designs.Designs.Add(design.FullName(), design.Data);
Designs.SaveToFile();
}
catch (Exception e)
{
Dalamud.Chat.PrintError("Could not save file:");
Dalamud.Chat.PrintError($" {e.Message}");
}
}
public void OnGlamour(string command, string arguments)
{
static void PrintHelp()
{
Dalamud.Chat.Print("Usage:");
Dalamud.Chat.Print($" {HelpString}");
}
arguments = arguments.Trim();
if (!arguments.Any())
{
PrintHelp();
return;
}
var split = arguments.Split(new[]
{
',',
}, 3, StringSplitOptions.RemoveEmptyEntries);
if (split.Length < 2)
{
PrintHelp();
return;
}
var player = GetPlayer(split[1]) as Character;
if (player == null)
{
Dalamud.Chat.Print($"Could not find object for {split[1]} or it was not a Character.");
return;
}
switch (split[0].ToLowerInvariant())
{
case "copy":
CopyToClipboard(player);
return;
case "apply":
{
if (split.Length < 3)
{
Dalamud.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'.");
return;
}
ApplyCommand(player, split[2]);
return;
}
case "save":
{
if (split.Length < 3)
{
Dalamud.Chat.Print("Saving requires a name for the save.");
return;
}
SaveCommand(player, split[2]);
return;
}
default:
PrintHelp();
return;
}
}
public void Dispose()
{
FixedDesigns.Dispose();
Penumbra.Dispose();
PlayerWatcher.Dispose();
_interface.Dispose();
Dalamud.Commands.RemoveHandler("/glamour");
Dalamud.Commands.RemoveHandler("/glamourer");
}
}
}

View file

@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer</AssemblyName>
<FileVersion>0.0.3.0</FileVersion>
<AssemblyVersion>0.0.3.0</AssemblyVersion>
<FileVersion>0.0.5.4</FileVersion>
<AssemblyVersion>0.0.5.4</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
@ -46,47 +47,40 @@
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="SDL2-CS">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\SDL2-CS.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
</Reference>
<Reference Include="Penumbra.Api">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.Api.dll</HintPath>
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\$(TargetFramework)\Penumbra.GameData.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Penumbra.PlayerWatch">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.PlayerWatch.dll</HintPath>
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\$(TargetFramework)\Penumbra.PlayerWatch.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Memory" Version="4.5.3" />
</ItemGroup>
@ -100,7 +94,7 @@
</None>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll, $(TargetDir)Penumbra.GameData.dll, $(TargetDir)Penumbra.Api.dll, $(TargetDir)Penumbra.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.PlayerWatch.dll $(SolutionDir)$(SolutionName).zip" />
<Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
</Target>
</Project>

View file

@ -1,11 +1,15 @@
{
"Author": "Ottermandias",
"Name": "Glamourer",
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
"InternalName": "Glamourer",
"AssemblyVersion": "0.0.3.0",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 3,
"LoadPriority": -100
{
"Author": "Ottermandias",
"Name": "Glamourer",
"Punchline": "Change and save appearance of players.",
"Description": "Adds functionality to change and store appearance of players, customization and equip. Requires Penumbra to be installed and activated to work. Can also add preview options to the Changed Items tab for Penumbra.",
"Tags": [ "Appearance", "Glamour", "Race", "Outfit", "Armor", "Clothes", "Skins", "Customization", "Design", "Character" ],
"InternalName": "Glamourer",
"AssemblyVersion": "0.0.5.4",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 4,
"LoadPriority": -100,
"ImageUrls": null,
"IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/master/images/icon.png"
}

View file

@ -1,36 +1,45 @@
using Dalamud.Configuration;
using System.Collections.Generic;
using Dalamud.Configuration;
namespace Glamourer
{
public class GlamourerConfig : IPluginConfiguration
{
public struct FixedDesign
{
public string Name;
public string Path;
public bool Enabled;
}
public int Version { get; set; } = 1;
public const uint DefaultCustomizationColor = 0xFFC000C0;
public const uint DefaultStateColor = 0xFF00C0C0;
public const uint DefaultEquipmentColor = 0xFF00C000;
public bool FoldersFirst { get; set; } = false;
public bool ColorDesigns { get; set; } = true;
public bool ShowLocks { get; set; } = true;
public bool AttachToPenumbra { get; set; } = true;
public bool FoldersFirst { get; set; } = false;
public bool ColorDesigns { get; set; } = true;
public bool ShowLocks { get; set; } = true;
public bool AttachToPenumbra { get; set; } = true;
public bool ApplyFixedDesigns { get; set; } = true;
public uint CustomizationColor { get; set; } = DefaultCustomizationColor;
public uint StateColor { get; set; } = DefaultStateColor;
public uint EquipmentColor { get; set; } = DefaultEquipmentColor;
public List<FixedDesign> FixedDesigns { get; set; } = new();
public void Save()
=> Glamourer.PluginInterface.SavePluginConfig(this);
=> Dalamud.PluginInterface.SavePluginConfig(this);
public static GlamourerConfig Create()
public static GlamourerConfig Load()
{
var config = Glamourer.PluginInterface.GetPluginConfig() as GlamourerConfig;
if (config == null)
{
config = new GlamourerConfig();
Glamourer.PluginInterface.SavePluginConfig(config);
}
if (Dalamud.PluginInterface.GetPluginConfig() is GlamourerConfig config)
return config;
config = new GlamourerConfig();
config.Save();
return config;
}
}

View file

@ -8,30 +8,48 @@ namespace Glamourer.Gui
{
public class ComboWithFilter<T>
{
private readonly string _label;
private readonly string _filterLabel;
private readonly string _listLabel;
private string _currentFilter = string.Empty;
private string _currentFilterLower = string.Empty;
private bool _focus;
private readonly float _size;
private float _previewSize;
private readonly IReadOnlyList<T> _items;
private readonly IReadOnlyList<string> _itemNamesLower;
private readonly Func<T, string> _itemToName;
private readonly string _label;
private readonly string _filterLabel;
private readonly string _listLabel;
private string _currentFilter = string.Empty;
private string _currentFilterLower = string.Empty;
private bool _focus;
private readonly float _size;
private float _previewSize;
private readonly IReadOnlyList<T> _items;
private readonly IReadOnlyList<(string, int)> _itemNamesLower;
private readonly Func<T, string> _itemToName;
private IReadOnlyList<(string, int)> _currentItemNames;
private bool _needsClear;
public Action? PrePreview = null;
public Action? PostPreview = null;
public Func<T, bool>? CreateSelectable = null;
public Action? PreList = null;
public Action? PostList = null;
public float? HeightPerItem = null;
public Action? PrePreview;
public Action? PostPreview;
public Func<T, bool>? CreateSelectable;
public Action? PreList;
public Action? PostList;
public float? HeightPerItem;
private float _heightPerItem;
public ImGuiComboFlags Flags { get; set; } = ImGuiComboFlags.None;
public int ItemsAtOnce { get; set; } = 12;
private void UpdateFilter(string newFilter)
{
if (newFilter == _currentFilter)
return;
var lower = newFilter.ToLowerInvariant();
if (_currentFilterLower.Any() && lower.Contains(_currentFilterLower))
_currentItemNames = _currentItemNames.Where(p => p.Item1.Contains(lower)).ToArray();
else if (lower.Any())
_currentItemNames = _itemNamesLower.Where(p => p.Item1.Contains(lower)).ToArray();
else
_currentItemNames = _itemNamesLower;
_currentFilter = newFilter;
_currentFilterLower = lower;
}
public ComboWithFilter(string label, float size, float previewSize, IReadOnlyList<T> items, Func<T, string> itemToName)
{
_label = label;
@ -42,26 +60,28 @@ namespace Glamourer.Gui
_size = size;
_previewSize = previewSize;
_itemNamesLower = _items.Select(i => _itemToName(i).ToLowerInvariant()).ToList();
_itemNamesLower = _items.Select((i, idx) => (_itemToName(i).ToLowerInvariant(), idx)).ToArray();
_currentItemNames = _itemNamesLower;
}
public ComboWithFilter(string label, ComboWithFilter<T> other)
{
_label = label;
_filterLabel = $"##_{label}_filter";
_listLabel = $"##_{label}_list";
_itemToName = other._itemToName;
_items = other._items;
_itemNamesLower = other._itemNamesLower;
_size = other._size;
_previewSize = other._previewSize;
PrePreview = other.PrePreview;
PostPreview = other.PostPreview;
CreateSelectable = other.CreateSelectable;
PreList = other.PreList;
PostList = other.PostList;
HeightPerItem = other.HeightPerItem;
Flags = other.Flags;
_label = label;
_filterLabel = $"##_{label}_filter";
_listLabel = $"##_{label}_list";
_itemToName = other._itemToName;
_items = other._items;
_itemNamesLower = other._itemNamesLower;
_currentItemNames = other._currentItemNames;
_size = other._size;
_previewSize = other._previewSize;
PrePreview = other.PrePreview;
PostPreview = other.PostPreview;
CreateSelectable = other.CreateSelectable;
PreList = other.PreList;
PostList = other.PostList;
HeightPerItem = other.HeightPerItem;
Flags = other.Flags;
}
private bool DrawList(string currentName, out int numItems, out int nodeIdx, ref T? value)
@ -69,7 +89,10 @@ namespace Glamourer.Gui
numItems = ItemsAtOnce;
nodeIdx = -1;
if (!ImGui.BeginChild(_listLabel, new Vector2(_size, ItemsAtOnce * _heightPerItem)))
{
ImGui.EndChild();
return false;
}
var ret = false;
try
@ -80,7 +103,6 @@ namespace Glamourer.Gui
_focus = true;
}
var scrollY = Math.Max((int) (ImGui.GetScrollY() / _heightPerItem) - 1, 0);
var restHeight = scrollY * _heightPerItem;
numItems = 0;
@ -89,38 +111,34 @@ namespace Glamourer.Gui
if (restHeight > 0)
ImGui.Dummy(Vector2.UnitY * restHeight);
for (var i = scrollY; i < _items.Count; ++i)
for (var i = scrollY; i < _currentItemNames.Count; ++i)
{
if (!_itemNamesLower[i].Contains(_currentFilterLower))
if (++numItems > ItemsAtOnce + 2)
continue;
++numItems;
if (numItems <= ItemsAtOnce + 2)
nodeIdx = _currentItemNames[i].Item2;
var item = _items[nodeIdx]!;
bool success;
if (CreateSelectable != null)
{
nodeIdx = i;
var item = _items[i]!;
var success = false;
if (CreateSelectable != null)
{
success = CreateSelectable(item);
}
else
{
var name = _itemToName(item);
success = ImGui.Selectable(name, name == currentName);
}
success = CreateSelectable(item);
}
else
{
var name = _itemToName(item);
success = ImGui.Selectable(name, name == currentName);
}
if (success)
{
value = item;
ImGui.CloseCurrentPopup();
ret = true;
}
if (success)
{
value = item;
ImGui.CloseCurrentPopup();
ret = true;
}
}
if (numItems > ItemsAtOnce + 2)
ImGui.Dummy(Vector2.UnitY * (numItems - ItemsAtOnce - 2) * _heightPerItem);
if (_currentItemNames.Count > ItemsAtOnce + 2)
ImGui.Dummy(Vector2.UnitY * (_currentItemNames.Count - ItemsAtOnce - 2 - scrollY) * _heightPerItem);
}
finally
{
@ -140,23 +158,29 @@ namespace Glamourer.Gui
PrePreview?.Invoke();
if (!ImGui.BeginCombo(_label, currentName, Flags))
{
_focus = false;
_currentFilter = string.Empty;
_currentFilterLower = string.Empty;
if (_needsClear)
{
_needsClear = false;
_focus = false;
UpdateFilter(string.Empty);
}
PostPreview?.Invoke();
return false;
}
_needsClear = true;
PostPreview?.Invoke();
_heightPerItem = HeightPerItem ?? ImGui.GetTextLineHeightWithSpacing();
var ret = false;
bool ret;
try
{
ImGui.SetNextItemWidth(-1);
if (ImGui.InputTextWithHint(_filterLabel, "Filter...", ref _currentFilter, 255))
_currentFilterLower = _currentFilter.ToLowerInvariant();
var tmp = _currentFilter;
if (ImGui.InputTextWithHint(_filterLabel, "Filter...", ref tmp, 255))
UpdateFilter(tmp);
var isFocused = ImGui.IsItemActive();
if (!_focus)

View file

@ -7,15 +7,12 @@ namespace Glamourer.Gui
{
public sealed class ImGuiRaii : IDisposable
{
private int _colorStack = 0;
private int _fontStack = 0;
private int _styleStack = 0;
private float _indentation = 0f;
private int _colorStack;
private int _fontStack;
private int _styleStack;
private float _indentation;
private Stack<Action>? _onDispose = null;
public ImGuiRaii()
{ }
private Stack<Action>? _onDispose;
public static ImGuiRaii NewGroup()
=> new ImGuiRaii().Group();
@ -51,6 +48,7 @@ namespace Glamourer.Gui
ImGui.PopStyleColor(actualN);
_colorStack -= actualN;
}
return this;
}
@ -76,6 +74,7 @@ namespace Glamourer.Gui
ImGui.PopStyleVar(actualN);
_styleStack -= actualN;
}
return this;
}
@ -95,6 +94,7 @@ namespace Glamourer.Gui
ImGui.PopFont();
--_fontStack;
}
return this;
}
@ -105,6 +105,7 @@ namespace Glamourer.Gui
ImGui.Indent(width);
_indentation += width;
}
return this;
}
@ -134,7 +135,7 @@ namespace Glamourer.Gui
public void End(int n = 1)
{
var actualN = Math.Min(n, _onDispose?.Count ?? 0);
while(actualN-- > 0)
while (actualN-- > 0)
_onDispose!.Pop()();
}

View file

@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Actors;
using System.Reflection;
using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.Designs;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
@ -14,12 +16,12 @@ namespace Glamourer.Gui
{
public const float SelectorWidth = 200;
public const float MinWindowWidth = 675;
public const int GPoseActorId = 201;
public const int GPoseObjectId = 201;
private const string PluginName = "Glamourer";
private readonly string _glamourerHeader;
private readonly IReadOnlyDictionary<byte, Stain> _stains;
private readonly ActorTable _actors;
private readonly IReadOnlyDictionary<uint, ModelChara> _models;
private readonly IObjectIdentifier _identifier;
private readonly Dictionary<EquipSlot, (ComboWithFilter<Item>, ComboWithFilter<Stain>)> _combos;
private readonly ImGuiScene.TextureWrap? _legacyTattooIcon;
@ -27,8 +29,8 @@ namespace Glamourer.Gui
private readonly DesignManager _designs;
private readonly Glamourer _plugin;
private bool _visible = false;
private bool _inGPose = false;
private bool _visible;
private bool _inGPose;
public Interface(Glamourer plugin)
{
@ -37,31 +39,37 @@ namespace Glamourer.Gui
_glamourerHeader = Glamourer.Version.Length > 0
? $"{PluginName} v{Glamourer.Version}###{PluginName}Main"
: $"{PluginName}###{PluginName}Main";
Glamourer.PluginInterface.UiBuilder.DisableGposeUiHide = true;
Glamourer.PluginInterface.UiBuilder.OnBuildUi += Draw;
Glamourer.PluginInterface.UiBuilder.OnOpenConfigUi += ToggleVisibility;
Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true;
Dalamud.PluginInterface.UiBuilder.Draw += Draw;
Dalamud.PluginInterface.UiBuilder.OpenConfigUi += ToggleVisibility;
_characterConstructor = typeof(Character).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[]
{
typeof(IntPtr),
}, null)!;
_equipSlotNames = GetEquipSlotNames();
_stains = GameData.Stains(Glamourer.PluginInterface);
_identifier = Penumbra.GameData.GameData.GetIdentifier(Glamourer.PluginInterface);
_actors = Glamourer.PluginInterface.ClientState.Actors;
_stains = GameData.Stains(Dalamud.GameData);
_models = GameData.Models(Dalamud.GameData);
_identifier = Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData, Dalamud.ClientState.ClientLanguage);
var stainCombo = CreateDefaultStainCombo(_stains.Values.ToArray());
var equip = GameData.ItemsBySlot(Glamourer.PluginInterface);
var equip = GameData.ItemsBySlot(Dalamud.GameData);
_combos = equip.ToDictionary(kvp => kvp.Key, kvp => CreateCombos(kvp.Key, kvp.Value, stainCombo));
_legacyTattooIcon = GetLegacyTattooIcon();
}
public void ToggleVisibility(object _, object _2)
public void ToggleVisibility()
=> _visible = !_visible;
public void Dispose()
{
_legacyTattooIcon?.Dispose();
Glamourer.PluginInterface.UiBuilder.OnBuildUi -= Draw;
Glamourer.PluginInterface.UiBuilder.OnOpenConfigUi -= ToggleVisibility;
Dalamud.PluginInterface.UiBuilder.Draw -= Draw;
Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= ToggleVisibility;
}
private void Draw()
@ -72,7 +80,10 @@ namespace Glamourer.Gui
ImGui.SetNextWindowSizeConstraints(Vector2.One * MinWindowWidth * ImGui.GetIO().FontGlobalScale,
Vector2.One * 5000 * ImGui.GetIO().FontGlobalScale);
if (!ImGui.Begin(_glamourerHeader, ref _visible))
{
ImGui.End();
return;
}
try
{
@ -80,7 +91,7 @@ namespace Glamourer.Gui
if (!raii.Begin(() => ImGui.BeginTabBar("##tabBar"), ImGui.EndTabBar))
return;
_inGPose = _actors[GPoseActorId] != null;
_inGPose = Dalamud.Objects[GPoseObjectId] != null;
_iconSize = Vector2.One * ImGui.GetTextLineHeightWithSpacing() * 2;
_actualIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding;
_comboSelectorSize = 4 * _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
@ -89,9 +100,11 @@ namespace Glamourer.Gui
_raceSelectorWidth = _inputIntSize + _percentageSize - _actualIconSize.X;
_itemComboWidth = 6 * _actualIconSize.X + 4 * ImGui.GetStyle().ItemSpacing.X - ColorButtonWidth + 1;
DrawActorTab();
DrawPlayerTab();
DrawSaves();
DrawFixedDesignsTab();
DrawConfigTab();
DrawRevertablesTab();
}
finally
{

View file

@ -1,9 +1,13 @@
using System;
using System.Linq;
using System.Numerics;
using System.Windows.Forms;
using System.Reflection;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
using Dalamud.Plugin;
using Dalamud.Logging;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.FileSystem;
using ImGuiNET;
@ -12,14 +16,15 @@ 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 readonly CharacterSave _currentSave = new();
private string _newDesignName = string.Empty;
private bool _keyboardFocus;
private const string DesignNamePopupLabel = "Save Design As...";
private const uint RedHeaderColor = 0xFF1818C0;
private const uint GreenHeaderColor = 0xFF18C018;
private readonly ConstructorInfo _characterConstructor;
private void DrawActorHeader()
private void DrawPlayerHeader()
{
var color = _player == null ? RedHeaderColor : GreenHeaderColor;
var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
@ -30,14 +35,14 @@ namespace Glamourer.Gui
.PushColor(ImGuiCol.ButtonActive, buttonColor)
.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.PushStyle(ImGuiStyleVar.FrameRounding, 0);
ImGui.Button($"{_currentActorName}##actorHeader", -Vector2.UnitX * 0.0001f);
ImGui.Button($"{_currentLabel}##playerHeader", -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.SetClipboardText(save.ToBase64());
ImGui.PopFont();
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Copy customization code to clipboard.");
@ -54,7 +59,7 @@ namespace Glamourer.Gui
if (!applyButton)
return false;
var text = Clipboard.GetText();
var text = ImGui.GetClipboardText();
if (!text.Any())
return false;
@ -88,41 +93,100 @@ namespace Glamourer.Gui
private void DrawTargetPlayerButton()
{
if (ImGui.Button("Target Player"))
Glamourer.PluginInterface.ClientState.Targets.SetCurrentTarget(_player);
Dalamud.Targets.SetTarget(_player);
}
private void DrawApplyToPlayerButton(CharacterSave save)
{
if (ImGui.Button("Apply to Self"))
if (!ImGui.Button("Apply to Self"))
return;
var player = _inGPose
? (Character?) Dalamud.Objects[GPoseObjectId]
: Dalamud.ClientState.LocalPlayer;
var fallback = _inGPose ? Dalamud.ClientState.LocalPlayer : null;
if (player == null)
return;
save.Apply(player);
if (_inGPose)
save.Apply(fallback!);
Glamourer.Penumbra.UpdateCharacters(player, fallback);
}
private const int ModelTypeOffset = 0x01B4;
private static unsafe int ModelType(GameObject actor)
=> *(int*) (actor.Address + ModelTypeOffset);
private static unsafe void SetModelType(GameObject actor, int value)
=> *(int*) (actor.Address + ModelTypeOffset) = value;
private Character Character(IntPtr address)
=> (Character) _characterConstructor.Invoke(new object[]
{
var player = _inGPose
? Glamourer.PluginInterface.ClientState.Actors[GPoseActorId]
: Glamourer.PluginInterface.ClientState.LocalPlayer;
var fallback = _inGPose ? Glamourer.PluginInterface.ClientState.LocalPlayer : null;
if (player != null)
address,
});
private Character? CreateCharacter(GameObject? actor)
{
if (actor == null)
return null;
return actor switch
{
PlayerCharacter p => p,
BattleChara b => b,
_ => actor.ObjectKind switch
{
save.Apply(player);
if (_inGPose)
save.Apply(fallback!);
_plugin.UpdateActors(player, fallback);
}
}
ObjectKind.BattleNpc => Character(actor.Address),
ObjectKind.Companion => Character(actor.Address),
ObjectKind.EventNpc => Character(actor.Address),
_ => null,
},
};
}
private static Character? TransformToCustomizable(Character? actor)
{
if (actor == null)
return null;
if (ModelType(actor) == 0)
return actor;
SetModelType(actor, 0);
CharacterCustomization.Default.Write(actor.Address);
return actor;
}
private void DrawApplyToTargetButton(CharacterSave save)
{
if (ImGui.Button("Apply to Target"))
{
var player = Glamourer.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);
}
}
if (!ImGui.Button("Apply to Target"))
return;
var player = TransformToCustomizable(CreateCharacter(Dalamud.Targets.Target));
if (player == null)
return;
var fallBackCharacter = _gPoseActors.TryGetValue(player.Name.ToString(), out var f) ? f : null;
save.Apply(player);
if (fallBackCharacter != null)
save.Apply(fallBackCharacter);
Glamourer.Penumbra.UpdateCharacters(player, fallBackCharacter);
}
private void DrawRevertButton()
{
if (!DrawDisableButton("Revert", _player == null))
return;
Glamourer.RevertableDesigns.Revert(_player!);
var fallBackCharacter = _gPoseActors.TryGetValue(_player!.Name.ToString(), out var f) ? f : null;
if (fallBackCharacter != null)
Glamourer.RevertableDesigns.Revert(fallBackCharacter);
Glamourer.Penumbra.UpdateCharacters(_player, fallBackCharacter);
}
private void SaveNewDesign(CharacterSave save)
@ -130,13 +194,13 @@ namespace Glamourer.Gui
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();
}
if (!name.Any())
return;
var newDesign = new Design(folder, name) { Data = save };
folder.AddChild(newDesign);
_designs.Designs[newDesign.FullName()] = save;
_designs.SaveToFile();
}
catch (Exception e)
{
@ -144,13 +208,42 @@ namespace Glamourer.Gui
}
}
private void DrawActorPanel()
private void DrawMonsterPanel()
{
ImGui.BeginGroup();
DrawActorHeader();
if (!ImGui.BeginChild("##actorData", -Vector2.One, true))
if (DrawApplyClipboardButton())
Glamourer.Penumbra.UpdateCharacters(_player!);
ImGui.SameLine();
if (ImGui.Button("Convert to Character"))
{
TransformToCustomizable(_player);
_currentLabel = _currentLabel.Replace("(Monster)", "(NPC)");
Glamourer.Penumbra.UpdateCharacters(_player!);
}
if (!_inGPose)
{
ImGui.SameLine();
DrawTargetPlayerButton();
}
var currentModel = ModelType(_player!);
using var raii = new ImGuiRaii();
if (!raii.Begin(() => ImGui.BeginCombo("Model Id", currentModel.ToString()), ImGui.EndCombo))
return;
foreach (var (id, _) in _models.Skip(1))
{
if (!ImGui.Selectable($"{id:D6}##models", id == currentModel) || id == currentModel)
continue;
SetModelType(_player!, (int) id);
Glamourer.Penumbra.UpdateCharacters(_player!);
}
}
private void DrawPlayerPanel()
{
DrawCopyClipboardButton(_currentSave);
ImGui.SameLine();
var changes = DrawApplyClipboardButton();
@ -169,9 +262,12 @@ namespace Glamourer.Gui
}
}
ImGui.SameLine();
DrawRevertButton();
if (DrawCustomization(ref _currentSave.Customizations) && _player != null)
{
Glamourer.RevertableDesigns.Add(_player);
_currentSave.Customizations.Write(_player.Address);
changes = true;
}
@ -180,9 +276,25 @@ namespace Glamourer.Gui
changes |= DrawMiscellaneous(_currentSave, _player);
if (_player != null && changes)
_plugin.UpdateActors(_player);
Glamourer.Penumbra.UpdateCharacters(_player);
}
private void DrawActorPanel()
{
using var raii = ImGuiRaii.NewGroup();
DrawPlayerHeader();
if (!ImGui.BeginChild("##playerData", -Vector2.One, true))
{
ImGui.EndChild();
return;
}
if (_player == null || ModelType(_player) == 0)
DrawPlayerPanel();
else
DrawMonsterPanel();
ImGui.EndChild();
ImGui.EndGroup();
}
}
}

View file

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Actors;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
using ImGuiNET;
@ -10,51 +10,83 @@ 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 Character? _player;
private string _currentLabel = string.Empty;
private string _playerFilter = string.Empty;
private string _playerFilterLower = string.Empty;
private readonly Dictionary<string, int> _playerNames = new(100);
private readonly Dictionary<string, Character?> _gPoseActors = new(48);
private void DrawActorFilter()
private void DrawPlayerFilter()
{
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();
if (ImGui.InputTextWithHint("##playerFilter", "Filter Players...", ref _playerFilter, 32))
_playerFilterLower = _playerFilter.ToLowerInvariant();
}
private void DrawActorSelectable(Actor actor, bool gPose)
private void DrawGPoseSelectable(Character player)
{
var actorName = actor.Name;
if (!actorName.Any())
var playerName = player.Name.ToString();
if (!playerName.Any())
return;
if (_playerNames.ContainsKey(actorName))
_gPoseActors[playerName] = null;
DrawSelectable(player, $"{playerName} (GPose)");
}
private static string GetLabel(Character player, string playerName, int num)
{
if (player.ObjectKind == ObjectKind.Player)
return num == 1 ? playerName : $"{playerName} #{num}";
if (ModelType(player) == 0)
return num == 1 ? $"{playerName} (NPC)" : $"{playerName} #{num} (NPC)";
return num == 1 ? $"{playerName} (Monster)" : $"{playerName} #{num} (Monster)";
}
private void DrawPlayerSelectable(Character player)
{
var playerName = player.Name.ToString();
if (!playerName.Any())
return;
if (_playerNames.TryGetValue(playerName, out var num))
_playerNames[playerName] = ++num;
else
_playerNames[playerName] = num = 1;
if (_gPoseActors.ContainsKey(playerName))
{
_playerNames[actorName] = actor;
_gPoseActors[playerName] = player;
return;
}
_playerNames.Add(actorName, null);
var label = GetLabel(player, playerName, num);
DrawSelectable(player, label);
}
var label = gPose ? $"{actorName} (GPose)" : actorName;
if (!_actorFilterLower.Any() || actorName.ToLowerInvariant().Contains(_actorFilterLower))
if (ImGui.Selectable(label, _currentActorName == actorName))
private void DrawSelectable(Character player, string label)
{
if (!_playerFilterLower.Any() || label.ToLowerInvariant().Contains(_playerFilterLower))
if (ImGui.Selectable(label, _currentLabel == label))
{
_currentActorName = actorName;
_currentSave.LoadActor(actor);
_player = actor;
_currentLabel = label;
_currentSave.LoadCharacter(player);
_player = player;
return;
}
if (_currentActorName == actor.Name)
{
_currentSave.LoadActor(actor);
_player = actor;
}
if (_currentLabel != label)
return;
_currentSave.LoadCharacter(player);
_player = player;
}
private void DrawSelectionButtons()
@ -63,10 +95,10 @@ namespace Glamourer.Gui
.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.PushStyle(ImGuiStyleVar.FrameRounding, 0)
.PushFont(UiBuilder.IconFont);
Actor? select = null;
var buttonWidth = Vector2.UnitX * SelectorWidth / 2;
Character? select = null;
var buttonWidth = Vector2.UnitX * SelectorWidth / 2;
if (ImGui.Button(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth))
select = Glamourer.PluginInterface.ClientState.LocalPlayer;
select = Dalamud.ClientState.LocalPlayer;
raii.PopFonts();
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Select the local player character.");
@ -81,49 +113,60 @@ namespace Glamourer.Gui
else
{
if (ImGui.Button(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth))
select = Glamourer.PluginInterface.ClientState.Targets.CurrentTarget;
select = CreateCharacter(Dalamud.Targets.Target);
}
raii.PopFonts();
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Select the current target, if it is a player actor.");
ImGui.SetTooltip("Select the current target, if it is in the list.");
if (select == null || select.ObjectKind != ObjectKind.Player)
if (select == null)
return;
_player = select;
_currentActorName = _player.Name;
_currentSave.LoadActor(_player);
_player = select;
_currentLabel = _player.Name.ToString();
_currentSave.LoadCharacter(_player);
}
private void DrawActorSelector()
private void DrawPlayerSelector()
{
ImGui.BeginGroup();
DrawActorFilter();
if (!ImGui.BeginChild("##actorSelector",
DrawPlayerFilter();
if (!ImGui.BeginChild("##playerSelector",
new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true))
{
ImGui.EndChild();
ImGui.EndGroup();
return;
}
_playerNames.Clear();
for (var i = GPoseActorId; i < GPoseActorId + 48; ++i)
_gPoseActors.Clear();
for (var i = GPoseObjectId; i < GPoseObjectId + 48; ++i)
{
var actor = _actors[i];
if (actor == null)
var player = CreateCharacter(Dalamud.Objects[i]);
if (player == null)
break;
if (actor.ObjectKind == ObjectKind.Player)
DrawActorSelectable(actor, true);
DrawGPoseSelectable(player);
}
for (var i = 0; i < GPoseActorId; i += 2)
for (var i = 0; i < GPoseObjectId; ++i)
{
var actor = _actors[i];
if (actor != null && actor.ObjectKind == ObjectKind.Player)
DrawActorSelectable(actor, false);
var player = CreateCharacter(Dalamud.Objects[i])!;
if (player != null)
DrawPlayerSelectable(player);
}
for (var i = GPoseObjectId + 48; i < Dalamud.Objects.Length; ++i)
{
var player = CreateCharacter(Dalamud.Objects[i])!;
if (player != null)
DrawPlayerSelectable(player);
}
using (var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
using (var _ = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
{
ImGui.EndChild();
}
@ -132,16 +175,16 @@ namespace Glamourer.Gui
ImGui.EndGroup();
}
private void DrawActorTab()
private void DrawPlayerTab()
{
using var raii = new ImGuiRaii();
_player = null;
if (!raii.Begin(() => ImGui.BeginTabItem("Current Players"), ImGui.EndTabItem))
return;
_player = null;
DrawActorSelector();
DrawPlayerSelector();
if (!_currentActorName.Any())
if (!_currentLabel.Any())
return;
ImGui.SameLine();

View file

@ -53,11 +53,8 @@ namespace Glamourer.Gui
return;
}
if (ImGui.Button(buttonLabel) && _plugin.GetPenumbra())
{
_plugin.UnregisterFunctions();
_plugin.RegisterFunctions();
}
if (ImGui.Button(buttonLabel))
Glamourer.Penumbra.Reattach(true);
if (ImGui.IsItemHovered())
ImGui.SetTooltip(
@ -87,18 +84,25 @@ namespace Glamourer.Gui
{
cfg.AttachToPenumbra = v;
if (v)
{
if (_plugin.GetPenumbra())
_plugin.RegisterFunctions();
}
Glamourer.Penumbra.Reattach(true);
else
{
_plugin.UnregisterFunctions();
}
Glamourer.Penumbra.Unattach();
});
ImGui.SameLine();
DrawRestorePenumbraButton();
DrawConfigCheckMark("Apply Fixed Designs",
"Automatically apply fixed designs to characters and redraw them when anything changes.",
cfg.ApplyFixedDesigns,
v =>
{
cfg.ApplyFixedDesigns = v;
if (v)
Glamourer.PlayerWatcher.Enable();
else
Glamourer.PlayerWatcher.Disable();
});
ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() / 2);
DrawColorPicker("Customization Color", "The color for designs that only apply their character customization.",

View file

@ -2,7 +2,7 @@
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Plugin;
using Dalamud.Logging;
using Glamourer.Customization;
using ImGuiNET;
using Penumbra.GameData.Enums;
@ -39,20 +39,20 @@ namespace Glamourer.Gui
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 Vector2 _iconSize = Vector2.Zero;
private Vector2 _actualIconSize = Vector2.Zero;
private float _raceSelectorWidth;
private float _inputIntSize;
private float _comboSelectorSize;
private float _percentageSize;
private float _itemComboWidth;
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)
if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue) && tmp != value + 1 && tmp >= minValue && tmp <= maxValue)
{
value = tmp - 1;
ret = true;
@ -64,7 +64,7 @@ namespace Glamourer.Gui
return ret;
}
private static (int, Customization.Customization) GetCurrentCustomization(ref ActorCustomization customization, CustomizationId id,
private static (int, Customization.Customization) GetCurrentCustomization(ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
var current = set.DataByValue(id, customization[id], out var custom);
@ -78,7 +78,7 @@ namespace Glamourer.Gui
return (current, custom!.Value);
}
private bool DrawColorPicker(string label, string tooltip, ref ActorCustomization customization, CustomizationId id,
private bool DrawColorPicker(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
var ret = false;
@ -93,11 +93,11 @@ namespace Glamourer.Gui
ImGui.SameLine();
using (var group = ImGuiRaii.NewGroup())
using (var _ = ImGuiRaii.NewGroup())
{
if (InputInt($"##text_{id}", ref current, 1, count))
{
customization[id] = set.Data(id, current - 1).Value;
customization[id] = set.Data(id, current).Value;
ret = true;
}
@ -116,7 +116,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawListSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id,
private bool DrawListSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
using var bigGroup = ImGuiRaii.NewGroup();
@ -158,7 +158,7 @@ namespace Glamourer.Gui
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)
private bool DrawMultiSelector(ref CharacterCustomization customization, CustomizationSet set)
{
using var bigGroup = ImGuiRaii.NewGroup();
var ret = false;
@ -242,7 +242,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawIconSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id,
private bool DrawIconSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
using var bigGroup = ImGuiRaii.NewGroup();
@ -282,6 +282,9 @@ namespace Glamourer.Gui
ret = true;
}
if (id == CustomizationId.Hairstyle && customization.Race == Race.Hrothgar)
customization[CustomizationId.Face] = (byte) ((customization[CustomizationId.Hairstyle] + 1) / 2);
ImGui.Text(label);
if (tooltip.Any() && ImGui.IsItemHovered())
ImGui.SetTooltip(tooltip);
@ -290,7 +293,7 @@ namespace Glamourer.Gui
}
private bool DrawPercentageSelector(string label, string tooltip, ref ActorCustomization customization, CustomizationId id,
private bool DrawPercentageSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
using var bigGroup = ImGuiRaii.NewGroup();
@ -320,7 +323,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawRaceSelector(ref ActorCustomization customization)
private bool DrawRaceSelector(ref CharacterCustomization customization)
{
using var group = ImGuiRaii.NewGroup();
var ret = false;
@ -345,7 +348,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawGenderSelector(ref ActorCustomization customization)
private bool DrawGenderSelector(ref CharacterCustomization customization)
{
var ret = false;
ImGui.PushFont(UiBuilder.IconFont);
@ -376,7 +379,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawPicker(CustomizationSet set, CustomizationId id, ref ActorCustomization customization)
private bool DrawPicker(CustomizationSet set, CustomizationId id, ref CharacterCustomization customization)
{
if (!set.IsAvailable(id))
return false;
@ -394,9 +397,18 @@ namespace Glamourer.Gui
return false;
}
private static readonly CustomizationId[] AllCustomizations = (CustomizationId[]) Enum.GetValues(typeof(CustomizationId));
private static CustomizationId[] GetCustomizationOrder()
{
var ret = (CustomizationId[])Enum.GetValues(typeof(CustomizationId));
ret[(int) CustomizationId.TattooColor] = CustomizationId.EyeColorL;
ret[(int) CustomizationId.EyeColorL] = CustomizationId.EyeColorR;
ret[(int) CustomizationId.EyeColorR] = CustomizationId.TattooColor;
return ret;
}
private bool DrawCustomization(ref ActorCustomization custom)
private static readonly CustomizationId[] AllCustomizations = GetCustomizationOrder();
private bool DrawCustomization(ref CharacterCustomization custom)
{
if (!ImGui.CollapsingHeader("Character Customization"))
return false;
@ -457,7 +469,7 @@ namespace Glamourer.Gui
}
tmp = custom.SmallIris;
if (ImGui.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {set.Option(CustomizationId.EyeColorL)}",
if (ImGui.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}",
ref tmp)
&& tmp != custom.SmallIris)
{

View file

@ -1,9 +1,8 @@
using System;
using System.Linq;
using System.Numerics;
using System.Windows.Forms;
using Dalamud.Interface;
using Dalamud.Plugin;
using Dalamud.Logging;
using Glamourer.Designs;
using Glamourer.FileSystem;
using ImGuiNET;
@ -12,9 +11,10 @@ namespace Glamourer.Gui
{
internal partial class Interface
{
private int _totalObject = 0;
private int _totalObject;
private Design? _selection = null;
private bool _inDesignMode;
private Design? _selection;
private string _newChildName = string.Empty;
private void DrawDesignSelector()
@ -50,7 +50,7 @@ namespace Glamourer.Gui
if (_selection!.Data.WriteProtected || !applyButton)
return;
var text = Clipboard.GetText();
var text = ImGui.GetClipboardText();
if (!text.Any())
return;
@ -208,7 +208,8 @@ namespace Glamourer.Gui
{
using var raii = new ImGuiRaii();
raii.PushStyle(ImGuiStyleVar.IndentSpacing, 12.5f * ImGui.GetIO().FontGlobalScale);
if (!raii.Begin(() => ImGui.BeginTabItem("Saves"), ImGui.EndTabItem))
_inDesignMode = raii.Begin(() => ImGui.BeginTabItem("Designs"), ImGui.EndTabItem);
if (!_inDesignMode)
return;
DrawDesignSelector();
@ -279,8 +280,7 @@ namespace Glamourer.Gui
private void ContextMenu(IFileSystemBase child)
{
var label = $"##fsPopup{child.FullName()}";
var renameLabel = $"{label}_rename";
var label = $"##fsPopup{child.FullName()}";
if (ImGui.BeginPopup(label))
{
if (ImGui.MenuItem("Delete"))
@ -289,7 +289,7 @@ namespace Glamourer.Gui
RenameChildInput(child);
if (child is Design d && ImGui.MenuItem("Copy to Clipboard"))
Clipboard.SetText(d.Data.ToBase64());
ImGui.SetClipboardText(d.Data.ToBase64());
ImGui.EndPopup();
}
@ -310,12 +310,12 @@ namespace Glamourer.Gui
var changesStates = save.SetHatState || save.SetVisorState || save.SetWeaponState || save.IsWet || save.Alpha != 1.0f;
if (save.WriteCustomizations)
if (save.WriteEquipment != ActorEquipMask.None)
if (save.WriteEquipment != CharacterEquipMask.None)
return white;
else
return changesStates ? white : Glamourer.Config.CustomizationColor;
if (save.WriteEquipment != ActorEquipMask.None)
if (save.WriteEquipment != CharacterEquipMask.None)
return changesStates ? white : Glamourer.Config.EquipmentColor;
return changesStates ? Glamourer.Config.StateColor : grey;

View file

@ -15,10 +15,17 @@ namespace Glamourer.Gui
stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush);
}
if (stainCombo.Draw(string.Empty, out var newStain) && _player != null && !newStain.RowIndex.Equals(stainIdx))
if (stainCombo.Draw(string.Empty, out var newStain) && !newStain.RowIndex.Equals(stainIdx))
{
newStain.Write(_player.Address, slot);
return true;
if (_player != null)
{
Glamourer.RevertableDesigns.Add(_player);
newStain.Write(_player.Address, slot);
return true;
}
if (_inDesignMode && (_selection?.Data.WriteStain(slot, newStain.RowIndex) ?? false))
return true;
}
return false;
@ -27,22 +34,29 @@ namespace Glamourer.Gui
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)
if (equipCombo.Draw(currentName, out var newItem, _itemComboWidth) && newItem.Base.RowId != item?.RowId)
{
newItem.Write(_player.Address);
return true;
if (_player != null)
{
Glamourer.RevertableDesigns.Add(_player);
newItem.Write(_player.Address);
return true;
}
if (_inDesignMode && (_selection?.Data.WriteItem(newItem) ?? false))
return true;
}
return false;
}
private static bool DrawCheckbox(ActorEquipMask flag, ref ActorEquipMask mask)
private static bool DrawCheckbox(CharacterEquipMask flag, ref CharacterEquipMask mask)
{
var tmp = (uint) mask;
var ret = false;
if (ImGui.CheckboxFlags($"##flag_{(uint) flag}", ref tmp, (uint) flag) && tmp != (uint) mask)
{
mask = (ActorEquipMask) tmp;
mask = (CharacterEquipMask) tmp;
ret = true;
}
@ -51,7 +65,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawEquipSlot(EquipSlot slot, ActorArmor equip)
private bool DrawEquipSlot(EquipSlot slot, CharacterArmor equip)
{
var (equipCombo, stainCombo) = _combos[slot];
@ -63,7 +77,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawEquipSlotWithCheck(EquipSlot slot, ActorArmor equip, ActorEquipMask flag, ref ActorEquipMask mask)
private bool DrawEquipSlotWithCheck(EquipSlot slot, CharacterArmor equip, CharacterEquipMask flag, ref CharacterEquipMask mask)
{
var ret = DrawCheckbox(flag, ref mask);
ImGui.SameLine();
@ -71,7 +85,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawWeapon(EquipSlot slot, ActorWeapon weapon)
private bool DrawWeapon(EquipSlot slot, CharacterWeapon weapon)
{
var (equipCombo, stainCombo) = _combos[slot];
@ -83,7 +97,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawWeaponWithCheck(EquipSlot slot, ActorWeapon weapon, ActorEquipMask flag, ref ActorEquipMask mask)
private bool DrawWeaponWithCheck(EquipSlot slot, CharacterWeapon weapon, CharacterEquipMask flag, ref CharacterEquipMask mask)
{
var ret = DrawCheckbox(flag, ref mask);
ImGui.SameLine();
@ -91,7 +105,7 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawEquip(ActorEquipment equip)
private bool DrawEquip(CharacterEquipment equip)
{
var ret = false;
if (ImGui.CollapsingHeader("Character Equipment"))
@ -113,23 +127,23 @@ namespace Glamourer.Gui
return ret;
}
private bool DrawEquip(ActorEquipment equip, ref ActorEquipMask mask)
private bool DrawEquip(CharacterEquipment equip, ref CharacterEquipMask 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);
ret |= DrawWeaponWithCheck(EquipSlot.MainHand, equip.MainHand, CharacterEquipMask.MainHand, ref mask);
ret |= DrawWeaponWithCheck(EquipSlot.OffHand, equip.OffHand, CharacterEquipMask.OffHand, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.Head, equip.Head, CharacterEquipMask.Head, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.Body, equip.Body, CharacterEquipMask.Body, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.Hands, equip.Hands, CharacterEquipMask.Hands, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.Legs, equip.Legs, CharacterEquipMask.Legs, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.Feet, equip.Feet, CharacterEquipMask.Feet, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.Ears, equip.Ears, CharacterEquipMask.Ears, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.Neck, equip.Neck, CharacterEquipMask.Neck, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.Wrists, equip.Wrists, CharacterEquipMask.Wrists, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.RFinger, equip.RFinger, CharacterEquipMask.RFinger, ref mask);
ret |= DrawEquipSlotWithCheck(EquipSlot.LFinger, equip.LFinger, CharacterEquipMask.LFinger, ref mask);
}
return ret;

View file

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Glamourer.Designs;
using Glamourer.FileSystem;
using ImGuiNET;
namespace Glamourer.Gui
{
internal partial class Interface
{
private const string FixDragDropLabel = "##FixDragDrop";
private List<string>? _fullPathCache;
private string _newFixCharacterName = string.Empty;
private string _newFixDesignPath = string.Empty;
private Design? _newFixDesign;
private int _fixDragDropIdx = -1;
private static unsafe bool IsDropping()
=> ImGui.AcceptDragDropPayload(FixDragDropLabel).NativePtr != null;
private void DrawFixedDesignsTab()
{
using var raii = new ImGuiRaii();
if (!raii.Begin(() => ImGui.BeginTabItem("Fixed Designs"), ImGui.EndTabItem))
{
_fullPathCache = null;
_newFixDesign = null;
_newFixDesignPath = string.Empty;
return;
}
_fullPathCache ??= _plugin.FixedDesigns.Data.Select(d => d.Design.FullName()).ToList();
raii.Begin(() => ImGui.BeginTable("##FixedTable", 3), ImGui.EndTable);
var buttonWidth = 23.5f * ImGuiHelpers.GlobalScale;
ImGui.TableSetupColumn("##DeleteColumn", ImGuiTableColumnFlags.WidthFixed, 2 * buttonWidth);
ImGui.TableSetupColumn("Character", ImGuiTableColumnFlags.WidthFixed, 200 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Design", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableHeadersRow();
var xPos = 0f;
for (var i = 0; i < _fullPathCache.Count; ++i)
{
var path = _fullPathCache[i];
var name = _plugin.FixedDesigns.Data[i];
ImGui.TableNextRow();
ImGui.TableNextColumn();
raii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2);
raii.PushFont(UiBuilder.IconFont);
if (ImGui.Button($"{FontAwesomeIcon.Trash.ToIconChar()}##{i}"))
{
_fullPathCache.RemoveAt(i);
_plugin.FixedDesigns.Remove(name);
}
var tmp = name.Enabled;
ImGui.SameLine();
xPos = ImGui.GetCursorPosX();
if (ImGui.Checkbox($"##Enabled{i}", ref tmp))
if (tmp && _plugin.FixedDesigns.EnableDesign(name)
|| !tmp && _plugin.FixedDesigns.DisableDesign(name))
Glamourer.Config.Save();
raii.PopStyles();
raii.PopFonts();
ImGui.TableNextColumn();
ImGui.Selectable($"{name.Name}##Fix{i}");
if (ImGui.BeginDragDropSource())
{
_fixDragDropIdx = i;
ImGui.SetDragDropPayload("##FixDragDrop", IntPtr.Zero, 0);
ImGui.Text($"Dragging {name.Name} ({path})...");
ImGui.EndDragDropSource();
}
if (ImGui.BeginDragDropTarget())
{
if (IsDropping() && _fixDragDropIdx >= 0)
{
var d = _plugin.FixedDesigns.Data[_fixDragDropIdx];
_plugin.FixedDesigns.Move(d, i);
var p = _fullPathCache[_fixDragDropIdx];
_fullPathCache.RemoveAt(_fixDragDropIdx);
_fullPathCache.Insert(i, p);
_fixDragDropIdx = -1;
}
ImGui.EndDragDropTarget();
}
ImGui.TableNextColumn();
ImGui.Text(path);
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
raii.PushFont(UiBuilder.IconFont);
ImGui.SetCursorPosX(xPos);
if (_newFixDesign == null || _newFixCharacterName == string.Empty)
{
raii.PushStyle(ImGuiStyleVar.Alpha, 0.5f);
ImGui.Button($"{FontAwesomeIcon.Plus.ToIconChar()}##NewFix");
raii.PopStyles();
}
else if (ImGui.Button($"{FontAwesomeIcon.Plus.ToIconChar()}##NewFix"))
{
_fullPathCache.Add(_newFixDesignPath);
_plugin.FixedDesigns.Add(_newFixCharacterName, _newFixDesign, false);
_newFixCharacterName = string.Empty;
_newFixDesignPath = string.Empty;
_newFixDesign = null;
}
raii.PopFonts();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.InputTextWithHint("##NewFix", "Enter new Character", ref _newFixCharacterName, 32);
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(-1);
if (!raii.Begin(() => ImGui.BeginCombo("##NewFixPath", _newFixDesignPath), ImGui.EndCombo))
return;
foreach (var design in _plugin.Designs.FileSystem.Root.AllLeaves(SortMode.Lexicographical).Cast<Design>())
{
var fullName = design.FullName();
ImGui.SetNextItemWidth(-1);
if (!ImGui.Selectable($"{fullName}##NewFixDesign", fullName == _newFixDesignPath))
continue;
_newFixDesignPath = fullName;
_newFixDesign = design;
}
}
}
}

View file

@ -1,11 +1,8 @@
using System;
using System.Linq;
using System.Windows.Forms;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Plugin;
using Dalamud.Logging;
using Glamourer.Customization;
using ImGuiNET;
using Penumbra.Api;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui
@ -27,7 +24,7 @@ namespace Glamourer.Gui
}
// Go through a whole customization struct and fix up all settings that need fixing.
private static void FixUpAttributes(ref ActorCustomization customization)
private static void FixUpAttributes(ref CharacterCustomization customization)
{
var set = Glamourer.Customization.GetList(customization.Clan, customization.Gender);
foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId)))
@ -46,7 +43,7 @@ namespace Glamourer.Gui
break;
default:
var count = set.Count(id);
if (set.DataByValue(id, customization[id], out var value) < 0)
if (set.DataByValue(id, customization[id], out _) < 0)
if (count == 0)
customization[id] = 0;
else
@ -57,7 +54,7 @@ namespace Glamourer.Gui
}
// Change a race and fix up all required customizations afterwards.
private static bool ChangeRace(ref ActorCustomization customization, SubRace clan)
private static bool ChangeRace(ref CharacterCustomization customization, SubRace clan)
{
if (clan == customization.Clan)
return false;
@ -79,7 +76,7 @@ namespace Glamourer.Gui
}
// Change a gender and fix up all required customizations afterwards.
private static bool ChangeGender(ref ActorCustomization customization, Gender gender)
private static bool ChangeGender(ref CharacterCustomization customization, Gender gender)
{
if (gender == customization.Gender)
return false;
@ -159,7 +156,7 @@ namespace Glamourer.Gui
break;
case DesignNameUse.NewDesign:
var empty = new CharacterSave();
empty.Load(ActorCustomization.Default);
empty.Load(CharacterCustomization.Default);
empty.WriteCustomizations = false;
SaveNewDesign(empty);
break;
@ -173,7 +170,7 @@ namespace Glamourer.Gui
case DesignNameUse.FromClipboard:
try
{
var text = Clipboard.GetText();
var text = ImGui.GetClipboardText();
var save = CharacterSave.FromString(text);
SaveNewDesign(save);
}

View file

@ -53,7 +53,7 @@ namespace Glamourer.Gui
{
var rawImage = new byte[resource.Length];
resource.Read(rawImage, 0, (int) resource.Length);
return Glamourer.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4);
return Dalamud.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4);
}
return null;
@ -61,7 +61,7 @@ namespace Glamourer.Gui
private static Dictionary<EquipSlot, string> GetEquipSlotNames()
{
var sheet = Glamourer.PluginInterface.Data.GetExcelSheet<Addon>();
var sheet = Dalamud.GameData.GetExcelSheet<Addon>()!;
var ret = new Dictionary<EquipSlot, string>(12)
{
[EquipSlot.MainHand] = sheet.GetRow(738)?.Text.ToString() ?? "Main Hand",

View file

@ -1,5 +1,5 @@
using System;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Objects.Types;
using ImGuiNET;
namespace Glamourer.Gui
@ -18,7 +18,18 @@ namespace Glamourer.Gui
return false;
}
private static bool DrawMiscellaneous(CharacterSave save, Actor? player)
private static bool DrawDisableButton(string label, bool disabled)
{
if (!disabled)
return ImGui.Button(label);
using var raii = new ImGuiRaii();
raii.PushStyle(ImGuiStyleVar.Alpha, 0.5f);
ImGui.Button(label);
return false;
}
private static bool DrawMiscellaneous(CharacterSave save, Character? player)
{
var ret = false;
if (!ImGui.CollapsingHeader("Miscellaneous"))

View file

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

View file

@ -1,320 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.Command;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.FileSystem;
using Glamourer.Gui;
using ImGuiNET;
using Penumbra.Api;
using Penumbra.PlayerWatch;
namespace Glamourer
{
public class Glamourer : IDalamudPlugin
{
public const int RequiredPenumbraShareVersion = 1;
private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],<Name for Save>";
public string Name
=> "Glamourer";
public static DalamudPluginInterface PluginInterface = null!;
public static GlamourerConfig Config = null!;
private Interface _interface = null!;
public static ICustomizationManager Customization = null!;
public DesignManager Designs = null!;
public IPlayerWatcher PlayerWatcher = null!;
public static string Version = string.Empty;
public static IPenumbraApi? Penumbra;
private Dalamud.Dalamud _dalamud = null!;
private List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> _plugins = null!;
private void SetDalamud(DalamudPluginInterface pi)
{
var dalamud = (Dalamud.Dalamud?) pi.GetType()
?.GetField("dalamud", BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(pi);
_dalamud = dalamud ?? throw new Exception("Could not obtain Dalamud.");
}
private static void PenumbraTooltip(object? it)
{
if (it is Lumina.Excel.GeneratedSheets.Item)
ImGui.Text("Right click to apply to current Glamourer Set. [Glamourer]");
}
private void PenumbraRightClick(MouseButton button, object? it)
{
if (button == MouseButton.Right && it is Lumina.Excel.GeneratedSheets.Item item)
{
var actors = PluginInterface.ClientState.Actors;
var gPose = actors[Interface.GPoseActorId];
var player = actors[0];
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);
UpdateActors(player);
}
}
}
public void RegisterFunctions()
{
if (Penumbra == null || !Penumbra.Valid)
return;
Penumbra!.ChangedItemTooltip += PenumbraTooltip;
Penumbra!.ChangedItemClicked += PenumbraRightClick;
}
public void UnregisterFunctions()
{
if (Penumbra == null || !Penumbra.Valid)
return;
Penumbra!.ChangedItemTooltip -= PenumbraTooltip;
Penumbra!.ChangedItemClicked -= PenumbraRightClick;
}
private void SetPlugins(DalamudPluginInterface pi)
{
var pluginManager = _dalamud?.GetType()
?.GetProperty("PluginManager", BindingFlags.Instance | BindingFlags.NonPublic)
?.GetValue(_dalamud);
if (pluginManager == null)
throw new Exception("Could not obtain plugin manager.");
var pluginsList =
(List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)>?) pluginManager
?.GetType()
?.GetProperty("Plugins", BindingFlags.Instance | BindingFlags.Public)
?.GetValue(pluginManager);
_plugins = pluginsList ?? throw new Exception("Could not obtain Dalamud.");
}
public bool GetPenumbra()
{
if (Penumbra?.Valid ?? false)
return true;
var plugin = _plugins.Find(p
=> p.Definition.InternalName == "Penumbra"
&& string.Compare(p.Definition.AssemblyVersion, "0.4.0.3", StringComparison.Ordinal) >= 0).Plugin;
var penumbra = (IPenumbraApiBase?) plugin?.GetType().GetProperty("Api", BindingFlags.Instance | BindingFlags.Public)
?.GetValue(plugin);
if (penumbra != null && penumbra.Valid && penumbra.ApiVersion >= RequiredPenumbraShareVersion)
Penumbra = (IPenumbraApi) penumbra!;
else
Penumbra = null;
return Penumbra != null;
}
public void Initialize(DalamudPluginInterface pluginInterface)
{
Version = Assembly.GetExecutingAssembly()?.GetName().Version.ToString() ?? "";
PluginInterface = pluginInterface;
Config = GlamourerConfig.Create();
Customization = CustomizationManager.Create(PluginInterface);
SetDalamud(PluginInterface);
SetPlugins(PluginInterface);
Designs = new DesignManager(PluginInterface);
if (GetPenumbra() && Config.AttachToPenumbra)
RegisterFunctions();
PlayerWatcher = PlayerWatchFactory.Create(PluginInterface);
PluginInterface.CommandManager.AddHandler("/glamourer", new CommandInfo(OnGlamourer)
{
HelpMessage = "Open or close the Glamourer window.",
});
PluginInterface.CommandManager.AddHandler("/glamour", new CommandInfo(OnGlamour)
{
HelpMessage = $"Use Glamourer Functions: {HelpString}",
});
_interface = new Interface(this);
}
public void OnGlamourer(string command, string arguments)
=> _interface?.ToggleVisibility(null!, null!);
private Actor? GetActor(string name)
{
var lowerName = name.ToLowerInvariant();
return lowerName switch
{
"" => 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()
{
PlayerWatcher?.Dispose();
UnregisterFunctions();
_interface?.Dispose();
PluginInterface.CommandManager.RemoveHandler("/glamour");
PluginInterface.CommandManager.RemoveHandler("/glamourer");
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);
}
}
}
}

140
Glamourer/PenumbraAttach.cs Normal file
View file

@ -0,0 +1,140 @@
using System;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
using Dalamud.Plugin.Ipc;
using Glamourer.Gui;
using ImGuiNET;
using Penumbra.GameData.Enums;
namespace Glamourer
{
public class PenumbraAttach : IDisposable
{
public const int RequiredPenumbraShareVersion = 3;
private ICallGateSubscriber<ChangedItemType, uint, object>? _tooltipSubscriber;
private ICallGateSubscriber<MouseButton, ChangedItemType, uint, object>? _clickSubscriber;
private ICallGateSubscriber<string, int, object>? _redrawSubscriberName;
private ICallGateSubscriber<GameObject, int, object>? _redrawSubscriberObject;
public PenumbraAttach(bool attach)
=> Reattach(attach);
public void Reattach(bool attach)
{
try
{
Unattach();
var versionSubscriber = Dalamud.PluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion");
var version = versionSubscriber.InvokeFunc();
if (version != RequiredPenumbraShareVersion)
throw new Exception($"Invalid Version {version}, required Version {RequiredPenumbraShareVersion}.");
_redrawSubscriberName = Dalamud.PluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
_redrawSubscriberObject = Dalamud.PluginInterface.GetIpcSubscriber<GameObject, int, object>("Penumbra.RedrawObject");
if (!attach)
return;
_tooltipSubscriber = Dalamud.PluginInterface.GetIpcSubscriber<ChangedItemType, uint, object>("Penumbra.ChangedItemTooltip");
_clickSubscriber =
Dalamud.PluginInterface.GetIpcSubscriber<MouseButton, ChangedItemType, uint, object>("Penumbra.ChangedItemClick");
_tooltipSubscriber.Subscribe(PenumbraTooltip);
_clickSubscriber.Subscribe(PenumbraRightClick);
}
catch (Exception e)
{
PluginLog.Debug($"Could not attach to Penumbra:\n{e}");
}
}
public void Unattach()
{
_tooltipSubscriber?.Unsubscribe(PenumbraTooltip);
_clickSubscriber?.Unsubscribe(PenumbraRightClick);
_tooltipSubscriber = null;
_clickSubscriber = null;
_redrawSubscriberName = null;
_redrawSubscriberObject = null;
}
public void Dispose()
=> Unattach();
private static void PenumbraTooltip(ChangedItemType type, uint _)
{
if (type == ChangedItemType.Item)
ImGui.Text("Right click to apply to current Glamourer Set. [Glamourer]");
}
private void PenumbraRightClick(MouseButton button, ChangedItemType type, uint id)
{
if (button != MouseButton.Right || type != ChangedItemType.Item)
return;
var gPose = Dalamud.Objects[Interface.GPoseObjectId] as Character;
var player = Dalamud.Objects[0] as Character;
var item = (Lumina.Excel.GeneratedSheets.Item) type.GetObject(id)!;
var writeItem = new Item(item, string.Empty);
if (gPose != null)
{
writeItem.Write(gPose.Address);
UpdateCharacters(gPose, player);
}
else if (player != null)
{
writeItem.Write(player.Address);
UpdateCharacters(player);
}
}
public void RedrawObject(GameObject actor, RedrawType settings, bool repeat)
{
if (_redrawSubscriberObject != null)
{
try
{
_redrawSubscriberObject.InvokeAction(actor, (int) settings);
}
catch (Exception e)
{
if (repeat)
{
Reattach(Glamourer.Config.AttachToPenumbra);
RedrawObject(actor, settings, false);
}
else
{
PluginLog.Debug($"Failure redrawing object:\n{e}");
}
}
}
else if (repeat)
{
Reattach(Glamourer.Config.AttachToPenumbra);
RedrawObject(actor, settings, false);
}
else
{
PluginLog.Debug("Trying to redraw object, but not attached to Penumbra.");
}
}
// Update objects without triggering PlayerWatcher Events,
// then manually redraw using Penumbra.
public void UpdateCharacters(Character character, Character? gPoseOriginalCharacter = null)
{
var newEquip = Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(character);
RedrawObject(character, RedrawType.WithSettings, true);
// Special case for carrying over changes to the gPose player to the regular player, too.
if (gPoseOriginalCharacter == null)
return;
newEquip.Write(gPoseOriginalCharacter.Address);
Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(gPoseOriginalCharacter);
RedrawObject(gPoseOriginalCharacter, RedrawType.AfterGPoseWithSettings, false);
}
}
}

BIN
images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View file

@ -1,19 +1,21 @@
[
{
"Author": "Ottermandias",
"Name": "Glamourer",
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
"InternalName": "Glamourer",
"AssemblyVersion": "0.0.3.0",
"TestingAssemblyVersion": "0.0.3.0",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 3,
"IsHide": "False",
"IsTestingExclusive": "false",
"DownloadCount": 1,
"LastUpdate": 1618608322,
"DownloadLinkInstall": "https://github.com/Ottermandias/Glamourer/raw/main/Glamourer.zip",
"DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/raw/main/Glamourer.zip",
"Author": "Ottermandias",
"Name": "Glamourer",
"Punchline": "Change and save appearance of players.",
"Description": "Adds functionality to change and store appearance of players, customization and equip. Requires Penumbra to be installed and activated to work. Can also add preview options to the Changed Items tab for Penumbra.",
"Tags": [ "Appearance", "Glamour", "Race", "Outfit", "Armor", "Clothes", "Skins", "Customization", "Design", "Character" ],
"InternalName": "Glamourer",
"AssemblyVersion": "0.0.5.4",
"TestingAssemblyVersion": "0.0.5.4",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 4,
"IsHide": "False",
"IsTestingExclusive": "false",
"DownloadCount": 1,
"LastUpdate": 1618608322,
"DownloadLinkInstall": "https://github.com/Ottermandias/Glamourer/raw/api4/Glamourer.zip",
"DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/raw/api4/Glamourer.zip"
}
]