Starting rework.

This commit is contained in:
Ottermandias 2022-07-15 13:09:38 +02:00
parent 0fc8992271
commit 7af38aa2ce
58 changed files with 8857 additions and 4923 deletions

3
.gitignore vendored
View file

@ -1,4 +1,5 @@
bin/
obj/
.vs/
Glamourer.json
Glamourer.json
private/

4
.gitmodules vendored
View file

@ -1,4 +0,0 @@
[submodule "OtterGui"]
path = OtterGui
url = git@github.com:Ottermandias/OtterGui.git
branch = main

View file

@ -0,0 +1,159 @@
using System;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
namespace Glamourer;
public readonly unsafe struct CharacterCustomization
{
public static readonly CharacterCustomization Null = new(null);
private readonly CustomizationData* _data;
public IntPtr Address
=> (IntPtr)_data;
public CharacterCustomization(CustomizationData* data)
=> _data = data;
public ref Race Race
=> ref _data->Race;
public ref SubRace Clan
=> ref _data->Clan;
public Gender Gender
{
get => _data->Gender;
set => _data->Gender = value;
}
public ref byte BodyType
=> ref _data->BodyType;
public ref byte Height
=> ref _data->Height;
public ref byte Face
=> ref _data->Face;
public ref byte Hairstyle
=> ref _data->Hairstyle;
public bool HighlightsOn
{
get => _data->HighlightsOn;
set => _data->HighlightsOn = value;
}
public ref byte SkinColor
=> ref _data->SkinColor;
public ref byte EyeColorRight
=> ref _data->EyeColorRight;
public ref byte HairColor
=> ref _data->HairColor;
public ref byte HighlightsColor
=> ref _data->HighlightsColor;
public ref byte FacialFeatures
=> ref _data->FacialFeatures;
public ref byte TattooColor
=> ref _data->TattooColor;
public ref byte Eyebrow
=> ref _data->Eyebrow;
public ref byte EyeColorLeft
=> ref _data->EyeColorLeft;
public byte EyeShape
{
get => _data->EyeShape;
set => _data->EyeShape = value;
}
public byte FacePaint
{
get => _data->FacePaint;
set => _data->FacePaint = value;
}
public bool FacePaintReversed
{
get => _data->FacePaintReversed;
set => _data->FacePaintReversed = value;
}
public byte Mouth
{
get => _data->Mouth;
set => _data->Mouth = value;
}
public bool SmallIris
{
get => _data->SmallIris;
set => _data->SmallIris = value;
}
public bool Lipstick
{
get => _data->Lipstick;
set => _data->Lipstick = value;
}
public ref byte Nose
=> ref _data->Nose;
public ref byte Jaw
=> ref _data->Jaw;
public ref byte LipColor
=> ref _data->LipColor;
public ref byte MuscleMass
=> ref _data->MuscleMass;
public ref byte TailShape
=> ref _data->TailShape;
public ref byte BustSize
=> ref _data->BustSize;
public ref byte FacePaintColor
=> ref _data->FacePaintColor;
public bool FacialFeature(int idx)
=> _data->FacialFeature(idx);
public void FacialFeature(int idx, bool set)
=> _data->FacialFeature(idx, set);
public byte this[CustomizationId id]
{
get => _data->Get(id);
set => _data->Set(id, value);
}
public static implicit operator CharacterCustomization(CustomizationData* val)
=> new(val);
public static implicit operator CharacterCustomization(IntPtr val)
=> new((CustomizationData*)val);
public static implicit operator bool(CharacterCustomization customize)
=> customize._data != null;
public static bool operator true(CharacterCustomization customize)
=> customize._data != null;
public static bool operator false(CharacterCustomization customize)
=> customize._data == null;
public static bool operator !(CharacterCustomization customize)
=> customize._data == null;
}

View file

@ -0,0 +1,105 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer;
public readonly unsafe struct CharacterEquip
{
public static readonly CharacterEquip Null = new(null);
private readonly CharacterArmor* _armor;
public IntPtr Address
=> (IntPtr)_armor;
public ref CharacterArmor this[int idx]
=> ref _armor[idx];
public ref CharacterArmor this[uint idx]
=> ref _armor[idx];
public ref CharacterArmor this[EquipSlot slot]
=> ref _armor[IndexOf(slot)];
public ref CharacterArmor Head
=> ref _armor[0];
public ref CharacterArmor Body
=> ref _armor[1];
public ref CharacterArmor Hands
=> ref _armor[2];
public ref CharacterArmor Legs
=> ref _armor[3];
public ref CharacterArmor Feet
=> ref _armor[4];
public ref CharacterArmor Ears
=> ref _armor[5];
public ref CharacterArmor Neck
=> ref _armor[6];
public ref CharacterArmor Wrists
=> ref _armor[7];
public ref CharacterArmor RFinger
=> ref _armor[8];
public ref CharacterArmor LFinger
=> ref _armor[9];
public CharacterEquip(CharacterArmor* val)
=> _armor = val;
public static implicit operator CharacterEquip(CharacterArmor* val)
=> new(val);
public static implicit operator CharacterEquip(IntPtr val)
=> new((CharacterArmor*)val);
public static implicit operator CharacterEquip(ReadOnlySpan<CharacterArmor> val)
{
if (val.Length != 10)
throw new ArgumentException("Invalid number of equipment pieces in span.");
fixed (CharacterArmor* ptr = val)
{
return new CharacterEquip(ptr);
}
}
public static implicit operator bool(CharacterEquip equip)
=> equip._armor != null;
public static bool operator true(CharacterEquip equip)
=> equip._armor != null;
public static bool operator false(CharacterEquip equip)
=> equip._armor == null;
public static bool operator !(CharacterEquip equip)
=> equip._armor == null;
private static int IndexOf(EquipSlot slot)
{
return slot switch
{
EquipSlot.Head => 0,
EquipSlot.Body => 1,
EquipSlot.Hands => 2,
EquipSlot.Legs => 3,
EquipSlot.Feet => 4,
EquipSlot.Ears => 5,
EquipSlot.Neck => 6,
EquipSlot.Wrists => 7,
EquipSlot.RFinger => 8,
EquipSlot.LFinger => 9,
_ => throw new ArgumentOutOfRangeException(nameof(slot), slot, null),
};
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;

View file

@ -4,6 +4,7 @@ using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Customization;
// A custom version of CharaMakeParams that is easier to parse.
[Sheet("CharaMakeParams")]
public class CharaMakeParams : ExcelRow
{
@ -43,10 +44,9 @@ public class CharaMakeParams : ExcelRow
public uint[] Icons;
}
public LazyRow<Race> Race { get; set; } = null!;
public LazyRow<Tribe> Tribe { get; set; } = null!;
public sbyte Gender { get; set; }
public LazyRow<Race> Race { get; set; } = null!;
public LazyRow<Tribe> Tribe { get; set; } = null!;
public sbyte Gender { get; set; }
public Menu[] Menus { get; set; } = new Menu[NumMenus];
public byte[] Voices { get; set; } = new byte[NumVoices];
@ -60,15 +60,17 @@ public class CharaMakeParams : ExcelRow
Race = new LazyRow<Race>(gameData, parser.ReadColumn<uint>(0), language);
Tribe = new LazyRow<Tribe>(gameData, parser.ReadColumn<uint>(1), language);
Gender = parser.ReadColumn<sbyte>(2);
var currentOffset = 0;
for (var i = 0; i < NumMenus; ++i)
{
Menus[i].Id = parser.ReadColumn<uint>(3 + 0 * NumMenus + i);
Menus[i].InitVal = parser.ReadColumn<byte>(3 + 1 * NumMenus + i);
Menus[i].Type = (MenuType)parser.ReadColumn<byte>(3 + 2 * NumMenus + i);
Menus[i].Size = parser.ReadColumn<byte>(3 + 3 * NumMenus + i);
Menus[i].LookAt = parser.ReadColumn<byte>(3 + 4 * NumMenus + i);
Menus[i].Mask = parser.ReadColumn<uint>(3 + 5 * NumMenus + i);
Menus[i].Customization = (CustomizationId)parser.ReadColumn<uint>(3 + 6 * NumMenus + i);
currentOffset = 3 + i;
Menus[i].Id = parser.ReadColumn<uint>(0 * NumMenus + currentOffset);
Menus[i].InitVal = parser.ReadColumn<byte>(1 * NumMenus + currentOffset);
Menus[i].Type = (MenuType)parser.ReadColumn<byte>(2 * NumMenus + currentOffset);
Menus[i].Size = parser.ReadColumn<byte>(3 * NumMenus + currentOffset);
Menus[i].LookAt = parser.ReadColumn<byte>(4 * NumMenus + currentOffset);
Menus[i].Mask = parser.ReadColumn<uint>(5 * NumMenus + currentOffset);
Menus[i].Customization = (CustomizationId)parser.ReadColumn<uint>(6 * NumMenus + currentOffset);
Menus[i].Values = new uint[Menus[i].Size];
switch (Menus[i].Type)
@ -78,47 +80,42 @@ public class CharaMakeParams : ExcelRow
case MenuType.Percentage:
break;
default:
currentOffset += 7 * NumMenus;
for (var j = 0; j < Menus[i].Size; ++j)
Menus[i].Values[j] = parser.ReadColumn<uint>(3 + (7 + j) * NumMenus + i);
Menus[i].Values[j] = parser.ReadColumn<uint>(j * NumMenus + currentOffset);
break;
}
Menus[i].Graphic = new byte[NumGraphics];
currentOffset = 3 + (MaxNumValues + 7) * NumMenus + i;
for (var j = 0; j < NumGraphics; ++j)
Menus[i].Graphic[j] = parser.ReadColumn<byte>(3 + (MaxNumValues + 7 + j) * NumMenus + i);
Menus[i].Graphic[j] = parser.ReadColumn<byte>(j * NumMenus + currentOffset);
}
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus;
for (var i = 0; i < NumVoices; ++i)
Voices[i] = parser.ReadColumn<byte>(3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + i);
Voices[i] = parser.ReadColumn<byte>(currentOffset++);
for (var i = 0; i < NumFaces; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + i;
FacialFeatureByFace[i].Icons = new uint[NumFeatures];
for (var j = 0; j < NumFeatures; ++j)
{
FacialFeatureByFace[i].Icons[j] =
(uint)parser.ReadColumn<int>(3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + j * NumFaces + i);
}
FacialFeatureByFace[i].Icons[j] = (uint)parser.ReadColumn<int>(j * NumFaces + currentOffset);
}
for (var i = 0; i < NumEquip; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7;
Equip[i] = new CharaMakeType.UnkData3347Obj()
{
Helmet = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 0),
Top = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 1),
Gloves = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 2),
Legs = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 3),
Shoes = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 4),
Weapon = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 5),
SubWeapon = parser.ReadColumn<ulong>(
3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7 + 6),
Helmet = parser.ReadColumn<ulong>(currentOffset + 0),
Top = parser.ReadColumn<ulong>(currentOffset + 1),
Gloves = parser.ReadColumn<ulong>(currentOffset + 2),
Legs = parser.ReadColumn<ulong>(currentOffset + 3),
Shoes = parser.ReadColumn<ulong>(currentOffset + 4),
Weapon = parser.ReadColumn<ulong>(currentOffset + 5),
SubWeapon = parser.ReadColumn<ulong>(currentOffset + 6),
};
}
}

View file

@ -1,23 +1,46 @@
using Dalamud.Data;
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Dalamud.Logging;
namespace Glamourer;
namespace Glamourer.Customization;
public class CmpFile
// Convert the Human.Cmp file into color sets.
// If the file can not be read due to TexTools corruption, create a 0-array of size MinSize.
internal class CmpFile
{
public readonly Lumina.Data.FileResource File;
public readonly uint[] RgbaColors;
private readonly Lumina.Data.FileResource? _file;
private readonly uint[] _rgbaColors;
// No error checking since only called internally.
public IEnumerable<uint> GetSlice(int offset, int count)
=> _rgbaColors.Length >= offset + count ? _rgbaColors.Skip(offset).Take(count) : Enumerable.Repeat(0u, count);
public bool Valid
=> _file != null;
public CmpFile(DataManager gameData)
{
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)
try
{
RgbaColors[i >> 2] = File.Data[i]
| (uint)(File.Data[i + 1] << 8)
| (uint)(File.Data[i + 2] << 16)
| (uint)(File.Data[i + 3] << 24);
_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)
{
_rgbaColors[i >> 2] = _file.Data[i]
| (uint)(_file.Data[i + 1] << 8)
| (uint)(_file.Data[i + 2] << 16)
| (uint)(_file.Data[i + 3] << 24);
}
}
catch (Exception e)
{
PluginLog.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n"
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
_file = null;
_rgbaColors = Array.Empty<uint>();
}
}
}

View file

@ -1,47 +1,45 @@
namespace Glamourer.Customization
{
public enum CustomName
{
Clan = 0,
Gender,
Reverse,
OddEyes,
IrisSmall,
IrisLarge,
IrisSize,
MidlanderM,
HighlanderM,
WildwoodM,
DuskwightM,
PlainsfolkM,
DunesfolkM,
SeekerOfTheSunM,
KeeperOfTheMoonM,
SeawolfM,
HellsguardM,
RaenM,
XaelaM,
HelionM,
LostM,
RavaM,
VeenaM,
MidlanderF,
HighlanderF,
WildwoodF,
DuskwightF,
PlainsfolkF,
DunesfolkF,
SeekerOfTheSunF,
KeeperOfTheMoonF,
SeawolfF,
HellsguardF,
RaenF,
XaelaF,
HelionF,
LostF,
RavaF,
VeenaF,
namespace Glamourer.Customization;
Num,
}
// Localization from the game files directly.
public enum CustomName
{
Clan = 0,
Gender,
Reverse,
OddEyes,
IrisSmall,
IrisLarge,
IrisSize,
MidlanderM,
HighlanderM,
WildwoodM,
DuskwightM,
PlainsfolkM,
DunesfolkM,
SeekerOfTheSunM,
KeeperOfTheMoonM,
SeawolfM,
HellsguardM,
RaenM,
XaelaM,
HelionM,
LostM,
RavaM,
VeenaM,
MidlanderF,
HighlanderF,
WildwoodF,
DuskwightF,
PlainsfolkF,
DunesfolkF,
SeekerOfTheSunF,
KeeperOfTheMoonF,
SeawolfF,
HellsguardF,
RaenF,
XaelaF,
HelionF,
LostF,
RavaF,
VeenaF,
}

View file

@ -1,32 +1,33 @@
using System.Runtime.InteropServices;
namespace Glamourer.Customization
namespace Glamourer.Customization;
// Any customization value can be represented in 8 bytes by its ID,
// a byte value, an optional value-id and an optional icon or color.
[StructLayout(LayoutKind.Explicit)]
public readonly struct Customization
{
[StructLayout(LayoutKind.Explicit)]
public readonly struct Customization
[FieldOffset(0)]
public readonly CustomizationId Id;
[FieldOffset(1)]
public readonly byte Value;
[FieldOffset(2)]
public readonly ushort CustomizeId;
[FieldOffset(4)]
public readonly uint IconId;
[FieldOffset(4)]
public readonly uint Color;
public Customization(CustomizationId id, byte value, uint data = 0, ushort customizeId = 0)
{
[FieldOffset(0)]
public readonly CustomizationId Id;
[FieldOffset(1)]
public readonly byte Value;
[FieldOffset(2)]
public readonly ushort CustomizeId;
[FieldOffset(4)]
public readonly uint IconId;
[FieldOffset(4)]
public readonly uint Color;
public Customization(CustomizationId id, byte value, uint data = 0, ushort customizeId = 0)
{
Id = id;
Value = value;
IconId = data;
Color = data;
CustomizeId = customizeId;
}
Id = id;
Value = value;
IconId = data;
Color = data;
CustomizeId = customizeId;
}
}

View file

@ -2,59 +2,17 @@
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization;
public unsafe struct LazyCustomization
{
public CharacterCustomization* Address;
public LazyCustomization(IntPtr characterPtr)
=> Address = (CharacterCustomization*)(characterPtr + CharacterCustomization.CustomizationOffset);
public ref CharacterCustomization Value
=> ref *Address;
public LazyCustomization(CharacterCustomization data)
=> Address = &data;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct CharacterCustomization
public struct CustomizationData
{
public const int CustomizationOffset = 0x830;
public const int CustomizationBytes = 26;
public static CharacterCustomization Default = new()
{
Race = Race.Hyur,
Gender = Gender.Male,
BodyType = 1,
Height = 50,
Clan = SubRace.Midlander,
Face = 1,
Hairstyle = 1,
HighlightsOn = false,
SkinColor = 1,
EyeColorRight = 1,
HighlightsColor = 1,
FacialFeatures = 0,
TattooColor = 1,
Eyebrow = 1,
EyeColorLeft = 1,
EyeShape = 1,
Nose = 1,
Jaw = 1,
Mouth = 1,
LipColor = 1,
MuscleMass = 50,
TailShape = 1,
BustSize = 50,
FacePaint = 1,
FacePaintColor = 1,
};
public Race Race;
private byte _gender;
public byte BodyType;
@ -82,21 +40,25 @@ public struct CharacterCustomization
private byte _facePaint;
public byte FacePaintColor;
// Skip Unknown Gender
public Gender Gender
{
get => (Gender)(_gender + 1);
set => _gender = (byte)(value - 1);
}
// Single bit flag.
public bool HighlightsOn
{
get => (_highlightsOn & 128) == 128;
set => _highlightsOn = (byte)(value ? _highlightsOn | 128 : _highlightsOn & 127);
}
// Get status of specific facial feature 0-7.
public bool FacialFeature(int idx)
=> (FacialFeatures & (1 << idx)) != 0;
// Set value of specific facial feature 0-7.
public void FacialFeature(int idx, bool set)
{
if (set)
@ -105,66 +67,108 @@ public struct CharacterCustomization
FacialFeatures &= (byte)~(1 << idx);
}
// Lower 7 bits
public byte EyeShape
{
get => (byte)(_eyeShape & 127);
set => _eyeShape = (byte)((value & 127) | (_eyeShape & 128));
}
// Uppermost bit flag.
public bool SmallIris
{
get => (_eyeShape & 128) == 128;
set => _eyeShape = (byte)(value ? _eyeShape | 128 : _eyeShape & 127);
}
// Lower 7 bits.
public byte Mouth
{
get => (byte)(_mouth & 127);
set => _mouth = (byte)((value & 127) | (_mouth & 128));
}
// Uppermost bit flag.
public bool Lipstick
{
get => (_mouth & 128) == 128;
set => _mouth = (byte)(value ? _mouth | 128 : _mouth & 127);
}
// Lower 7 bits.
public byte FacePaint
{
get => (byte)(_facePaint & 127);
set => _facePaint = (byte)((value & 127) | (_facePaint & 128));
}
// Uppermost bit flag.
public bool FacePaintReversed
{
get => (_facePaint & 128) == 128;
set => _facePaint = (byte)(value ? _facePaint | 128 : _facePaint & 127);
}
public unsafe void Read(IntPtr customizeAddress)
public static CustomizationData Default = new()
{
fixed (Race* ptr = &Race)
Race = Race.Hyur,
Gender = Gender.Male,
BodyType = 1,
Height = 50,
Clan = SubRace.Midlander,
Face = 1,
Hairstyle = 1,
HighlightsOn = false,
SkinColor = 1,
EyeColorRight = 1,
HighlightsColor = 1,
FacialFeatures = 0,
TattooColor = 1,
Eyebrow = 1,
EyeColorLeft = 1,
EyeShape = 1,
Nose = 1,
Jaw = 1,
Mouth = 1,
LipColor = 1,
MuscleMass = 50,
TailShape = 1,
BustSize = 50,
FacePaint = 1,
FacePaintColor = 1,
};
public unsafe void Read(CustomizationData* customize)
{
fixed (CustomizationData* ptr = &this)
{
Buffer.MemoryCopy(customizeAddress.ToPointer(), ptr, CustomizationBytes, CustomizationBytes);
*ptr = *customize;
}
}
public unsafe void Read(Customization* customize)
=> Read((IntPtr)customize);
public unsafe void Read(IntPtr customizeAddress)
=> Read((CustomizationData*)customizeAddress);
public void Read(Character character)
=> Read(character.Address + CustomizationOffset);
public CharacterCustomization(Character character)
public unsafe void Read(Human* human)
=> Read((CustomizationData*)human->CustomizeData);
public CustomizationData(Character character)
: this()
{
Read(character.Address + CustomizationOffset);
}
public byte this[CustomizationId id]
public unsafe CustomizationData(Human* human)
: this()
{
get => id switch
Read(human);
}
public byte Get(CustomizationId id)
=> id switch
{
CustomizationId.Race => (byte)Race,
CustomizationId.Gender => (byte)Gender,
@ -194,100 +198,59 @@ public struct CharacterCustomization
CustomizationId.FacePaintColor => FacePaintColor,
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
};
set
public void Set(CustomizationId id, byte value)
{
switch (id)
{
switch (id)
{
case CustomizationId.Race:
Race = (Race)value;
break;
case CustomizationId.Gender:
Gender = (Gender)value;
break;
case CustomizationId.BodyType:
BodyType = value;
break;
case CustomizationId.Height:
Height = value;
break;
case CustomizationId.Clan:
Clan = (SubRace)value;
break;
case CustomizationId.Face:
Face = value;
break;
case CustomizationId.Hairstyle:
Hairstyle = value;
break;
case CustomizationId.HighlightsOnFlag:
HighlightsOn = (value & 128) == 128;
break;
case CustomizationId.SkinColor:
SkinColor = value;
break;
case CustomizationId.EyeColorR:
EyeColorRight = value;
break;
case CustomizationId.HairColor:
HairColor = value;
break;
case CustomizationId.HighlightColor:
HighlightsColor = value;
break;
case CustomizationId.FacialFeaturesTattoos:
FacialFeatures = value;
break;
case CustomizationId.TattooColor:
TattooColor = value;
break;
case CustomizationId.Eyebrows:
Eyebrow = value;
break;
case CustomizationId.EyeColorL:
EyeColorLeft = value;
break;
case CustomizationId.EyeShape:
EyeShape = value;
break;
case CustomizationId.Nose:
Nose = value;
break;
case CustomizationId.Jaw:
Jaw = value;
break;
case CustomizationId.Mouth:
Mouth = value;
break;
case CustomizationId.LipColor:
LipColor = value;
break;
case CustomizationId.MuscleToneOrTailEarLength:
MuscleMass = value;
break;
case CustomizationId.TailEarShape:
TailShape = value;
break;
case CustomizationId.BustSize:
BustSize = value;
break;
case CustomizationId.FacePaint:
FacePaint = value;
break;
case CustomizationId.FacePaintColor:
FacePaintColor = value;
break;
default: throw new ArgumentOutOfRangeException(nameof(id), id, null);
}
// @formatter:off
case CustomizationId.Race: Race = (Race)value; break;
case CustomizationId.Gender: Gender = (Gender)value; break;
case CustomizationId.BodyType: BodyType = value; break;
case CustomizationId.Height: Height = value; break;
case CustomizationId.Clan: Clan = (SubRace)value; break;
case CustomizationId.Face: Face = value; break;
case CustomizationId.Hairstyle: Hairstyle = value; break;
case CustomizationId.HighlightsOnFlag: HighlightsOn = (value & 128) == 128; break;
case CustomizationId.SkinColor: SkinColor = value; break;
case CustomizationId.EyeColorR: EyeColorRight = value; break;
case CustomizationId.HairColor: HairColor = value; break;
case CustomizationId.HighlightColor: HighlightsColor = value; break;
case CustomizationId.FacialFeaturesTattoos: FacialFeatures = value; break;
case CustomizationId.TattooColor: TattooColor = value; break;
case CustomizationId.Eyebrows: Eyebrow = value; break;
case CustomizationId.EyeColorL: EyeColorLeft = value; break;
case CustomizationId.EyeShape: EyeShape = value; break;
case CustomizationId.Nose: Nose = value; break;
case CustomizationId.Jaw: Jaw = value; break;
case CustomizationId.Mouth: Mouth = value; break;
case CustomizationId.LipColor: LipColor = value; break;
case CustomizationId.MuscleToneOrTailEarLength: MuscleMass = value; break;
case CustomizationId.TailEarShape: TailShape = value; break;
case CustomizationId.BustSize: BustSize = value; break;
case CustomizationId.FacePaint: FacePaint = value; break;
case CustomizationId.FacePaintColor: FacePaintColor = value; break;
default: throw new ArgumentOutOfRangeException(nameof(id), id, null);
// @formatter:on
}
}
public byte this[CustomizationId id]
{
get => Get(id);
set => Set(id, value);
}
public unsafe void Write(FFXIVClientStructs.FFXIV.Client.Game.Character.Character* character)
{
fixed (CustomizationData* ptr = &this)
{
Buffer.MemoryCopy(ptr, character->CustomizeData, CustomizationBytes, CustomizationBytes);
}
}
public unsafe void Write(IntPtr characterAddress)
{
fixed (Race* ptr = &Race)
{
Buffer.MemoryCopy(ptr, (byte*)characterAddress + CustomizationOffset, CustomizationBytes, CustomizationBytes);
}
}
=> Write((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)characterAddress);
public unsafe void WriteBytes(byte[] array, int offset = 0)
{

View file

@ -1,108 +1,107 @@
using System;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
namespace Glamourer.Customization;
public enum CustomizationId : byte
{
public enum CustomizationId : byte
{
Race = 0,
Gender = 1,
BodyType = 2,
Height = 3,
Clan = 4,
Face = 5,
Hairstyle = 6,
HighlightsOnFlag = 7,
SkinColor = 8,
EyeColorR = 9,
HairColor = 10,
HighlightColor = 11,
FacialFeaturesTattoos = 12, // Bitmask, 1-7 per face, 8 is 1.0 tattoo
TattooColor = 13,
Eyebrows = 14,
EyeColorL = 15,
EyeShape = 16, // Flag 128 for Small
Nose = 17,
Jaw = 18,
Mouth = 19, // Flag 128 for Lip Color set
LipColor = 20, // Flag 128 for Light instead of Dark
MuscleToneOrTailEarLength = 21,
TailEarShape = 22,
BustSize = 23,
FacePaint = 24,
FacePaintColor = 25, // Flag 128 for Light instead of Dark.
}
public static class CustomizationExtensions
{
public static string ToDefaultName(this CustomizationId customizationId)
=> customizationId switch
{
CustomizationId.Race => "Race",
CustomizationId.Gender => "Gender",
CustomizationId.BodyType => "Body Type",
CustomizationId.Height => "Height",
CustomizationId.Clan => "Clan",
CustomizationId.Face => "Head Style",
CustomizationId.Hairstyle => "Hair Style",
CustomizationId.HighlightsOnFlag => "Highlights",
CustomizationId.SkinColor => "Skin Color",
CustomizationId.EyeColorR => "Right Eye Color",
CustomizationId.HairColor => "Hair Color",
CustomizationId.HighlightColor => "Highlights Color",
CustomizationId.FacialFeaturesTattoos => "Facial Features",
CustomizationId.TattooColor => "Tattoo Color",
CustomizationId.Eyebrows => "Eyebrow Style",
CustomizationId.EyeColorL => "Left Eye Color",
CustomizationId.EyeShape => "Eye Shape",
CustomizationId.Nose => "Nose Style",
CustomizationId.Jaw => "Jaw Style",
CustomizationId.Mouth => "Mouth Style",
CustomizationId.MuscleToneOrTailEarLength => "Muscle Tone",
CustomizationId.TailEarShape => "Tail Shape",
CustomizationId.BustSize => "Bust Size",
CustomizationId.FacePaint => "Face Paint",
CustomizationId.FacePaintColor => "Face Paint Color",
CustomizationId.LipColor => "Lip Color",
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
public static CharaMakeParams.MenuType ToType(this CustomizationId customizationId, Race race = Race.Hyur)
=> customizationId switch
{
CustomizationId.Race => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Gender => CharaMakeParams.MenuType.IconSelector,
CustomizationId.BodyType => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Height => CharaMakeParams.MenuType.Percentage,
CustomizationId.Clan => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Face => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Hairstyle => CharaMakeParams.MenuType.IconSelector,
CustomizationId.HighlightsOnFlag => CharaMakeParams.MenuType.ListSelector,
CustomizationId.SkinColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.EyeColorR => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.HairColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.HighlightColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.FacialFeaturesTattoos => CharaMakeParams.MenuType.MultiIconSelector,
CustomizationId.TattooColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.Eyebrows => CharaMakeParams.MenuType.ListSelector,
CustomizationId.EyeColorL => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.EyeShape => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Nose => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Jaw => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Mouth => CharaMakeParams.MenuType.ListSelector,
CustomizationId.MuscleToneOrTailEarLength => CharaMakeParams.MenuType.Percentage,
CustomizationId.BustSize => CharaMakeParams.MenuType.Percentage,
CustomizationId.FacePaint => CharaMakeParams.MenuType.IconSelector,
CustomizationId.FacePaintColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.TailEarShape => race == Race.Elezen || race == Race.Lalafell
? CharaMakeParams.MenuType.ListSelector
: CharaMakeParams.MenuType.IconSelector,
CustomizationId.LipColor => race == Race.Hrothgar
? CharaMakeParams.MenuType.IconSelector
: CharaMakeParams.MenuType.ColorPicker,
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
}
Race = 0,
Gender = 1,
BodyType = 2,
Height = 3,
Clan = 4,
Face = 5,
Hairstyle = 6,
HighlightsOnFlag = 7,
SkinColor = 8,
EyeColorR = 9,
HairColor = 10,
HighlightColor = 11,
FacialFeaturesTattoos = 12, // Bitmask, 1-7 per face, 8 is 1.0 tattoo
TattooColor = 13,
Eyebrows = 14,
EyeColorL = 15,
EyeShape = 16, // Flag 128 for Small
Nose = 17,
Jaw = 18,
Mouth = 19, // Flag 128 for Lip Color set
LipColor = 20, // Flag 128 for Light instead of Dark
MuscleToneOrTailEarLength = 21,
TailEarShape = 22,
BustSize = 23,
FacePaint = 24,
FacePaintColor = 25, // Flag 128 for Light instead of Dark.
}
public static class CustomizationExtensions
{
public static string ToDefaultName(this CustomizationId customizationId)
=> customizationId switch
{
CustomizationId.Race => "Race",
CustomizationId.Gender => "Gender",
CustomizationId.BodyType => "Body Type",
CustomizationId.Height => "Height",
CustomizationId.Clan => "Clan",
CustomizationId.Face => "Head Style",
CustomizationId.Hairstyle => "Hair Style",
CustomizationId.HighlightsOnFlag => "Highlights",
CustomizationId.SkinColor => "Skin Color",
CustomizationId.EyeColorR => "Right Eye Color",
CustomizationId.HairColor => "Hair Color",
CustomizationId.HighlightColor => "Highlights Color",
CustomizationId.FacialFeaturesTattoos => "Facial Features",
CustomizationId.TattooColor => "Tattoo Color",
CustomizationId.Eyebrows => "Eyebrow Style",
CustomizationId.EyeColorL => "Left Eye Color",
CustomizationId.EyeShape => "Eye Shape",
CustomizationId.Nose => "Nose Style",
CustomizationId.Jaw => "Jaw Style",
CustomizationId.Mouth => "Mouth Style",
CustomizationId.MuscleToneOrTailEarLength => "Muscle Tone",
CustomizationId.TailEarShape => "Tail Shape",
CustomizationId.BustSize => "Bust Size",
CustomizationId.FacePaint => "Face Paint",
CustomizationId.FacePaintColor => "Face Paint Color",
CustomizationId.LipColor => "Lip Color",
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
public static CharaMakeParams.MenuType ToType(this CustomizationId customizationId, Race race = Race.Hyur)
=> customizationId switch
{
CustomizationId.Race => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Gender => CharaMakeParams.MenuType.IconSelector,
CustomizationId.BodyType => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Height => CharaMakeParams.MenuType.Percentage,
CustomizationId.Clan => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Face => CharaMakeParams.MenuType.IconSelector,
CustomizationId.Hairstyle => CharaMakeParams.MenuType.IconSelector,
CustomizationId.HighlightsOnFlag => CharaMakeParams.MenuType.ListSelector,
CustomizationId.SkinColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.EyeColorR => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.HairColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.HighlightColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.FacialFeaturesTattoos => CharaMakeParams.MenuType.MultiIconSelector,
CustomizationId.TattooColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.Eyebrows => CharaMakeParams.MenuType.ListSelector,
CustomizationId.EyeColorL => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.EyeShape => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Nose => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Jaw => CharaMakeParams.MenuType.ListSelector,
CustomizationId.Mouth => CharaMakeParams.MenuType.ListSelector,
CustomizationId.MuscleToneOrTailEarLength => CharaMakeParams.MenuType.Percentage,
CustomizationId.BustSize => CharaMakeParams.MenuType.Percentage,
CustomizationId.FacePaint => CharaMakeParams.MenuType.IconSelector,
CustomizationId.FacePaintColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.TailEarShape => race is Race.Elezen or Race.Lalafell
? CharaMakeParams.MenuType.ListSelector
: CharaMakeParams.MenuType.IconSelector,
CustomizationId.LipColor => race == Race.Hrothgar
? CharaMakeParams.MenuType.IconSelector
: CharaMakeParams.MenuType.ColorPicker,
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
}

View file

@ -5,7 +5,7 @@ using System.Reflection;
using Dalamud;
using Dalamud.Data;
using Dalamud.Plugin;
using Lumina.Data;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Classes;
@ -14,360 +14,407 @@ using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Customization;
// Generate everything about customization per tribe and gender.
public partial class CustomizationOptions
{
internal static readonly Race[] Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
// All races except for Unknown
internal static readonly Race[] Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
// All tribes except for Unknown
internal static readonly SubRace[] Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
// Two genders.
internal static readonly Gender[] Genders =
{
Gender.Male,
Gender.Female,
};
// Every tribe and gender has a separate set of available customizations.
internal CustomizationSet GetList(SubRace race, Gender gender)
=> _list[ToIndex(race, gender)];
=> _customizationSets[ToIndex(race, gender)];
// Get specific icons.
internal ImGuiScene.TextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id);
private static readonly int ListSize = Clans.Length * Genders.Length;
private readonly IconStorage _icons;
private readonly CustomizationSet[] _list = new CustomizationSet[ListSize];
private readonly IconStorage _icons;
private static readonly int ListSize = Clans.Length * Genders.Length;
private readonly CustomizationSet[] _customizationSets = new CustomizationSet[ListSize];
// Get the index for the given pair of tribe and gender.
private static int ToIndex(SubRace race, Gender gender)
{
var idx = ((int)race - 1) * Genders.Length + (gender == Gender.Female ? 1 : 0);
if (idx < 0 || idx >= ListSize)
ThrowException(race, gender);
return idx;
}
private static void ThrowException(SubRace race, Gender gender)
=> throw new Exception($"Invalid customization requested for {race} {gender}.");
}
private static int ToIndex(SubRace race, Gender gender)
{
if (race == SubRace.Unknown || gender != Gender.Female && gender != Gender.Male)
ThrowException(race, gender);
var ret = (int)race - 1;
ret = ret * Genders.Length + (gender == Gender.Female ? 1 : 0);
return ret;
}
private Customization[] GetHairStyles(SubRace race, Gender 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)
{
var name = $"Unknown{66 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
var hairRow = _customizeSheet.GetRow(customizeIdx);
hairList.Add(hairRow != null
? new Customization(CustomizationId.Hairstyle, hairRow.FeatureID, hairRow.Icon, (ushort)hairRow.RowId)
: new Customization(CustomizationId.Hairstyle, (byte)i, customizeIdx, 0));
}
return hairList.ToArray();
}
private Customization[] CreateColorPicker(CustomizationId id, int offset, int num, bool light = false)
=> _cmpFile.RgbaColors.Skip(offset).Take(num)
.Select((c, i) => new Customization(id, (byte)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))
.ToArray();
private (Customization[], Customization[]) GetColors(SubRace race, Gender gender)
{
if (race > SubRace.Veena || race == SubRace.Unknown)
throw new ArgumentOutOfRangeException(nameof(race), race, null);
var gv = gender == Gender.Male ? 0 : 1;
var idx = ((int)race * 2 + gv) * 5 + 3;
return (CreateColorPicker(CustomizationId.SkinColor, idx << 8, 192),
CreateColorPicker(CustomizationId.HairColor, (idx + 1) << 8, 192));
}
private Customization FromValueAndIndex(CustomizationId id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new Customization(id, (byte)(index + 1), value, 0)
: new Customization(id, row.FeatureID, row.Icon, (ushort)row.RowId);
}
private static int GetListSize(CharaMakeParams row, CustomizationId id)
{
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == id);
return menu?.Size ?? 0;
}
private Customization[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<Customization>(row.Unknown37);
for (var i = 0; i < row.Unknown37; ++i)
{
var name = $"Unknown{73 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
paintList.Add(paintRow != null
? new Customization(CustomizationId.FacePaint, paintRow.FeatureID, paintRow.Icon, (ushort)paintRow.RowId)
: new Customization(CustomizationId.FacePaint, (byte)i, customizeIdx, 0));
}
return paintList.ToArray();
}
private Customization[] GetTailEarShapes(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.TailEarShape)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.TailEarShape, v, i)).ToArray()
?? Array.Empty<Customization>();
private Customization[] GetFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.Face)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.Face, v, i)).ToArray()
?? Array.Empty<Customization>();
private Customization[] HrothgarFurPattern(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.LipColor)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.LipColor, v, i)).ToArray()
?? Array.Empty<Customization>();
private Customization[] HrothgarFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.Hairstyle)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.Hairstyle, v, i)).ToArray()
?? Array.Empty<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 set = new CustomizationSet(race, gender)
{
HairStyles = GetHairStyles(race, gender),
HairColors = hair,
SkinColors = skin,
EyeColors = _eyeColorPicker,
HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker,
LipColorsDark = race.ToRace() == Race.Hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = race.ToRace() == Race.Hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight,
FacePaintColorsDark = _facePaintColorPickerDark,
FacePaintColorsLight = _facePaintColorPickerLight,
Faces = GetFaces(row),
NumEyebrows = GetListSize(row, CustomizationId.Eyebrows),
NumEyeShapes = GetListSize(row, CustomizationId.EyeShape),
NumNoseShapes = GetListSize(row, CustomizationId.Nose),
NumJawShapes = GetListSize(row, CustomizationId.Jaw),
NumMouthShapes = GetListSize(row, CustomizationId.Mouth),
FacePaints = GetFacePaints(race, gender),
TailEarShapes = GetTailEarShapes(row),
};
if (GetListSize(row, CustomizationId.BustSize) > 0)
set.SetAvailable(CustomizationId.BustSize);
if (GetListSize(row, CustomizationId.MuscleToneOrTailEarLength) > 0)
set.SetAvailable(CustomizationId.MuscleToneOrTailEarLength);
if (set.NumEyebrows > 0)
set.SetAvailable(CustomizationId.Eyebrows);
if (set.NumEyeShapes > 0)
set.SetAvailable(CustomizationId.EyeShape);
if (set.NumNoseShapes > 0)
set.SetAvailable(CustomizationId.Nose);
if (set.NumJawShapes > 0)
set.SetAvailable(CustomizationId.Jaw);
if (set.NumMouthShapes > 0)
set.SetAvailable(CustomizationId.Mouth);
if (set.FacePaints.Count > 0)
{
set.SetAvailable(CustomizationId.FacePaint);
set.SetAvailable(CustomizationId.FacePaintColor);
}
if (set.TailEarShapes.Count > 0)
set.SetAvailable(CustomizationId.TailEarShape);
if (set.Faces.Count > 0)
set.SetAvailable(CustomizationId.Face);
var count = set.Faces.Count;
var featureDict = new List<IReadOnlyList<Customization>>(count);
for (var i = 0; i < count; ++i)
{
featureDict.Add(row.FacialFeatureByFace[i].Icons.Select((val, idx)
=> new Customization(CustomizationId.FacialFeaturesTattoos, (byte)(1 << idx), val, (ushort)(i * 8 + idx)))
.Append(new Customization(CustomizationId.FacialFeaturesTattoos, 1 << 7, 137905, (ushort)((i + 1) * 8)))
.ToArray());
}
set.FeaturesTattoos = featureDict;
var nameArray = ((CustomizationId[])Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customization == c);
if (menu == null)
{
if (c == CustomizationId.HighlightsOnFlag)
return _lobby.GetRow(237)?.Text.ToString() ?? "Highlights";
return c.ToDefaultName();
}
if (c == CustomizationId.FacialFeaturesTattoos)
return
$"{_lobby.GetRow(1741)?.Text.ToString() ?? "Facial Features"} & {_lobby.GetRow(1742)?.Text.ToString() ?? "Tattoos"}";
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 =>
{
switch (c)
{
case CustomizationId.HighlightColor:
case CustomizationId.EyeColorL:
case CustomizationId.EyeColorR:
return CharaMakeParams.MenuType.ColorPicker;
}
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customization == c);
return menu?.Type ?? CharaMakeParams.MenuType.ListSelector;
}).ToArray();
return set;
}
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
private readonly ExcelSheet<CharaMakeParams> _listSheet;
private readonly ExcelSheet<HairMakeType> _hairSheet;
private readonly ExcelSheet<Lobby> _lobby;
private readonly CmpFile _cmpFile;
private readonly Customization[] _highlightPicker;
private readonly Customization[] _eyeColorPicker;
private readonly Customization[] _facePaintColorPickerDark;
private readonly Customization[] _facePaintColorPickerLight;
private readonly Customization[] _lipColorPickerDark;
private readonly Customization[] _lipColorPickerLight;
private readonly Customization[] _tattooColorPicker;
private readonly string[] _names = new string[(int)CustomName.Num];
public partial class CustomizationOptions
{
internal readonly bool Valid;
public string GetName(CustomName name)
=> _names[(int)name];
private static Language FromClientLanguage(ClientLanguage language)
=> language switch
{
ClientLanguage.English => Language.English,
ClientLanguage.French => Language.French,
ClientLanguage.German => Language.German,
ClientLanguage.Japanese => Language.Japanese,
_ => Language.English,
};
internal CustomizationOptions(DalamudPluginInterface pi, DataManager gameData, ClientLanguage language)
{
try
{
_cmpFile = new CmpFile(gameData);
}
catch (Exception e)
{
throw new Exception("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n"
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
}
_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(language),
null,
}) as ExcelSheet<CharaMakeParams>;
_listSheet = tmp!;
_hairSheet = gameData.GetExcelSheet<HairMakeType>()!;
SetNames(gameData);
_highlightPicker = CreateColorPicker(CustomizationId.HighlightColor, 256, 192);
_lipColorPickerDark = CreateColorPicker(CustomizationId.LipColor, 512, 96);
_lipColorPickerLight = CreateColorPicker(CustomizationId.LipColor, 1024, 96, true);
_eyeColorPicker = CreateColorPicker(CustomizationId.EyeColorL, 0, 192);
_facePaintColorPickerDark = CreateColorPicker(CustomizationId.FacePaintColor, 640, 96);
_facePaintColorPickerLight = CreateColorPicker(CustomizationId.FacePaintColor, 1152, 96, true);
_tattooColorPicker = CreateColorPicker(CustomizationId.TattooColor, 0, 192);
_icons = new IconStorage(pi, gameData, _list.Length * 50);
var tmp = new TemporaryData(gameData, this, language);
_icons = new IconStorage(pi, gameData, _customizationSets.Length * 50);
Valid = tmp.Valid;
SetNames(gameData, tmp);
foreach (var race in Clans)
{
foreach (var gender in Genders)
_list[ToIndex(race, gender)] = GetSet(race, gender);
_customizationSets[ToIndex(race, gender)] = tmp.GetSet(race, gender);
}
}
private void SetNames(DataManager gameData)
// Obtain localized names of customization options and race names from the game data.
private readonly string[] _names = new string[Enum.GetValues<CustomName>().Length];
private void SetNames(DataManager gameData, TemporaryData tmp)
{
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] =
subRace.GetRow((int)SubRace.Highlander)?.Masculine.ToString() ?? SubRace.Highlander.ToName();
_names[(int)CustomName.HighlanderF] = subRace.GetRow((int)SubRace.Highlander)?.Feminine.ToString() ?? SubRace.Highlander.ToName();
_names[(int)CustomName.WildwoodM] = subRace.GetRow((int)SubRace.Wildwood)?.Masculine.ToString() ?? SubRace.Wildwood.ToName();
_names[(int)CustomName.WildwoodF] = subRace.GetRow((int)SubRace.Wildwood)?.Feminine.ToString() ?? SubRace.Wildwood.ToName();
_names[(int)CustomName.DuskwightM] = subRace.GetRow((int)SubRace.Duskwight)?.Masculine.ToString() ?? SubRace.Duskwight.ToName();
_names[(int)CustomName.DuskwightF] = subRace.GetRow((int)SubRace.Duskwight)?.Feminine.ToString() ?? SubRace.Duskwight.ToName();
_names[(int)CustomName.PlainsfolkM] =
subRace.GetRow((int)SubRace.Plainsfolk)?.Masculine.ToString() ?? SubRace.Plainsfolk.ToName();
_names[(int)CustomName.PlainsfolkF] = subRace.GetRow((int)SubRace.Plainsfolk)?.Feminine.ToString() ?? SubRace.Plainsfolk.ToName();
_names[(int)CustomName.DunesfolkM] = subRace.GetRow((int)SubRace.Dunesfolk)?.Masculine.ToString() ?? SubRace.Dunesfolk.ToName();
_names[(int)CustomName.DunesfolkF] = subRace.GetRow((int)SubRace.Dunesfolk)?.Feminine.ToString() ?? SubRace.Dunesfolk.ToName();
_names[(int)CustomName.SeekerOfTheSunM] =
subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Masculine.ToString() ?? SubRace.SeekerOfTheSun.ToName();
_names[(int)CustomName.SeekerOfTheSunF] =
subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Feminine.ToString() ?? SubRace.SeekerOfTheSun.ToName();
_names[(int)CustomName.KeeperOfTheMoonM] =
subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Masculine.ToString() ?? SubRace.KeeperOfTheMoon.ToName();
_names[(int)CustomName.KeeperOfTheMoonF] =
subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Feminine.ToString() ?? SubRace.KeeperOfTheMoon.ToName();
_names[(int)CustomName.SeawolfM] = subRace.GetRow((int)SubRace.Seawolf)?.Masculine.ToString() ?? SubRace.Seawolf.ToName();
_names[(int)CustomName.SeawolfF] = subRace.GetRow((int)SubRace.Seawolf)?.Feminine.ToString() ?? SubRace.Seawolf.ToName();
_names[(int)CustomName.HellsguardM] =
subRace.GetRow((int)SubRace.Hellsguard)?.Masculine.ToString() ?? SubRace.Hellsguard.ToName();
_names[(int)CustomName.HellsguardF] = subRace.GetRow((int)SubRace.Hellsguard)?.Feminine.ToString() ?? SubRace.Hellsguard.ToName();
_names[(int)CustomName.RaenM] = subRace.GetRow((int)SubRace.Raen)?.Masculine.ToString() ?? SubRace.Raen.ToName();
_names[(int)CustomName.RaenF] = subRace.GetRow((int)SubRace.Raen)?.Feminine.ToString() ?? SubRace.Raen.ToName();
_names[(int)CustomName.XaelaM] = subRace.GetRow((int)SubRace.Xaela)?.Masculine.ToString() ?? SubRace.Xaela.ToName();
_names[(int)CustomName.XaelaF] = subRace.GetRow((int)SubRace.Xaela)?.Feminine.ToString() ?? SubRace.Xaela.ToName();
_names[(int)CustomName.HelionM] = subRace.GetRow((int)SubRace.Helion)?.Masculine.ToString() ?? SubRace.Helion.ToName();
_names[(int)CustomName.HelionF] = subRace.GetRow((int)SubRace.Helion)?.Feminine.ToString() ?? SubRace.Helion.ToName();
_names[(int)CustomName.LostM] = subRace.GetRow((int)SubRace.Lost)?.Masculine.ToString() ?? SubRace.Lost.ToName();
_names[(int)CustomName.LostF] = subRace.GetRow((int)SubRace.Lost)?.Feminine.ToString() ?? SubRace.Lost.ToName();
_names[(int)CustomName.RavaM] = subRace.GetRow((int)SubRace.Rava)?.Masculine.ToString() ?? SubRace.Rava.ToName();
_names[(int)CustomName.RavaF] = subRace.GetRow((int)SubRace.Rava)?.Feminine.ToString() ?? SubRace.Rava.ToName();
_names[(int)CustomName.VeenaM] = subRace.GetRow((int)SubRace.Veena)?.Masculine.ToString() ?? SubRace.Veena.ToName();
_names[(int)CustomName.VeenaF] = subRace.GetRow((int)SubRace.Veena)?.Feminine.ToString() ?? SubRace.Veena.ToName();
void Set(CustomName id, Lumina.Text.SeString? s, string def)
=> _names[(int)id] = s?.ToDalamudString().TextValue ?? def;
Set(CustomName.Clan, tmp.Lobby.GetRow(102)?.Text, "Clan");
Set(CustomName.Gender, tmp.Lobby.GetRow(103)?.Text, "Gender");
Set(CustomName.Reverse, tmp.Lobby.GetRow(2135)?.Text, "Reverse");
Set(CustomName.OddEyes, tmp.Lobby.GetRow(2125)?.Text, "Odd Eyes");
Set(CustomName.IrisSmall, tmp.Lobby.GetRow(1076)?.Text, "Small");
Set(CustomName.IrisLarge, tmp.Lobby.GetRow(1075)?.Text, "Large");
Set(CustomName.IrisSize, tmp.Lobby.GetRow(244)?.Text, "Iris Size");
Set(CustomName.MidlanderM, subRace.GetRow((int)SubRace.Midlander)?.Masculine, SubRace.Midlander.ToName());
Set(CustomName.MidlanderF, subRace.GetRow((int)SubRace.Midlander)?.Feminine, SubRace.Midlander.ToName());
Set(CustomName.HighlanderM, subRace.GetRow((int)SubRace.Highlander)?.Masculine, SubRace.Highlander.ToName());
Set(CustomName.HighlanderF, subRace.GetRow((int)SubRace.Highlander)?.Feminine, SubRace.Highlander.ToName());
Set(CustomName.WildwoodM, subRace.GetRow((int)SubRace.Wildwood)?.Masculine, SubRace.Wildwood.ToName());
Set(CustomName.WildwoodF, subRace.GetRow((int)SubRace.Wildwood)?.Feminine, SubRace.Wildwood.ToName());
Set(CustomName.DuskwightM, subRace.GetRow((int)SubRace.Duskwight)?.Masculine, SubRace.Duskwight.ToName());
Set(CustomName.DuskwightF, subRace.GetRow((int)SubRace.Duskwight)?.Feminine, SubRace.Duskwight.ToName());
Set(CustomName.PlainsfolkM, subRace.GetRow((int)SubRace.Plainsfolk)?.Masculine, SubRace.Plainsfolk.ToName());
Set(CustomName.PlainsfolkF, subRace.GetRow((int)SubRace.Plainsfolk)?.Feminine, SubRace.Plainsfolk.ToName());
Set(CustomName.DunesfolkM, subRace.GetRow((int)SubRace.Dunesfolk)?.Masculine, SubRace.Dunesfolk.ToName());
Set(CustomName.DunesfolkF, subRace.GetRow((int)SubRace.Dunesfolk)?.Feminine, SubRace.Dunesfolk.ToName());
Set(CustomName.SeekerOfTheSunM, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Masculine, SubRace.SeekerOfTheSun.ToName());
Set(CustomName.SeekerOfTheSunF, subRace.GetRow((int)SubRace.SeekerOfTheSun)?.Feminine, SubRace.SeekerOfTheSun.ToName());
Set(CustomName.KeeperOfTheMoonM, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Masculine, SubRace.KeeperOfTheMoon.ToName());
Set(CustomName.KeeperOfTheMoonF, subRace.GetRow((int)SubRace.KeeperOfTheMoon)?.Feminine, SubRace.KeeperOfTheMoon.ToName());
Set(CustomName.SeawolfM, subRace.GetRow((int)SubRace.Seawolf)?.Masculine, SubRace.Seawolf.ToName());
Set(CustomName.SeawolfF, subRace.GetRow((int)SubRace.Seawolf)?.Feminine, SubRace.Seawolf.ToName());
Set(CustomName.HellsguardM, subRace.GetRow((int)SubRace.Hellsguard)?.Masculine, SubRace.Hellsguard.ToName());
Set(CustomName.HellsguardF, subRace.GetRow((int)SubRace.Hellsguard)?.Feminine, SubRace.Hellsguard.ToName());
Set(CustomName.RaenM, subRace.GetRow((int)SubRace.Raen)?.Masculine, SubRace.Raen.ToName());
Set(CustomName.RaenF, subRace.GetRow((int)SubRace.Raen)?.Feminine, SubRace.Raen.ToName());
Set(CustomName.XaelaM, subRace.GetRow((int)SubRace.Xaela)?.Masculine, SubRace.Xaela.ToName());
Set(CustomName.XaelaF, subRace.GetRow((int)SubRace.Xaela)?.Feminine, SubRace.Xaela.ToName());
Set(CustomName.HelionM, subRace.GetRow((int)SubRace.Helion)?.Masculine, SubRace.Helion.ToName());
Set(CustomName.HelionF, subRace.GetRow((int)SubRace.Helion)?.Feminine, SubRace.Helion.ToName());
Set(CustomName.LostM, subRace.GetRow((int)SubRace.Lost)?.Masculine, SubRace.Lost.ToName());
Set(CustomName.LostF, subRace.GetRow((int)SubRace.Lost)?.Feminine, SubRace.Lost.ToName());
Set(CustomName.RavaM, subRace.GetRow((int)SubRace.Rava)?.Masculine, SubRace.Rava.ToName());
Set(CustomName.RavaF, subRace.GetRow((int)SubRace.Rava)?.Feminine, SubRace.Rava.ToName());
Set(CustomName.VeenaM, subRace.GetRow((int)SubRace.Veena)?.Masculine, SubRace.Veena.ToName());
Set(CustomName.VeenaF, subRace.GetRow((int)SubRace.Veena)?.Feminine, SubRace.Veena.ToName());
}
private class TemporaryData
{
public bool Valid
=> _cmpFile.Valid;
public CustomizationSet GetSet(SubRace race, Gender gender)
{
var (skin, hair) = GetColors(race, gender);
var row = _listSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
// Create the initial set with all the easily accessible parameters available for anyone.
var set = new CustomizationSet(race, gender)
{
HairStyles = GetHairStyles(race, gender),
HairColors = hair,
SkinColors = skin,
EyeColors = _eyeColorPicker,
HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker,
LipColorsDark = race.ToRace() == Race.Hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = race.ToRace() == Race.Hrothgar ? Array.Empty<Customization>() : _lipColorPickerLight,
FacePaintColorsDark = _facePaintColorPickerDark,
FacePaintColorsLight = _facePaintColorPickerLight,
Faces = GetFaces(row),
NumEyebrows = GetListSize(row, CustomizationId.Eyebrows),
NumEyeShapes = GetListSize(row, CustomizationId.EyeShape),
NumNoseShapes = GetListSize(row, CustomizationId.Nose),
NumJawShapes = GetListSize(row, CustomizationId.Jaw),
NumMouthShapes = GetListSize(row, CustomizationId.Mouth),
FacePaints = GetFacePaints(race, gender),
TailEarShapes = GetTailEarShapes(row),
};
SetAvailability(set, row);
SetFacialFeatures(set, row);
SetMenuTypes(set, row);
SetNames(set, row);
return set;
}
public TemporaryData(DataManager gameData, CustomizationOptions options, ClientLanguage language)
{
_options = options;
_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",
language.ToLumina(),
null,
}) as ExcelSheet<CharaMakeParams>;
_listSheet = tmp!;
_hairSheet = gameData.GetExcelSheet<HairMakeType>()!;
_highlightPicker = CreateColorPicker(CustomizationId.HighlightColor, 256, 192);
_lipColorPickerDark = CreateColorPicker(CustomizationId.LipColor, 512, 96);
_lipColorPickerLight = CreateColorPicker(CustomizationId.LipColor, 1024, 96, true);
_eyeColorPicker = CreateColorPicker(CustomizationId.EyeColorL, 0, 192);
_facePaintColorPickerDark = CreateColorPicker(CustomizationId.FacePaintColor, 640, 96);
_facePaintColorPickerLight = CreateColorPicker(CustomizationId.FacePaintColor, 1152, 96, true);
_tattooColorPicker = CreateColorPicker(CustomizationId.TattooColor, 0, 192);
}
// Required sheets.
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
private readonly ExcelSheet<CharaMakeParams> _listSheet;
private readonly ExcelSheet<HairMakeType> _hairSheet;
public readonly ExcelSheet<Lobby> Lobby;
private readonly CmpFile _cmpFile;
// Those values are shared between all races.
private readonly Customization[] _highlightPicker;
private readonly Customization[] _eyeColorPicker;
private readonly Customization[] _facePaintColorPickerDark;
private readonly Customization[] _facePaintColorPickerLight;
private readonly Customization[] _lipColorPickerDark;
private readonly Customization[] _lipColorPickerLight;
private readonly Customization[] _tattooColorPicker;
private readonly CustomizationOptions _options;
private Customization[] CreateColorPicker(CustomizationId id, int offset, int num, bool light = false)
=> _cmpFile.GetSlice(offset, num)
.Select((c, i) => new Customization(id, (byte)(light ? 128 + i : 0 + i), c, (ushort)(offset + i)))
.ToArray();
private static void SetMenuTypes(CustomizationSet set, CharaMakeParams row)
{
// Set up the menu types for all customizations.
set.Types = ((CustomizationId[])Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
// Those types are not correctly given in the menu, so special case them to color pickers.
switch (c)
{
case CustomizationId.HighlightColor:
case CustomizationId.EyeColorL:
case CustomizationId.EyeColorR:
return CharaMakeParams.MenuType.ColorPicker;
}
// Otherwise find the first menu corresponding to the id.
// If there is none, assume a list.
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customization == c);
return menu?.Type ?? CharaMakeParams.MenuType.ListSelector;
}).ToArray();
set.Order = CustomizationSet.ComputeOrder(set);
}
// Set customizations available if they have any options.
private static void SetAvailability(CustomizationSet set, CharaMakeParams row)
{
void Set(bool available, CustomizationId flag)
{
if (available)
set.SetAvailable(flag);
}
// Both are percentages that are either unavailable or 0-100.
Set(GetListSize(row, CustomizationId.BustSize) > 0, CustomizationId.BustSize);
Set(GetListSize(row, CustomizationId.MuscleToneOrTailEarLength) > 0, CustomizationId.MuscleToneOrTailEarLength);
Set(set.NumEyebrows > 0, CustomizationId.Eyebrows);
Set(set.NumEyeShapes > 0, CustomizationId.EyeShape);
Set(set.NumNoseShapes > 0, CustomizationId.Nose);
Set(set.NumJawShapes > 0, CustomizationId.Jaw);
Set(set.NumMouthShapes > 0, CustomizationId.Mouth);
Set(set.TailEarShapes.Count > 0, CustomizationId.TailEarShape);
Set(set.Faces.Count > 0, CustomizationId.Face);
Set(set.FacePaints.Count > 0, CustomizationId.FacePaint);
Set(set.FacePaints.Count > 0, CustomizationId.FacePaintColor);
}
// Create a list of lists of facial features and the legacy tattoo.
private static void SetFacialFeatures(CustomizationSet set, CharaMakeParams row)
{
var count = set.Faces.Count;
var featureDict = new List<IReadOnlyList<Customization>>(count);
for (var i = 0; i < count; ++i)
{
var legacyTattoo = new Customization(CustomizationId.FacialFeaturesTattoos, 1 << 7, 137905, (ushort)((i + 1) * 8));
featureDict.Add(row.FacialFeatureByFace[i].Icons.Select((val, idx)
=> new Customization(CustomizationId.FacialFeaturesTattoos, (byte)(1 << idx), val, (ushort)(i * 8 + idx)))
.Append(legacyTattoo)
.ToArray());
}
set.FeaturesTattoos = featureDict.ToArray();
}
// Set the names for the given set of parameters.
private void SetNames(CustomizationSet set, CharaMakeParams row)
{
var nameArray = ((CustomizationId[])Enum.GetValues(typeof(CustomizationId))).Select(c =>
{
// Find the first menu that corresponds to the Id.
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customization == c);
if (menu == null)
{
// If none exists and the id corresponds to highlights, set the Highlights name.
if (c == CustomizationId.HighlightsOnFlag)
return Lobby.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights";
// Otherwise there is an error and we use the default name.
return c.ToDefaultName();
}
// Facial Features and Tattoos is created by combining two strings.
if (c == CustomizationId.FacialFeaturesTattoos)
return
$"{Lobby.GetRow(1741)?.Text.ToDalamudString().ToString() ?? "Facial Features"} & {Lobby.GetRow(1742)?.Text.ToDalamudString().ToString() ?? "Tattoos"}";
// Otherwise all is normal, get the menu name or if it does not work the default name.
var textRow = Lobby.GetRow(menu.Value.Id);
return textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName();
}).ToArray();
// Add names for both eye colors.
nameArray[(int)CustomizationId.EyeColorL] = nameArray[(int)CustomizationId.EyeColorR];
nameArray[(int)CustomizationId.EyeColorR] = _options.GetName(CustomName.OddEyes);
set.OptionName = nameArray;
}
// Obtain available skin and hair colors for the given subrace and gender.
private (Customization[], Customization[]) GetColors(SubRace race, Gender gender)
{
if (race is > SubRace.Veena or SubRace.Unknown)
throw new ArgumentOutOfRangeException(nameof(race), race, null);
var gv = gender == Gender.Male ? 0 : 1;
var idx = ((int)race * 2 + gv) * 5 + 3;
return (CreateColorPicker(CustomizationId.SkinColor, idx << 8, 192),
CreateColorPicker(CustomizationId.HairColor, (idx + 1) << 8, 192));
}
// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender.
private Customization[] GetHairStyles(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
// Unknown30 is the number of available hairstyles.
var hairList = new List<Customization>(row.Unknown30);
// Hairstyles can be found starting at Unknown66.
for (var i = 0; i < row.Unknown30; ++i)
{
var name = $"Unknown{66 + i * 9}";
var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
// Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx);
hairList.Add(hairRow != null
? new Customization(CustomizationId.Hairstyle, hairRow.FeatureID, hairRow.Icon, (ushort)hairRow.RowId)
: new Customization(CustomizationId.Hairstyle, (byte)i, customizeIdx));
}
return hairList.ToArray();
}
// Get Features.
private Customization FromValueAndIndex(CustomizationId id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new Customization(id, (byte)(index + 1), value)
: new Customization(id, row.FeatureID, row.Icon, (ushort)row.RowId);
}
// Get List sizes.
private static int GetListSize(CharaMakeParams row, CustomizationId id)
{
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == id);
return menu?.Size ?? 0;
}
// Get face paints from the hair sheet via reflection.
private Customization[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<Customization>(row.Unknown37);
// Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i)
{
// Face paints start at Unknown73.
var name = $"Unknown{73 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
paintList.Add(paintRow != null
? new Customization(CustomizationId.FacePaint, paintRow.FeatureID, paintRow.Icon, (ushort)paintRow.RowId)
: new Customization(CustomizationId.FacePaint, (byte)i, customizeIdx));
}
return paintList.ToArray();
}
// Specific icons for tails or ears.
private Customization[] GetTailEarShapes(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.TailEarShape)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.TailEarShape, v, i)).ToArray()
?? Array.Empty<Customization>();
// Specific icons for faces.
private Customization[] GetFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.Face)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.Face, v, i)).ToArray()
?? Array.Empty<Customization>();
// Specific icons for Hrothgar patterns.
private Customization[] HrothgarFurPattern(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.LipColor)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.LipColor, v, i)).ToArray()
?? Array.Empty<Customization>();
}
}

View file

@ -1,31 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization;
// Each Subrace and Gender combo has a customization set.
// This describes the available customizations, their types and their names.
public class CustomizationSet
{
public const int DefaultAvailable =
(1 << (int)CustomizationId.Height)
| (1 << (int)CustomizationId.Hairstyle)
| (1 << (int)CustomizationId.SkinColor)
| (1 << (int)CustomizationId.EyeColorR)
| (1 << (int)CustomizationId.EyeColorL)
| (1 << (int)CustomizationId.HairColor)
| (1 << (int)CustomizationId.HighlightColor)
| (1 << (int)CustomizationId.FacialFeaturesTattoos)
| (1 << (int)CustomizationId.TattooColor)
| (1 << (int)CustomizationId.LipColor)
| (1 << (int)CustomizationId.Height);
internal CustomizationSet(SubRace clan, Gender gender)
{
Gender = gender;
Clan = clan;
_settingAvailable = clan.ToRace() == Race.Hrothgar && gender == Gender.Female
? 0
? 0u
: DefaultAvailable;
}
@ -35,39 +25,50 @@ public class CustomizationSet
public Race Race
=> Clan.ToRace();
private int _settingAvailable;
private uint _settingAvailable;
internal void SetAvailable(CustomizationId id)
=> _settingAvailable |= 1 << (int)id;
=> _settingAvailable |= 1u << (int)id;
public bool IsAvailable(CustomizationId id)
=> (_settingAvailable & (1 << (int)id)) != 0;
=> (_settingAvailable & (1u << (int)id)) != 0;
public int NumEyebrows { get; internal set; }
public int NumEyeShapes { get; internal set; }
public int NumNoseShapes { get; internal set; }
public int NumJawShapes { get; internal set; }
public int NumMouthShapes { get; internal set; }
public int NumEyebrows { get; internal init; }
public int NumEyeShapes { get; internal init; }
public int NumNoseShapes { get; internal init; }
public int NumJawShapes { get; internal init; }
public int NumMouthShapes { get; internal init; }
public string ToHumanReadable(CustomizationData customizationData)
{
var sb = new StringBuilder();
foreach (var id in Enum.GetValues<CustomizationId>().Where(IsAvailable))
sb.AppendFormat("{0,-20}", Option(id)).Append(customizationData[id]);
return sb.ToString();
}
public IReadOnlyList<string> OptionName { get; internal set; } = null!;
public IReadOnlyList<Customization> Faces { get; internal set; } = null!;
public IReadOnlyList<Customization> HairStyles { get; internal set; } = null!;
public IReadOnlyList<Customization> TailEarShapes { get; internal set; } = null!;
public IReadOnlyList<IReadOnlyList<Customization>> FeaturesTattoos { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaints { get; internal set; } = null!;
public IReadOnlyList<string> OptionName { get; internal set; } = null!;
public IReadOnlyList<Customization> Faces { get; internal init; } = null!;
public IReadOnlyList<Customization> HairStyles { get; internal init; } = null!;
public IReadOnlyList<Customization> TailEarShapes { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<Customization>> FeaturesTattoos { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaints { get; internal init; } = null!;
public IReadOnlyList<Customization> SkinColors { get; internal set; } = null!;
public IReadOnlyList<Customization> HairColors { get; internal set; } = null!;
public IReadOnlyList<Customization> HighlightColors { get; internal set; } = null!;
public IReadOnlyList<Customization> EyeColors { get; internal set; } = null!;
public IReadOnlyList<Customization> TattooColors { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaintColorsLight { get; internal set; } = null!;
public IReadOnlyList<Customization> FacePaintColorsDark { get; internal set; } = null!;
public IReadOnlyList<Customization> LipColorsLight { get; internal set; } = null!;
public IReadOnlyList<Customization> LipColorsDark { get; internal set; } = null!;
public IReadOnlyList<Customization> SkinColors { get; internal init; } = null!;
public IReadOnlyList<Customization> HairColors { get; internal init; } = null!;
public IReadOnlyList<Customization> HighlightColors { get; internal init; } = null!;
public IReadOnlyList<Customization> EyeColors { get; internal init; } = null!;
public IReadOnlyList<Customization> TattooColors { get; internal init; } = null!;
public IReadOnlyList<Customization> FacePaintColorsLight { get; internal init; } = null!;
public IReadOnlyList<Customization> FacePaintColorsDark { get; internal init; } = null!;
public IReadOnlyList<Customization> LipColorsLight { get; internal init; } = null!;
public IReadOnlyList<Customization> LipColorsDark { get; internal init; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizationId[]> Order { get; internal set; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public string Option(CustomizationId id)
=> OptionName[(int)id];
@ -154,6 +155,15 @@ public class CustomizationSet
public CharaMakeParams.MenuType Type(CustomizationId id)
=> Types[(int)id];
internal static IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizationId[]> ComputeOrder(CustomizationSet set)
{
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.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray());
}
public int Count(CustomizationId id)
{
@ -187,4 +197,17 @@ public class CustomizationSet
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
};
}
private const uint DefaultAvailable =
(1u << (int)CustomizationId.Height)
| (1u << (int)CustomizationId.Hairstyle)
| (1u << (int)CustomizationId.SkinColor)
| (1u << (int)CustomizationId.EyeColorR)
| (1u << (int)CustomizationId.EyeColorL)
| (1u << (int)CustomizationId.HairColor)
| (1u << (int)CustomizationId.HighlightColor)
| (1u << (int)CustomizationId.FacialFeaturesTattoos)
| (1u << (int)CustomizationId.TattooColor)
| (1u << (int)CustomizationId.LipColor)
| (1u << (int)CustomizationId.Height);
}

View file

@ -1,17 +1,16 @@
using System.Collections.Generic;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
namespace Glamourer.Customization;
public interface ICustomizationManager
{
public interface ICustomizationManager
{
public IReadOnlyList<Race> Races { get; }
public IReadOnlyList<SubRace> Clans { get; }
public IReadOnlyList<Gender> Genders { get; }
public IReadOnlyList<Race> Races { get; }
public IReadOnlyList<SubRace> Clans { get; }
public IReadOnlyList<Gender> Genders { get; }
public CustomizationSet GetList(SubRace race, Gender gender);
public CustomizationSet GetList(SubRace race, Gender gender);
public ImGuiScene.TextureWrap GetIcon(uint iconId);
public string GetName(CustomName name);
}
public ImGuiScene.TextureWrap GetIcon(uint iconId);
public string GetName(CustomName name);
}

View file

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Glamourer.Structs;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
@ -17,20 +20,14 @@ public static class GameData
private static Dictionary<EquipSlot, List<Item>>? _itemsBySlot;
private static Dictionary<byte, Job>? _jobs;
private static Dictionary<ushort, JobGroup>? _jobGroups;
private static SortedList<uint, ModelChara>? _models;
private static ModelData? _models;
private static RestrictedGear? _restrictedGear;
public static IReadOnlyDictionary<uint, ModelChara> Models(DataManager dataManager)
{
if (_models != null)
return _models;
public static RestrictedGear RestrictedGear(DataManager dataManager)
=> _restrictedGear ??= new RestrictedGear(dataManager);
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 ModelData Models(DataManager dataManager)
=> _models ??= new ModelData(dataManager);
public static IReadOnlyDictionary<byte, Stain> Stains(DataManager dataManager)
{

View file

@ -39,6 +39,10 @@
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private>False</Private>

View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Lumina.Excel.GeneratedSheets;
using Companion = Lumina.Excel.GeneratedSheets.Companion;
namespace Glamourer;
public class ModelData
{
public struct Data
{
public readonly ModelChara Model;
public string FirstName { get; }
public string AllNames { get; internal set; }
public Data(ModelChara model, string name)
{
Model = model;
FirstName = $"{name} #{model.RowId:D4}";
AllNames = $"#{model.RowId:D4}\n{name}";
}
}
private readonly SortedList<uint, Data> _models;
public IReadOnlyDictionary<uint, Data> Models
=> _models;
public ModelData(DataManager dataManager)
{
var modelSheet = dataManager.GetExcelSheet<ModelChara>();
_models = new SortedList<uint, Data>(NpcNames.ModelCharas.Count);
void UpdateData(uint model, string name)
{
name = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name);
if (_models.TryGetValue(model, out var data))
data.AllNames = $"{data.AllNames}\n{name}";
else
data = new Data(modelSheet!.GetRow(model)!, name);
_models[model] = data;
}
var companionSheet = dataManager.GetExcelSheet<Companion>()!;
foreach (var companion in companionSheet.Where(c => c.Model.Row != 0 && c.Singular.RawData.Length > 0))
UpdateData(companion.Model.Row, companion.Singular.ToDalamudString().TextValue);
var mountSheet = dataManager.GetExcelSheet<Mount>()!;
foreach (var mount in mountSheet.Where(c => c.ModelChara.Row != 0 && c.Singular.RawData.Length > 0))
UpdateData(mount.ModelChara.Row, mount.Singular.ToDalamudString().TextValue);
var bNpcNames = dataManager.GetExcelSheet<BNpcName>()!;
foreach (var (model, list) in NpcNames.ModelCharas)
{
foreach (var nameId in list)
{
var name = nameId >= 0
? bNpcNames.GetRow((uint)nameId)?.Singular.ToDalamudString().TextValue ?? string.Empty
: NpcNames.Names[~nameId];
if (name.Length == 0)
continue;
UpdateData(model, name);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,441 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Dalamud.Logging;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer;
// Handle gender- or race-locked gear in the draw model itself.
// Racial gear gets swapped to the correct current race and gender (it is one set each).
// Gender-locked gear gets swapped to the equivalent set if it exists (most of them do),
// with some items getting send to emperor's new clothes and a few funny entries.
public class RestrictedGear
{
private readonly ExcelSheet<Item> _items;
private readonly ExcelSheet<EquipRaceCategory> _categories;
private readonly HashSet<uint> _raceGenderSet = RaceGenderGroup.Where(c => c != 0).ToHashSet();
private readonly Dictionary<uint, uint> _maleToFemale = new();
private readonly Dictionary<uint, uint> _femaleToMale = new();
internal RestrictedGear(DataManager gameData)
{
_items = gameData.GetExcelSheet<Item>()!;
_categories = gameData.GetExcelSheet<EquipRaceCategory>()!;
AddKnown();
UnhandledRestrictedGear(false); // Set this to true to create a print of unassigned gear on launch.
}
// Resolve a model given by its model id, variant and slot for your current race and gender.
public (bool Replaced, SetId ModelId, byte Variant) ResolveRestricted(SetId modelId, byte variant, EquipSlot slot, Race race,
Gender gender)
{
var quad = modelId.Value | ((uint)variant << 16);
// Check racial gear, this does not need slots.
if (RaceGenderGroup.Contains(quad))
{
var idx = ((int)race - 1) * 2 + (gender is Gender.Female or Gender.FemaleNpc ? 1 : 0);
var value = RaceGenderGroup[idx];
return (value != quad, (ushort)value, (byte)(value >> 16));
}
// Check gender slots. If current gender is female, check if anything needs to be changed from male to female,
// and vice versa.
// Some items lead to the exact same model- and variant id just gender specified,
// so check for actual difference in the Replaced bool.
var needle = quad | ((uint)slot.ToSlot() << 24);
if (gender is Gender.Female or Gender.FemaleNpc && _maleToFemale.TryGetValue(needle, out var newValue)
|| gender is Gender.Male or Gender.MaleNpc && _femaleToMale.TryGetValue(needle, out newValue))
return (quad != newValue, (ushort)newValue, (byte)(newValue >> 16));
// The gear is not restricted.
return (false, modelId, variant);
}
// Add all unknown restricted gear and pair it with emperor's new gear on start up.
// Can also print unhandled items.
public void UnhandledRestrictedGear(bool print = false)
{
if (print)
PluginLog.Information("#### MALE ONLY ######");
void AddEmperor(Item item, bool male, bool female)
{
var slot = ((EquipSlot)item.EquipSlotCategory.Row).ToSlot();
var emperor = slot switch
{
EquipSlot.Head => 10032u,
EquipSlot.Body => 10033u,
EquipSlot.Hands => 10034u,
EquipSlot.Legs => 10035u,
EquipSlot.Feet => 10036u,
EquipSlot.Ears => 09293u,
EquipSlot.Neck => 09292u,
EquipSlot.Wrists => 09294u,
EquipSlot.RFinger => 09295u,
EquipSlot.LFinger => 09295u,
_ => 0u,
};
if (emperor == 0)
return;
if (male)
AddItem(item.RowId, emperor, true, false);
if (female)
AddItem(emperor, item.RowId, false, true);
}
var unhandled = 0;
foreach (var item in _items.Where(i => i.EquipRestriction == 2))
{
if (_maleToFemale.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24)))
continue;
++unhandled;
AddEmperor(item, true, false);
if (print)
PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}");
}
if (print)
PluginLog.Information("#### FEMALE ONLY ####");
foreach (var item in _items.Where(i => i.EquipRestriction == 3))
{
if (_femaleToMale.ContainsKey((uint)item.ModelMain | ((uint)((EquipSlot)item.EquipSlotCategory.Row).ToSlot() << 24)))
continue;
++unhandled;
AddEmperor(item, false, true);
if (print)
PluginLog.Information($"{item.RowId:D5} {item.Name.ToDalamudString().TextValue}");
}
if (print)
PluginLog.Information("#### OTHER #########");
foreach (var item in _items.Where(i => i.EquipRestriction > 3))
{
if (_raceGenderSet.Contains((uint)item.ModelMain))
continue;
++unhandled;
if (print)
PluginLog.Information(
$"{item.RowId:D5} {item.Name.ToDalamudString().TextValue} RestrictionGroup {_categories.GetRow(item.EquipRestriction)!.RowId:D2}");
}
if (unhandled > 0)
PluginLog.Warning("There were {Num} restricted items not handled and directed to Emperor's New Set.", unhandled);
}
// Add a item redirection by its item - NOT MODEL - id.
// This uses the items model as well as its slot.
// Creates a <-> redirection by default but can add -> or <- redirections by setting the corresponding bools to false.
// Prints warnings if anything does not make sense.
private void AddItem(uint itemIdMale, uint itemIdFemale, bool addMale = true, bool addFemale = true)
{
if (!addMale && !addFemale)
return;
var mItem = _items.GetRow(itemIdMale);
var fItem = _items.GetRow(itemIdFemale);
if (mItem == null || fItem == null)
{
PluginLog.Warning($"Could not add item {itemIdMale} or {itemIdFemale} to restricted items.");
return;
}
if (mItem.EquipRestriction != 2 && addMale)
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not restricted anymore.");
return;
}
if (fItem.EquipRestriction != 3 && addFemale)
{
PluginLog.Warning($"{fItem.Name.ToDalamudString().TextValue} is not restricted anymore.");
return;
}
var mSlot = ((EquipSlot)mItem.EquipSlotCategory.Row).ToSlot();
var fSlot = ((EquipSlot)fItem.EquipSlotCategory.Row).ToSlot();
if (!mSlot.IsAccessory() && !mSlot.IsEquipment())
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} is not equippable to a known slot.");
return;
}
if (mSlot != fSlot)
{
PluginLog.Warning($"{mItem.Name.ToDalamudString().TextValue} and {fItem.Name.ToDalamudString().TextValue} are not compatible.");
return;
}
var mModelIdSlot = (uint)mItem.ModelMain | ((uint)mSlot << 24);
var fModelIdSlot = (uint)fItem.ModelMain | ((uint)fSlot << 24);
if (addMale)
_maleToFemale.TryAdd(mModelIdSlot, fModelIdSlot);
if (addFemale)
_femaleToMale.TryAdd(fModelIdSlot, mModelIdSlot);
}
// Add all currently existing and known gender restricted items.
private void AddKnown()
{
AddItem(02967, 02970); // Lord's Yukata (Blue) <-> Lady's Yukata (Red)
AddItem(02968, 02971); // Lord's Yukata (Green) <-> Lady's Yukata (Blue)
AddItem(02969, 02972); // Lord's Yukata (Grey) <-> Lady's Yukata (Black)
AddItem(02973, 02978); // Red Summer Top <-> Red Summer Halter
AddItem(02974, 02979); // Green Summer Top <-> Green Summer Halter
AddItem(02975, 02980); // Blue Summer Top <-> Blue Summer Halter
AddItem(02976, 02981); // Solar Summer Top <-> Solar Summer Halter
AddItem(02977, 02982); // Lunar Summer Top <-> Lunar Summer Halter
AddItem(02996, 02997); // Hempen Undershirt <-> Hempen Camise
AddItem(03280, 03283); // Lord's Drawers (Black) <-> Lady's Knickers (Black)
AddItem(03281, 03284); // Lord's Drawers (White) <-> Lady's Knickers (White)
AddItem(03282, 03285); // Lord's Drawers (Gold) <-> Lady's Knickers (Gold)
AddItem(03286, 03291); // Red Summer Trunks <-> Red Summer Tanga
AddItem(03287, 03292); // Green Summer Trunks <-> Green Summer Tanga
AddItem(03288, 03293); // Blue Summer Trunks <-> Blue Summer Tanga
AddItem(03289, 03294); // Solar Summer Trunks <-> Solar Summer Tanga
AddItem(03290, 03295); // Lunar Summer Trunks <-> Lunar Summer Tanga
AddItem(03307, 03308); // Hempen Underpants <-> Hempen Pantalettes
AddItem(03748, 03749); // Lord's Clogs <-> Lady's Clogs
AddItem(06045, 06041); // Bohemian's Coat <-> Guardian Corps Coat
AddItem(06046, 06042); // Bohemian's Gloves <-> Guardian Corps Gauntlets
AddItem(06047, 06043); // Bohemian's Trousers <-> Guardian Corps Skirt
AddItem(06048, 06044); // Bohemian's Boots <-> Guardian Corps Boots
AddItem(06094, 06098); // Summer Evening Top <-> Summer Morning Halter
AddItem(06095, 06099); // Summer Evening Trunks <-> Summer Morning Tanga
AddItem(06096, 06100); // Striped Summer Top <-> Striped Summer Halter
AddItem(06097, 06101); // Striped Summer Trunks <-> Striped Summer Tanga
AddItem(06102, 06104); // Black Summer Top <-> Black Summer Halter
AddItem(06103, 06105); // Black Summer Trunks <-> Black Summer Tanga
AddItem(06972, 06973); // Valentione Apron <-> Valentione Apron Dress
AddItem(06975, 06976); // Valentione Trousers <-> Valentione Skirt
AddItem(08532, 08535); // Lord's Yukata (Blackflame) <-> Lady's Yukata (Redfly)
AddItem(08533, 08536); // Lord's Yukata (Whiteflame) <-> Lady's Yukata (Bluefly)
AddItem(08534, 08537); // Lord's Yukata (Blueflame) <-> Lady's Yukata (Pinkfly)
AddItem(08542, 08549); // Ti Leaf Lei <-> Coronal Summer Halter
AddItem(08543, 08550); // Red Summer Maro <-> Red Summer Pareo
AddItem(08544, 08551); // South Seas Talisman <-> Sea Breeze Summer Halter
AddItem(08545, 08552); // Blue Summer Maro <-> Sea Breeze Summer Pareo
AddItem(08546, 08553); // Coeurl Talisman <-> Coeurl Beach Halter
AddItem(08547, 08554); // Coeurl Beach Maro <-> Coeurl Beach Pareo
AddItem(08548, 08555); // Coeurl Beach Briefs <-> Coeurl Beach Tanga
AddItem(10316, 10317); // Southern Seas Vest <-> Southern Seas Swimsuit
AddItem(10318, 10319); // Southern Seas Trunks <-> Southern Seas Tanga
AddItem(10320, 10321); // Striped Southern Seas Vest <-> Striped Southern Seas Swimsuit
AddItem(13298, 13567); // Black-feathered Flat Hat <-> Red-feathered Flat Hat
AddItem(13300, 13639); // Lord's Suikan <-> Lady's Suikan
AddItem(13724, 13725); // Little Lord's Clogs <-> Little Lady's Clogs
AddItem(14854, 14857); // Eastern Lord's Togi <-> Eastern Lady's Togi
AddItem(14855, 14858); // Eastern Lord's Trousers <-> Eastern Lady's Loincloth
AddItem(14856, 14859); // Eastern Lord's Crakows <-> Eastern Lady's Crakows
AddItem(15639, 15642); // Far Eastern Patriarch's Hat <-> Far Eastern Matriarch's Sun Hat
AddItem(15640, 15643); // Far Eastern Patriarch's Tunic <-> Far Eastern Matriarch's Dress
AddItem(15641, 15644); // Far Eastern Patriarch's Longboots <-> Far Eastern Matriarch's Boots
AddItem(15922, 15925); // Moonfire Vest <-> Moonfire Halter
AddItem(15923, 15926); // Moonfire Trunks <-> Moonfire Tanga
AddItem(15924, 15927); // Moonfire Caligae <-> Moonfire Sandals
AddItem(16106, 16111); // Makai Mauler's Facemask <-> Makai Manhandler's Facemask
AddItem(16107, 16112); // Makai Mauler's Oilskin <-> Makai Manhandler's Jerkin
AddItem(16108, 16113); // Makai Mauler's Fingerless Gloves <-> Makai Manhandler's Fingerless Gloves
AddItem(16109, 16114); // Makai Mauler's Leggings <-> Makai Manhandler's Quartertights
AddItem(16110, 16115); // Makai Mauler's Boots <-> Makai Manhandler's Longboots
AddItem(16116, 16121); // Makai Marksman's Eyepatch <-> Makai Markswoman's Ribbon
AddItem(16117, 16122); // Makai Marksman's Battlegarb <-> Makai Markswoman's Battledress
AddItem(16118, 16123); // Makai Marksman's Fingerless Gloves <-> Makai Markswoman's Fingerless Gloves
AddItem(16119, 16124); // Makai Marksman's Slops <-> Makai Markswoman's Quartertights
AddItem(16120, 16125); // Makai Marksman's Boots <-> Makai Markswoman's Longboots
AddItem(16126, 16131); // Makai Sun Guide's Circlet <-> Makai Moon Guide's Circlet
AddItem(16127, 16132); // Makai Sun Guide's Oilskin <-> Makai Moon Guide's Gown
AddItem(16128, 16133); // Makai Sun Guide's Fingerless Gloves <-> Makai Moon Guide's Fingerless Gloves
AddItem(16129, 16134); // Makai Sun Guide's Slops <-> Makai Moon Guide's Quartertights
AddItem(16130, 16135); // Makai Sun Guide's Boots <-> Makai Moon Guide's Longboots
AddItem(16136, 16141); // Makai Priest's Coronet <-> Makai Priestess's Headdress
AddItem(16137, 16142); // Makai Priest's Doublet Robe <-> Makai Priestess's Jerkin
AddItem(16138, 16143); // Makai Priest's Fingerless Gloves <-> Makai Priestess's Fingerless Gloves
AddItem(16139, 16144); // Makai Priest's Slops <-> Makai Priestess's Skirt
AddItem(16140, 16145); // Makai Priest's Boots <-> Makai Priestess's Longboots
AddItem(16588, 16592); // Far Eastern Gentleman's Hat <-> Far Eastern Beauty's Hairpin
AddItem(16589, 16593); // Far Eastern Gentleman's Robe <-> Far Eastern Beauty's Robe
AddItem(16590, 16594); // Far Eastern Gentleman's Haidate <-> Far Eastern Beauty's Koshita
AddItem(16591, 16595); // Far Eastern Gentleman's Boots <-> Far Eastern Beauty's Boots
AddItem(17204, 17209); // Common Makai Mauler's Facemask <-> Common Makai Manhandler's Facemask
AddItem(17205, 17210); // Common Makai Mauler's Oilskin <-> Common Makai Manhandler's Jerkin
AddItem(17206, 17211); // Common Makai Mauler's Fingerless Gloves <-> Common Makai Manhandler's Fingerless Glove
AddItem(17207, 17212); // Common Makai Mauler's Leggings <-> Common Makai Manhandler's Quartertights
AddItem(17208, 17213); // Common Makai Mauler's Boots <-> Common Makai Manhandler's Longboots
AddItem(17214, 17219); // Common Makai Marksman's Eyepatch <-> Common Makai Markswoman's Ribbon
AddItem(17215, 17220); // Common Makai Marksman's Battlegarb <-> Common Makai Markswoman's Battledress
AddItem(17216, 17221); // Common Makai Marksman's Fingerless Gloves <-> Common Makai Markswoman's Fingerless Glove
AddItem(17217, 17222); // Common Makai Marksman's Slops <-> Common Makai Markswoman's Quartertights
AddItem(17218, 17223); // Common Makai Marksman's Boots <-> Common Makai Markswoman's Longboots
AddItem(17224, 17229); // Common Makai Sun Guide's Circlet <-> Common Makai Moon Guide's Circlet
AddItem(17225, 17230); // Common Makai Sun Guide's Oilskin <-> Common Makai Moon Guide's Gown
AddItem(17226, 17231); // Common Makai Sun Guide's Fingerless Gloves <-> Common Makai Moon Guide's Fingerless Glove
AddItem(17227, 17232); // Common Makai Sun Guide's Slops <-> Common Makai Moon Guide's Quartertights
AddItem(17228, 17233); // Common Makai Sun Guide's Boots <-> Common Makai Moon Guide's Longboots
AddItem(17234, 17239); // Common Makai Priest's Coronet <-> Common Makai Priestess's Headdress
AddItem(17235, 17240); // Common Makai Priest's Doublet Robe <-> Common Makai Priestess's Jerkin
AddItem(17236, 17241); // Common Makai Priest's Fingerless Gloves <-> Common Makai Priestess's Fingerless Gloves
AddItem(17237, 17242); // Common Makai Priest's Slops <-> Common Makai Priestess's Skirt
AddItem(17238, 17243); // Common Makai Priest's Boots <-> Common Makai Priestess's Longboots
AddItem(17481, 17476); // Royal Seneschal's Chapeau <-> Songbird Hat
AddItem(17482, 17477); // Royal Seneschal's Coat <-> Songbird Jacket
AddItem(17483, 17478); // Royal Seneschal's Fingerless Gloves <-> Songbird Gloves
AddItem(17484, 17479); // Royal Seneschal's Breeches <-> Songbird Skirt
AddItem(17485, 17480); // Royal Seneschal's Boots <-> Songbird Boots
AddItem(20479, 20484); // Star of the Nezha Lord <-> Star of the Nezha Lady
AddItem(20480, 20485); // Nezha Lord's Togi <-> Nezha Lady's Togi
AddItem(20481, 20486); // Nezha Lord's Gloves <-> Nezha Lady's Gloves
AddItem(20482, 20487); // Nezha Lord's Slops <-> Nezha Lady's Slops
AddItem(20483, 20488); // Nezha Lord's Boots <-> Nezha Lady's Kneeboots
AddItem(22367, 22372); // Faerie Tale Prince's Circlet <-> Faerie Tale Princess's Tiara
AddItem(22368, 22373); // Faerie Tale Prince's Vest <-> Faerie Tale Princess's Dress
AddItem(22369, 22374); // Faerie Tale Prince's Gloves <-> Faerie Tale Princess's Gloves
AddItem(22370, 22375); // Faerie Tale Prince's Slops <-> Faerie Tale Princess's Long Skirt
AddItem(22371, 22376); // Faerie Tale Prince's Boots <-> Faerie Tale Princess's Heels
AddItem(24599, 24602); // Far Eastern Schoolboy's Hat <-> Far Eastern Schoolgirl's Hair Ribbon
AddItem(24600, 24603); // Far Eastern Schoolboy's Hakama <-> Far Eastern Schoolgirl's Hakama
AddItem(24601, 24604); // Far Eastern Schoolboy's Zori <-> Far Eastern Schoolgirl's Boots
AddItem(28558, 28573); // Valentione Rose Hat <-> Valentione Rose Ribboned Hat
AddItem(28559, 28574); // Valentione Rose Waistcoat <-> Valentione Rose Dress
AddItem(28560, 28575); // Valentione Rose Gloves <-> Valentione Rose Ribboned Gloves
AddItem(28561, 28576); // Valentione Rose Slacks <-> Valentione Rose Tights
AddItem(28562, 28577); // Valentione Rose Shoes <-> Valentione Rose Heels
AddItem(28563, 28578); // Valentione Forget-me-not Hat <-> Valentione Forget-me-not Ribboned Hat
AddItem(28564, 28579); // Valentione Forget-me-not Waistcoat <-> Valentione Forget-me-not Dress
AddItem(28565, 28580); // Valentione Forget-me-not Gloves <-> Valentione Forget-me-not Ribboned Gloves
AddItem(28566, 28581); // Valentione Forget-me-not Slacks <-> Valentione Forget-me-not Tights
AddItem(28567, 28582); // Valentione Forget-me-not Shoes <-> Valentione Forget-me-not Heels
AddItem(28568, 28583); // Valentione Acacia Hat <-> Valentione Acacia Ribboned Hat
AddItem(28569, 28584); // Valentione Acacia Waistcoat <-> Valentione Acacia Dress
AddItem(28570, 28585); // Valentione Acacia Gloves <-> Valentione Acacia Ribboned Gloves
AddItem(28571, 28586); // Valentione Acacia Slacks <-> Valentione Acacia Tights
AddItem(28572, 28587); // Valentione Acacia Shoes <-> Valentione Acacia Heels
AddItem(28600, 28605); // Eastern Lord Errant's Hat <-> Eastern Lady Errant's Hat
AddItem(28601, 28606); // Eastern Lord Errant's Jacket <-> Eastern Lady Errant's Coat
AddItem(28602, 28607); // Eastern Lord Errant's Wristbands <-> Eastern Lady Errant's Gloves
AddItem(28603, 28608); // Eastern Lord Errant's Trousers <-> Eastern Lady Errant's Skirt
AddItem(28604, 28609); // Eastern Lord Errant's Shoes <-> Eastern Lady Errant's Boots
AddItem(31408, 31413); // Bergsteiger's Hat <-> Dirndl's Hat
AddItem(31409, 31414); // Bergsteiger's Jacket <-> Dirndl's Bodice
AddItem(31410, 31415); // Bergsteiger's Halfgloves <-> Dirndl's Wrist Torque
AddItem(31411, 31416); // Bergsteiger's Halfslops <-> Dirndl's Long Skirt
AddItem(31412, 31417); // Bergsteiger's Boots <-> Dirndl's Pumps
AddItem(36336, 36337); // Omega-M Attire <-> Omega-F Attire
AddItem(36338, 36339); // Omega-M Ear Cuffs <-> Omega-F Earrings
AddItem(37442, 37447); // Makai Vanguard's Monocle <-> Makai Vanbreaker's Ribbon
AddItem(37443, 37448); // Makai Vanguard's Battlegarb <-> Makai Vanbreaker's Battledress
AddItem(37444, 37449); // Makai Vanguard's Fingerless Gloves <-> Makai Vanbreaker's Fingerless Gloves
AddItem(37445, 37450); // Makai Vanguard's Leggings <-> Makai Vanbreaker's Quartertights
AddItem(37446, 37451); // Makai Vanguard's Boots <-> Makai Vanbreaker's Longboots
AddItem(37452, 37457); // Makai Harbinger's Facemask <-> Makai Harrower's Facemask
AddItem(37453, 37458); // Makai Harbinger's Battlegarb <-> Makai Harrower's Jerkin
AddItem(37454, 37459); // Makai Harbinger's Fingerless Gloves <-> Makai Harrower's Fingerless Gloves
AddItem(37455, 37460); // Makai Harbinger's Leggings <-> Makai Harrower's Quartertights
AddItem(37456, 37461); // Makai Harbinger's Boots <-> Makai Harrower's Longboots
AddItem(37462, 37467); // Common Makai Vanguard's Monocle <-> Common Makai Vanbreaker's Ribbon
AddItem(37463, 37468); // Common Makai Vanguard's Battlegarb <-> Common Makai Vanbreaker's Battledress
AddItem(37464, 37469); // Common Makai Vanguard's Fingerless Gloves <-> Common Makai Vanbreaker's Fingerless Gloves
AddItem(37465, 37470); // Common Makai Vanguard's Leggings <-> Common Makai Vanbreaker's Quartertights
AddItem(37466, 37471); // Common Makai Vanguard's Boots <-> Common Makai Vanbreaker's Longboots
AddItem(37472, 37477); // Common Makai Harbinger's Facemask <-> Common Makai Harrower's Facemask
AddItem(37473, 37478); // Common Makai Harbinger's Battlegarb <-> Common Makai Harrower's Jerkin
AddItem(37474, 37479); // Common Makai Harbinger's Fingerless Gloves <-> Common Makai Harrower's Fingerless Gloves
AddItem(37475, 37480); // Common Makai Harbinger's Leggings <-> Common Makai Harrower's Quartertights
AddItem(37476, 37481); // Common Makai Harbinger's Boots <-> Common Makai Harrower's Longboots
AddItem(23003, 23008); // Mun'gaek Hat <-> Eastern Socialite's Hat
AddItem(23004, 23009); // Mun'gaek Uibok <-> Eastern Socialite's Cheongsam
AddItem(23005, 23010); // Mun'gaek Cuffs <-> Eastern Socialite's Gloves
AddItem(23006, 23011); // Mun'gaek Trousers <-> Eastern Socialite's Skirt
AddItem(23007, 23012); // Mun'gaek Boots <-> Eastern Socialite's Boots
AddItem(24148, 24153); // Far Eastern Officer's Hat <-> Far Eastern Maiden's Hat
AddItem(24149, 24154); // Far Eastern Officer's Robe <-> Far Eastern Maiden's Tunic
AddItem(24150, 24155); // Far Eastern Officer's Armband <-> Far Eastern Maiden's Armband
AddItem(24151, 24156); // Far Eastern Officer's Bottoms <-> Far Eastern Maiden's Bottoms
AddItem(24152, 24157); // Far Eastern Officer's Boots <-> Far Eastern Maiden's Boots
AddItem(13323, 13322); // Scion Thief's Tunic <-> Scion Conjurer's Dalmatica
AddItem(13693, 10034, true, false); // Scion Thief's Halfgloves -> The Emperor's New Gloves
AddItem(13694, 13691); // Scion Thief's Gaskins <-> Scion Conjurer's Chausses
AddItem(13695, 13692); // Scion Thief's Armored Caligae <-> Scion Conjurer's Pattens
AddItem(13326, 30063); // Scion Thaumaturge's Robe <-> Scion Sorceress's Headdress
AddItem(13696, 30062); // Scion Thaumaturge's Monocle <-> Scion Sorceress's Robe
AddItem(13697, 30064); // Scion Thaumaturge's Gauntlets <-> Scion Sorceress's Shadowtalons
AddItem(13698, 10035, true, false); // Scion Thaumaturge's Gaskins -> The Emperor's New Breeches
AddItem(13699, 30065); // Scion Thaumaturge's Moccasins <-> Scion Sorceress's High Boots
AddItem(13327, 15942); // Scion Chronocler's Cowl <-> Scion Healer's Robe
AddItem(13700, 10034, true, false); // Scion Chronocler's Ringbands -> The Emperor's New Gloves
AddItem(13701, 15943); // Scion Chronocler's Tights <-> Scion Healer's Halftights
AddItem(13702, 15944); // Scion Chronocler's Caligae <-> Scion Healer's Highboots
AddItem(14861, 13324); // Head Engineer's Goggles <-> Scion Striker's Visor
AddItem(14862, 13325); // Head Engineer's Attire <-> Scion Striker's Attire
AddItem(15938, 33751); // Scion Rogue's Jacket <-> Oracle Top
AddItem(15939, 10034, true, false); // Scion Rogue's Armguards -> The Emperor's New Gloves
AddItem(15940, 33752); // Scion Rogue's Gaskins <-> Oracle Leggings
AddItem(15941, 33753); // Scion Rogue's Boots <-> Oracle Pantalettes
AddItem(16042, 16046); // Abes Jacket <-> High Summoner's Dress
AddItem(16043, 16047); // Abes Gloves <-> High Summoner's Armlets
AddItem(16044, 10035, true, false); // Abes Halfslops -> The Emperor's New Breeches
AddItem(16045, 16048); // Abes Boots <-> High Summoner's Boots
AddItem(17473, 28553); // Lord Commander's Coat <-> Majestic Dress
AddItem(17474, 28554); // Lord Commander's Gloves <-> Majestic Wristdresses
AddItem(10036, 28555, false); // Emperor's New Boots <- Majestic Boots
AddItem(21021, 21026); // Werewolf Feet <-> Werewolf Legs
AddItem(22452, 20633); // Cracked Manderville Monocle <-> Blackbosom Hat
AddItem(22453, 20634); // Torn Manderville Coatee <-> Blackbosom Dress
AddItem(22454, 20635); // Singed Manderville Gloves <-> Blackbosom Dress Gloves
AddItem(22455, 10035, true, false); // Stained Manderville Bottoms -> The Emperor's New Breeches
AddItem(22456, 20636); // Scuffed Manderville Gaiters <-> lackbosom Boots
AddItem(23013, 21302); // Doman Liege's Dogi <-> Scion Liberator's Jacket
AddItem(23014, 21303); // Doman Liege's Kote <-> Scion Liberator's Fingerless Gloves
AddItem(23015, 21304); // Doman Liege's Kyakui <-> Scion Liberator's Pantalettes
AddItem(23016, 21305); // Doman Liege's Kyahan <-> Scion Liberator's Sabatons
AddItem(09293, 21306, false); // The Emperor's New Earrings <- Scion Liberator's Earrings
AddItem(24158, 23008); // Leal Samurai's Kasa <-> Eastern Socialite's Hat
AddItem(24159, 23009); // Leal Samurai's Dogi <-> Eastern Socialite's Cheongsam
AddItem(24160, 23010); // Leal Samurai's Tekko <-> Eastern Socialite's Gloves
AddItem(24161, 23011); // Leal Samurai's Tsutsu-hakama <-> Eastern Socialite's Skirt
AddItem(24162, 23012); // Leal Samurai's Geta <-> Eastern Socialite's Boots
AddItem(02966, 13321, false); // Reindeer Suit <- Antecedent's Attire
AddItem(15479, 36843, false); // Swine Body <- Lyse's Leadership Attire
AddItem(21941, 24999, false); // Ala Mhigan Gown <- Gown of Light
AddItem(30757, 25000, false); // Southern Seas Skirt <- Skirt of Light
AddItem(36821, 27933, false); // Archfiend Helm <- Scion Hearer's Hood
AddItem(36822, 27934, false); // Archfiend Armor <- Scion Hearer's Coat
AddItem(36825, 27935, false); // Archfiend Sabatons <- Scion Hearer's Shoes
}
// The racial starter sets are available for all 4 slots each,
// but have no associated accessories or hats.
private static readonly uint[] RaceGenderGroup =
{
0x020054,
0x020055,
0x020056,
0x020057,
0x02005C,
0x02005D,
0x020058,
0x020059,
0x02005A,
0x02005B,
0x020101,
0x020102,
0x010255,
uint.MaxValue, // TODO: Female Hrothgar
0x0102E8,
0x010245,
};
}

View file

@ -3,23 +3,25 @@ using Penumbra.GameData.Enums;
namespace Glamourer.Structs;
// Turn EquipSlot into a bitfield flag enum.
[Flags]
public enum CharacterEquipMask : ushort
{
None = 0,
None = 0,
MainHand = 0b000000000001,
OffHand = 0b000000000010,
Head = 0b000000000100,
Body = 0b000000001000,
Hands = 0b000000010000,
Legs = 0b000000100000,
Feet = 0b000001000000,
Ears = 0b000010000000,
Neck = 0b000100000000,
Wrists = 0b001000000000,
RFinger = 0b010000000000,
LFinger = 0b100000000000,
All = 0b111111111111,
OffHand = 0b000000000010,
Head = 0b000000000100,
Body = 0b000000001000,
Hands = 0b000000010000,
Legs = 0b000000100000,
Feet = 0b000001000000,
Ears = 0b000010000000,
Neck = 0b000100000000,
Wrists = 0b001000000000,
RFinger = 0b010000000000,
LFinger = 0b100000000000,
All = 0b111111111111,
}
public static class CharacterEquipMaskExtensions
@ -28,16 +30,16 @@ public static class CharacterEquipMaskExtensions
=> slot switch
{
EquipSlot.Unknown => false,
EquipSlot.Head => mask.HasFlag(CharacterEquipMask.Head),
EquipSlot.Body => mask.HasFlag(CharacterEquipMask.Body),
EquipSlot.Hands => mask.HasFlag(CharacterEquipMask.Hands),
EquipSlot.Legs => mask.HasFlag(CharacterEquipMask.Legs),
EquipSlot.Feet => mask.HasFlag(CharacterEquipMask.Feet),
EquipSlot.Ears => mask.HasFlag(CharacterEquipMask.Ears),
EquipSlot.Neck => mask.HasFlag(CharacterEquipMask.Neck),
EquipSlot.Wrists => mask.HasFlag(CharacterEquipMask.Wrists),
EquipSlot.Head => mask.HasFlag(CharacterEquipMask.Head),
EquipSlot.Body => mask.HasFlag(CharacterEquipMask.Body),
EquipSlot.Hands => mask.HasFlag(CharacterEquipMask.Hands),
EquipSlot.Legs => mask.HasFlag(CharacterEquipMask.Legs),
EquipSlot.Feet => mask.HasFlag(CharacterEquipMask.Feet),
EquipSlot.Ears => mask.HasFlag(CharacterEquipMask.Ears),
EquipSlot.Neck => mask.HasFlag(CharacterEquipMask.Neck),
EquipSlot.Wrists => mask.HasFlag(CharacterEquipMask.Wrists),
EquipSlot.RFinger => mask.HasFlag(CharacterEquipMask.RFinger),
EquipSlot.LFinger => mask.HasFlag(CharacterEquipMask.LFinger),
_ => false,
_ => false,
};
}

View file

@ -3,8 +3,37 @@ using Penumbra.GameData.Structs;
namespace Glamourer.Structs;
// An Item wrapper struct that contains the item table, a precomputed name and the associated equip slot.
public readonly struct Item
{
public readonly Lumina.Excel.GeneratedSheets.Item Base;
public readonly string Name;
public readonly EquipSlot EquippableTo;
// Obtain the main model info used by the item.
public (SetId id, WeaponType type, ushort variant) MainModel
=> ParseModel(EquippableTo, Base.ModelMain);
// Obtain the sub model info used by the item. Will be 0 if the item has no sub model.
public (SetId id, WeaponType type, ushort variant) SubModel
=> ParseModel(EquippableTo, Base.ModelSub);
public bool HasSubModel
=> Base.ModelSub != 0;
// Create a new item from its sheet list with the given name and either the inferred equip slot or the given one.
public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown)
{
Base = item;
Name = name;
EquippableTo = slot == EquipSlot.Unknown ? ((EquipSlot)item.EquipSlotCategory.Row).ToSlot() : slot;
}
// Create empty Nothing items.
public static Item Nothing(EquipSlot slot)
=> new("Nothing", slot);
// Produce the relevant model information for a given item and equip slot.
private static (SetId id, WeaponType type, ushort variant) ParseModel(EquipSlot slot, ulong data)
{
if (slot is EquipSlot.MainHand or EquipSlot.OffHand)
@ -22,33 +51,14 @@ public readonly struct Item
}
}
public readonly Lumina.Excel.GeneratedSheets.Item Base;
public readonly string Name;
public readonly EquipSlot EquippableTo;
public (SetId id, WeaponType type, ushort variant) MainModel
=> ParseModel(EquippableTo, Base.ModelMain);
public bool HasSubModel
=> Base.ModelSub != 0;
public (SetId id, WeaponType type, ushort variant) SubModel
=> ParseModel(EquippableTo, Base.ModelSub);
public Item(Lumina.Excel.GeneratedSheets.Item item, string name, EquipSlot slot = EquipSlot.Unknown)
{
Base = item;
Name = name;
EquippableTo = slot == EquipSlot.Unknown ? ((EquipSlot)item.EquipSlotCategory.Row).ToSlot() : slot;
}
public static Item Nothing(EquipSlot slot)
=> new("Nothing", slot);
// Used for 'Nothing' items.
private Item(string name, EquipSlot slot)
{
Name = name;
Base = new Lumina.Excel.GeneratedSheets.Item();
EquippableTo = slot;
}
public override string ToString()
=> Name;
}

View file

@ -1,7 +1,10 @@
using Lumina.Excel.GeneratedSheets;
using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Structs;
// A struct containing the different jobs the game supports.
// Also contains the jobs Name and Abbreviation as strings.
public readonly struct Job
{
public readonly string Name;
@ -14,7 +17,10 @@ public readonly struct Job
public Job(ClassJob job)
{
Base = job;
Name = job.Name.ToString();
Abbreviation = job.Abbreviation.ToString();
Name = job.Name.ToDalamudString().ToString();
Abbreviation = job.Abbreviation.ToDalamudString().ToString();
}
public override string ToString()
=> Name;
}

View file

@ -1,29 +1,31 @@
using System.Diagnostics;
using System.Linq;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Structs;
// The game specifies different job groups that can contain specific jobs or not.
public readonly struct JobGroup
{
public readonly string Name;
private readonly ulong _flags;
public readonly int Count;
public readonly uint Id;
public readonly string Name;
public readonly int Count;
public readonly uint Id;
private readonly ulong _flags;
// Create a job group from a given category and the ClassJob sheet.
// It looks up the different jobs contained in the category and sets the flags appropriately.
public JobGroup(ClassJobCategory group, ExcelSheet<ClassJob> jobs)
{
Count = 0;
Count = 0;
_flags = 0ul;
Id = group.RowId;
Name = group.Name.ToString();
Id = group.RowId;
Name = group.Name.ToString();
Debug.Assert(jobs.RowCount < 64);
foreach (var job in jobs)
{
var abbr = job.Abbreviation.ToString();
if (!abbr.Any())
if (abbr.Length == 0)
continue;
var prop = group.GetType().GetProperty(abbr);
@ -37,9 +39,11 @@ public readonly struct JobGroup
}
}
// Check if a job is contained inside this group.
public bool Fits(Job job)
=> Fits(job.Id);
// Check if a job is contained inside this group.
public bool Fits(uint jobId)
{
var flag = 1ul << (int)jobId;

View file

@ -1,22 +1,28 @@
using Penumbra.GameData.Structs;
using Dalamud.Utility;
using Penumbra.GameData.Structs;
namespace Glamourer.Structs;
// A wrapper for the clothing dyes the game provides with their RGBA color value, game ID, unmodified color value and name.
public readonly struct Stain
{
public readonly string Name;
public readonly uint RgbaColor;
// An empty stain with transparent color.
public static readonly Stain None = new("None");
public readonly string Name;
public readonly uint RgbaColor;
// Combine the Id byte with the 3 bytes of color values.
private readonly uint _seColorId;
public byte R
=> (byte)(RgbaColor & 0xFF);
public byte G
=> (byte)(RgbaColor >> 8 & 0xFF);
=> (byte)((RgbaColor >> 8) & 0xFF);
public byte B
=> (byte)(RgbaColor >> 16 & 0xFF);
=> (byte)((RgbaColor >> 16) & 0xFF);
public byte Intensity
=> (byte)((1 + R + G + B) / 3);
@ -27,23 +33,22 @@ public readonly struct Stain
public StainId RowIndex
=> (StainId)(_seColorId >> 24);
// R and B need to be shuffled and Alpha set to max.
public static uint SeColorToRgba(uint color)
=> (color & 0xFF) << 16 | color >> 16 & 0xFF | color & 0xFF00 | 0xFF000000;
=> ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000;
public Stain(byte index, Lumina.Excel.GeneratedSheets.Stain stain)
{
Name = stain.Name.ToString();
_seColorId = stain.Color | (uint)index << 24;
RgbaColor = SeColorToRgba(stain.Color);
Name = stain.Name.ToDalamudString().ToString();
_seColorId = stain.Color | ((uint)index << 24);
RgbaColor = SeColorToRgba(stain.Color);
}
public static readonly Stain None = new("None");
// Only used by None.
private Stain(string name)
{
Name = name;
Name = name;
_seColorId = 0;
RgbaColor = 0;
RgbaColor = 0;
}
}

View file

@ -15,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.GameData", "Glamo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj", "{9BEE2336-AA93-4669-8EEA-4756B3B2D024}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "..\Penumbra\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{FECEDB39-C103-4333-82A6-A422BDC51EEE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OtterGui", "..\Penumbra\OtterGui\OtterGui.csproj", "{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}"
EndProject
Global
@ -65,18 +63,6 @@ Global
{9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|x64.Build.0 = Release|Any CPU
{9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|x86.ActiveCfg = Release|Any CPU
{9BEE2336-AA93-4669-8EEA-4756B3B2D024}.Release|x86.Build.0 = Release|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Debug|x64.ActiveCfg = Debug|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Debug|x64.Build.0 = Debug|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Debug|x86.ActiveCfg = Debug|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Debug|x86.Build.0 = Debug|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Release|Any CPU.Build.0 = Release|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Release|x64.ActiveCfg = Release|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Release|x64.Build.0 = Release|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Release|x86.ActiveCfg = Release|Any CPU
{FECEDB39-C103-4333-82A6-A422BDC51EEE}.Release|x86.Build.0 = Release|Any CPU
{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A4F7788-DB91-41B6-A264-7FD9CCACD7AA}.Debug|x64.ActiveCfg = Debug|Any CPU

View file

@ -53,270 +53,269 @@ public class GlamourerIpc : IDisposable
private void DisposeProviders()
{
ProviderGetAllCustomization?.UnregisterFunc();
ProviderGetAllCustomizationFromCharacter?.UnregisterFunc();
ProviderApplyAll?.UnregisterAction();
ProviderApplyAllToCharacter?.UnregisterAction();
ProviderApplyOnlyCustomization?.UnregisterAction();
ProviderApplyOnlyCustomizationToCharacter?.UnregisterAction();
ProviderApplyOnlyEquipment?.UnregisterAction();
ProviderApplyOnlyEquipmentToCharacter?.UnregisterAction();
ProviderRevert?.UnregisterAction();
ProviderRevertCharacter?.UnregisterAction();
ProviderGetApiVersion?.UnregisterFunc();
// ProviderGetAllCustomization?.UnregisterFunc();
// ProviderGetAllCustomizationFromCharacter?.UnregisterFunc();
// ProviderApplyAll?.UnregisterAction();
// ProviderApplyAllToCharacter?.UnregisterAction();
// ProviderApplyOnlyCustomization?.UnregisterAction();
// ProviderApplyOnlyCustomizationToCharacter?.UnregisterAction();
// ProviderApplyOnlyEquipment?.UnregisterAction();
// ProviderApplyOnlyEquipmentToCharacter?.UnregisterAction();
// ProviderRevert?.UnregisterAction();
// ProviderRevertCharacter?.UnregisterAction();
// ProviderGetApiVersion?.UnregisterFunc();
}
private void InitializeProviders()
{
try
{
ProviderGetApiVersion = _pluginInterface.GetIpcProvider<int>(LabelProviderApiVersion);
ProviderGetApiVersion.RegisterFunc(GetApiVersion);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApiVersion}.");
}
try
{
ProviderGetAllCustomization = _pluginInterface.GetIpcProvider<string, string?>(LabelProviderGetAllCustomization);
ProviderGetAllCustomization.RegisterFunc(GetAllCustomization);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
}
try
{
ProviderGetAllCustomizationFromCharacter = _pluginInterface.GetIpcProvider<Character?, string?>(LabelProviderGetAllCustomizationFromCharacter);
ProviderGetAllCustomizationFromCharacter.RegisterFunc(GetAllCustomization);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderGetAllCustomizationFromCharacter}.");
}
try
{
ProviderApplyAll =
_pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyAll);
ProviderApplyAll.RegisterAction(ApplyAll);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyAll}.");
}
try
{
ProviderApplyAllToCharacter =
_pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyAllToCharacter);
ProviderApplyAllToCharacter.RegisterAction(ApplyAll);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyAll}.");
}
try
{
ProviderApplyOnlyCustomization =
_pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyOnlyCustomization);
ProviderApplyOnlyCustomization.RegisterAction(ApplyOnlyCustomization);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyCustomization}.");
}
try
{
ProviderApplyOnlyCustomizationToCharacter =
_pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyOnlyCustomizationToCharacter);
ProviderApplyOnlyCustomizationToCharacter.RegisterAction(ApplyOnlyCustomization);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyCustomization}.");
}
try
{
ProviderApplyOnlyEquipment =
_pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyOnlyEquipment);
ProviderApplyOnlyEquipment.RegisterAction(ApplyOnlyEquipment);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
}
try
{
ProviderApplyOnlyEquipmentToCharacter =
_pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyOnlyEquipmentToCharacter);
ProviderApplyOnlyEquipmentToCharacter.RegisterAction(ApplyOnlyEquipment);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
}
try
{
ProviderRevert =
_pluginInterface.GetIpcProvider<string, object>(LabelProviderRevert);
ProviderRevert.RegisterAction(Revert);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderRevert}.");
}
try
{
ProviderRevertCharacter =
_pluginInterface.GetIpcProvider<Character?, object>(LabelProviderRevertCharacter);
ProviderRevertCharacter.RegisterAction(Revert);
}
catch (Exception ex)
{
PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderRevert}.");
}
//try
//{
// ProviderGetApiVersion = _pluginInterface.GetIpcProvider<int>(LabelProviderApiVersion);
// ProviderGetApiVersion.RegisterFunc(GetApiVersion);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApiVersion}.");
//}
//
//try
//{
// ProviderGetAllCustomization = _pluginInterface.GetIpcProvider<string, string?>(LabelProviderGetAllCustomization);
// ProviderGetAllCustomization.RegisterFunc(GetAllCustomization);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
//}
//
//try
//{
// ProviderGetAllCustomizationFromCharacter = _pluginInterface.GetIpcProvider<Character?, string?>(LabelProviderGetAllCustomizationFromCharacter);
// ProviderGetAllCustomizationFromCharacter.RegisterFunc(GetAllCustomization);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderGetAllCustomizationFromCharacter}.");
//}
//
//try
//{
// ProviderApplyAll =
// _pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyAll);
// ProviderApplyAll.RegisterAction(ApplyAll);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyAll}.");
//}
//
//try
//{
// ProviderApplyAllToCharacter =
// _pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyAllToCharacter);
// ProviderApplyAllToCharacter.RegisterAction(ApplyAll);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyAll}.");
//}
//
//try
//{
// ProviderApplyOnlyCustomization =
// _pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyOnlyCustomization);
// ProviderApplyOnlyCustomization.RegisterAction(ApplyOnlyCustomization);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyCustomization}.");
//}
//
//try
//{
// ProviderApplyOnlyCustomizationToCharacter =
// _pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyOnlyCustomizationToCharacter);
// ProviderApplyOnlyCustomizationToCharacter.RegisterAction(ApplyOnlyCustomization);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyCustomization}.");
//}
//
//try
//{
// ProviderApplyOnlyEquipment =
// _pluginInterface.GetIpcProvider<string, string, object>(LabelProviderApplyOnlyEquipment);
// ProviderApplyOnlyEquipment.RegisterAction(ApplyOnlyEquipment);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
//}
//
//try
//{
// ProviderApplyOnlyEquipmentToCharacter =
// _pluginInterface.GetIpcProvider<string, Character?, object>(LabelProviderApplyOnlyEquipmentToCharacter);
// ProviderApplyOnlyEquipmentToCharacter.RegisterAction(ApplyOnlyEquipment);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderApplyOnlyEquipment}.");
//}
//
//try
//{
// ProviderRevert =
// _pluginInterface.GetIpcProvider<string, object>(LabelProviderRevert);
// ProviderRevert.RegisterAction(Revert);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderRevert}.");
//}
//
//try
//{
// ProviderRevertCharacter =
// _pluginInterface.GetIpcProvider<Character?, object>(LabelProviderRevertCharacter);
// ProviderRevertCharacter.RegisterAction(Revert);
//}
//catch (Exception ex)
//{
// PluginLog.Error(ex, $"Error registering IPC provider for {LabelProviderRevert}.");
//}
}
private static int GetApiVersion()
=> CurrentApiVersion;
private void ApplyAll(string customization, string characterName)
{
var save = CharacterSave.FromString(customization);
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() != characterName)
continue;
var player = (Character)gameObject;
Glamourer.RevertableDesigns.Revert(player);
save.Apply(player);
Glamourer.Penumbra.UpdateCharacters(player, null);
break;
}
}
private void ApplyAll(string customization, Character? character)
{
if (character == null)
return;
var save = CharacterSave.FromString(customization);
Glamourer.RevertableDesigns.Revert(character);
save.Apply(character);
Glamourer.Penumbra.UpdateCharacters(character, null);
}
private void ApplyOnlyCustomization(string customization, string characterName)
{
var save = CharacterSave.FromString(customization);
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() != characterName)
continue;
var player = (Character)gameObject;
Glamourer.RevertableDesigns.Revert(player);
save.ApplyOnlyCustomizations(player);
Glamourer.Penumbra.UpdateCharacters(player, null);
break;
}
}
private void ApplyOnlyCustomization(string customization, Character? character)
{
if (character == null)
return;
var save = CharacterSave.FromString(customization);
Glamourer.RevertableDesigns.Revert(character);
save.ApplyOnlyCustomizations(character);
Glamourer.Penumbra.UpdateCharacters(character, null);
}
private void ApplyOnlyEquipment(string customization, string characterName)
{
var save = CharacterSave.FromString(customization);
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() != characterName)
continue;
var player = (Character)gameObject;
Glamourer.RevertableDesigns.Revert(player);
save.ApplyOnlyEquipment(player);
Glamourer.Penumbra.UpdateCharacters(player, null);
break;
}
}
private void ApplyOnlyEquipment(string customization, Character? character)
{
if (character == null)
return;
var save = CharacterSave.FromString(customization);
Glamourer.RevertableDesigns.Revert(character);
save.ApplyOnlyEquipment(character);
Glamourer.Penumbra.UpdateCharacters(character, null);
}
private void Revert(string characterName)
{
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() != characterName)
continue;
var player = (Character)gameObject;
Glamourer.RevertableDesigns.Revert(player);
Glamourer.Penumbra.UpdateCharacters(player, null);
return;
}
Glamourer.RevertableDesigns.RevertByNameWithoutApplication(characterName);
}
private void Revert(Character? character)
{
if (character == null)
return;
Glamourer.RevertableDesigns.Revert(character);
Glamourer.Penumbra.UpdateCharacters(character, null);
}
private string? GetAllCustomization(Character? character)
{
if (character == null)
return null;
CharacterSave save = new CharacterSave();
save.LoadCharacter(character);
return save.ToBase64();
}
private string? GetAllCustomization(string characterName)
{
CharacterSave save = null!;
foreach (var gameObject in _objectTable)
{
if (gameObject.Name.ToString() != characterName)
continue;
var player = (Character)gameObject;
save = new CharacterSave();
save.LoadCharacter(player);
break;
}
return save?.ToBase64() ?? null;
}
//private static int GetApiVersion()
// => CurrentApiVersion;
//
//private void ApplyAll(string customization, string characterName)
//{
// var save = CharacterSave.FromString(customization);
// foreach (var gameObject in _objectTable)
// {
// if (gameObject.Name.ToString() != characterName)
// continue;
//
// var player = (Character)gameObject;
// Glamourer.RevertableDesigns.Revert(player);
// save.Apply(player);
// Glamourer.Penumbra.UpdateCharacters(player, null);
// break;
// }
//}
//
//private void ApplyAll(string customization, Character? character)
//{
// if (character == null)
// return;
// var save = CharacterSave.FromString(customization);
// Glamourer.RevertableDesigns.Revert(character);
// save.Apply(character);
// Glamourer.Penumbra.UpdateCharacters(character, null);
//}
//
//private void ApplyOnlyCustomization(string customization, string characterName)
//{
// var save = CharacterSave.FromString(customization);
// foreach (var gameObject in _objectTable)
// {
// if (gameObject.Name.ToString() != characterName)
// continue;
//
// var player = (Character)gameObject;
// Glamourer.RevertableDesigns.Revert(player);
// save.ApplyOnlyCustomizations(player);
// Glamourer.Penumbra.UpdateCharacters(player, null);
// break;
// }
//}
//
//private void ApplyOnlyCustomization(string customization, Character? character)
//{
// if (character == null)
// return;
// var save = CharacterSave.FromString(customization);
// Glamourer.RevertableDesigns.Revert(character);
// save.ApplyOnlyCustomizations(character);
// Glamourer.Penumbra.UpdateCharacters(character, null);
//}
//
//private void ApplyOnlyEquipment(string customization, string characterName)
//{
// var save = CharacterSave.FromString(customization);
// foreach (var gameObject in _objectTable)
// {
// if (gameObject.Name.ToString() != characterName)
// continue;
//
// var player = (Character)gameObject;
// Glamourer.RevertableDesigns.Revert(player);
// save.ApplyOnlyEquipment(player);
// Glamourer.Penumbra.UpdateCharacters(player, null);
// break;
// }
//}
//
//private void ApplyOnlyEquipment(string customization, Character? character)
//{
// if (character == null)
// return;
// var save = CharacterSave.FromString(customization);
// Glamourer.RevertableDesigns.Revert(character);
// save.ApplyOnlyEquipment(character);
// Glamourer.Penumbra.UpdateCharacters(character, null);
//}
//
//private void Revert(string characterName)
//{
// foreach (var gameObject in _objectTable)
// {
// if (gameObject.Name.ToString() != characterName)
// continue;
//
// var player = (Character)gameObject;
// Glamourer.RevertableDesigns.Revert(player);
// Glamourer.Penumbra.UpdateCharacters(player, null);
// return;
// }
//
// Glamourer.RevertableDesigns.RevertByNameWithoutApplication(characterName);
//}
//
//private void Revert(Character? character)
//{
// if (character == null)
// return;
// Glamourer.RevertableDesigns.Revert(character);
// Glamourer.Penumbra.UpdateCharacters(character, null);
//}
//
//private string? GetAllCustomization(Character? character)
//{
// if (character == null)
// return null;
//
// CharacterSave save = new CharacterSave();
// save.LoadCharacter(character);
// return save.ToBase64();
//}
//
//private string? GetAllCustomization(string characterName)
//{
// CharacterSave save = null!;
// foreach (var gameObject in _objectTable)
// {
// if (gameObject.Name.ToString() != characterName)
// continue;
//
// var player = (Character)gameObject;
// save = new CharacterSave();
// save.LoadCharacter(player);
// break;
// }
//
// return save?.ToBase64() ?? null;
//}
}

View file

@ -2,8 +2,7 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
using Dalamud.Plugin.Ipc;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Gui;
using Glamourer.Structs;
using ImGuiNET;
using Penumbra.GameData.Enums;
@ -14,17 +13,17 @@ public class PenumbraAttach : IDisposable
public const int RequiredPenumbraBreakingVersion = 4;
public const int RequiredPenumbraFeatureVersion = 0;
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;
private ICallGateSubscriber<IntPtr, (IntPtr, string)>? _drawObjectInfo;
private ICallGateSubscriber<IntPtr, string, IntPtr, IntPtr, object?>? _creatingCharacterBase;
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;
private ICallGateSubscriber<IntPtr, (IntPtr, string)>? _drawObjectInfo;
private ICallGateSubscriber<IntPtr, string, IntPtr, IntPtr, IntPtr, object?>? _creatingCharacterBase;
private readonly ICallGateSubscriber<object?> _initializedEvent;
private readonly ICallGateSubscriber<object?> _disposedEvent;
public event Action<IntPtr, IntPtr, IntPtr>? CreatingCharacterBase;
public event Action<IntPtr, IntPtr, IntPtr, IntPtr>? CreatingCharacterBase;
public PenumbraAttach(bool attach)
{
@ -61,7 +60,7 @@ public class PenumbraAttach : IDisposable
_clickSubscriber =
Dalamud.PluginInterface.GetIpcSubscriber<MouseButton, ChangedItemType, uint, object>("Penumbra.ChangedItemClick");
_creatingCharacterBase =
Dalamud.PluginInterface.GetIpcSubscriber<IntPtr, string, IntPtr, IntPtr, object?>("Penumbra.CreatingCharacterBase");
Dalamud.PluginInterface.GetIpcSubscriber<IntPtr, string, IntPtr, IntPtr, IntPtr, object?>("Penumbra.CreatingCharacterBase");
_tooltipSubscriber.Subscribe(PenumbraTooltip);
_clickSubscriber.Subscribe(PenumbraRightClick);
_creatingCharacterBase.Subscribe(SubscribeCharacterBase);
@ -73,8 +72,8 @@ public class PenumbraAttach : IDisposable
}
}
private void SubscribeCharacterBase(IntPtr gameObject, string _, IntPtr customize, IntPtr equipment)
=> CreatingCharacterBase?.Invoke(gameObject, customize, equipment);
private void SubscribeCharacterBase(IntPtr gameObject, string _, IntPtr modelId, IntPtr customize, IntPtr equipment)
=> CreatingCharacterBase?.Invoke(gameObject, modelId, customize, equipment);
public void Unattach()
{
@ -111,27 +110,30 @@ public class PenumbraAttach : IDisposable
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);
}
//var gPose = ObjectManager.GPosePlayer;
//var player = ObjectManager.Player;
//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 unsafe FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* GameObjectFromDrawObject(IntPtr drawObject)
=> (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(_drawObjectInfo?.InvokeFunc(drawObject).Item1 ?? IntPtr.Zero);
public void RedrawObject(GameObject actor, RedrawType settings, bool repeat)
public void RedrawObject(GameObject? actor, RedrawType settings, bool repeat)
{
if (actor == null)
return;
if (_redrawSubscriberObject != null)
{
try
@ -166,15 +168,13 @@ public class PenumbraAttach : IDisposable
// then manually redraw using Penumbra.
public void UpdateCharacters(Character character, Character? gPoseOriginalCharacter = null)
{
var newEquip = Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(character);
RedrawObject(character, RedrawType.Redraw, true);
// Special case for carrying over changes to the gPose player to the regular player, too.
if (gPoseOriginalCharacter == null)
return;
newEquip.Write(gPoseOriginalCharacter.Address);
Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(gPoseOriginalCharacter);
RedrawObject(gPoseOriginalCharacter, RedrawType.AfterGPose, false);
//RedrawObject(character, RedrawType.Redraw, true);
//
//// Special case for carrying over changes to the gPose player to the regular player, too.
//if (gPoseOriginalCharacter == null)
// return;
//
//newEquip.Write(gPoseOriginalCharacter.Address);
//RedrawObject(gPoseOriginalCharacter, RedrawType.AfterGPose, false);
}
}

View file

@ -1,265 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Functions = Penumbra.GameData.Util.Functions;
namespace Glamourer;
public class CharacterSaveConverter : JsonConverter
public class CharacterSaveConverter : JsonConverter<CharacterSave>
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(CharacterSave);
public override void WriteJson(JsonWriter writer, CharacterSave value, JsonSerializer serializer)
{
var s = value.ToBase64();
serializer.Serialize(writer, s);
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
public override CharacterSave ReadJson(JsonReader reader, Type objectType, CharacterSave existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
var token = JToken.Load(reader);
var s = token.ToObject<string>();
return CharacterSave.FromString(s!);
}
public override bool CanWrite
=> true;
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value != null)
{
var s = ((CharacterSave)value).ToBase64();
serializer.Serialize(writer, s);
}
}
}
[JsonConverter(typeof(CharacterSaveConverter))]
public class CharacterSave
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct CharacterSave
{
public const byte CurrentVersion = 2;
public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + CharacterCustomization.CustomizationBytes;
public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + CharacterCustomization.CustomizationBytes + 4 + 1;
[Flags]
public enum SaveFlags : byte
{
WriteCustomizations = 0x01,
IsWet = 0x02,
SetHatState = 0x04,
SetWeaponState = 0x08,
SetVisorState = 0x10,
HatState = 0x20,
WeaponState = 0x40,
VisorState = 0x80,
}
public const byte TotalSize = TotalSizeVersion2;
public const byte TotalSizeVersion1 = 1 + 1 + 2 + 56 + CustomizationData.CustomizationBytes;
public const byte TotalSizeVersion2 = 1 + 1 + 2 + 56 + CustomizationData.CustomizationBytes + 4 + 1;
private readonly byte[] _bytes = new byte[TotalSize];
public const byte CurrentVersion = 3;
public byte Version = CurrentVersion;
public SaveFlags Flags = 0;
public CharacterEquipMask Equip = 0;
public CharacterWeapon MainHand = default;
public CharacterWeapon OffHand = default;
public ushort Padding = 0;
public CharacterArmor Head = default;
public CharacterArmor Body = default;
public CharacterArmor Hands = default;
public CharacterArmor Legs = default;
public CharacterArmor Feet = default;
public CharacterArmor Ears = default;
public CharacterArmor Neck = default;
public CharacterArmor Wrist = default;
public CharacterArmor RFinger = default;
public CharacterArmor LFinger = default;
private CustomizationData CustomizationData = CustomizationData.Default;
public float Alpha = 1f;
public CharacterSave()
{
_bytes[0] = CurrentVersion;
Alpha = 1.0f;
}
{ }
public CharacterSave Copy()
public void Load(Actor actor)
{
var ret = new CharacterSave();
_bytes.CopyTo((Span<byte>)ret._bytes);
return ret;
}
if (!actor.IsHuman || actor.Pointer->GameObject.DrawObject == null)
return;
public byte Version
=> _bytes[0];
public bool WriteCustomizations
{
get => (_bytes[1] & 0x01) != 0;
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x01 : _bytes[1] & ~0x01);
}
public bool IsWet
{
get => (_bytes[1] & 0x02) != 0;
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x02 : _bytes[1] & ~0x02);
}
public bool SetHatState
{
get => (_bytes[1] & 0x04) != 0;
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x04 : _bytes[1] & ~0x04);
}
public bool SetWeaponState
{
get => (_bytes[1] & 0x08) != 0;
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x08 : _bytes[1] & ~0x08);
}
public bool SetVisorState
{
get => (_bytes[1] & 0x10) != 0;
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x10 : _bytes[1] & ~0x10);
}
public bool WriteProtected
{
get => (_bytes[1] & 0x20) != 0;
set => _bytes[1] = (byte)(value ? _bytes[1] | 0x20 : _bytes[1] & ~0x20);
}
public byte StateFlags
{
get => _bytes[64 + CharacterCustomization.CustomizationBytes];
set => _bytes[64 + CharacterCustomization.CustomizationBytes] = value;
}
public bool HatState
{
get => (StateFlags & 0x01) == 0;
set => StateFlags = (byte)(value ? StateFlags & ~0x01 : StateFlags | 0x01);
}
public bool VisorState
{
get => (StateFlags & 0x10) != 0;
set => StateFlags = (byte)(value ? StateFlags | 0x10 : StateFlags & ~0x10);
}
public bool WeaponState
{
get => (StateFlags & 0x02) == 0;
set => StateFlags = (byte)(value ? StateFlags & ~0x02 : StateFlags | 0x02);
}
public CharacterEquipMask WriteEquipment
{
get => (CharacterEquipMask)(_bytes[2] | (_bytes[3] << 8));
set
var human = (Human*)actor.Pointer->GameObject.DrawObject;
CustomizationData = *(CustomizationData*)human->CustomizeData;
fixed (void* equip = &Head)
{
_bytes[2] = (byte)((ushort)value & 0xFF);
_bytes[3] = (byte)((ushort)value >> 8);
Functions.MemCpyUnchecked(equip, human->EquipSlotData, sizeof(CharacterArmor) * 10);
}
}
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
public CharacterCustomization Customize
{
get
{
fixed (byte* ptr = &_bytes[60 + CharacterCustomization.CustomizationBytes])
fixed (CustomizationData* ptr = &CustomizationData)
{
return *(float*)ptr;
return new CharacterCustomization(ptr);
}
}
set
}
public CharacterEquip Equipment
{
get
{
fixed (byte* ptr = _bytes)
fixed (CharacterArmor* ptr = &Head)
{
*(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);
return new CharacterEquip(ptr);
}
}
}
public void Load(CharacterCustomization customization)
{
WriteCustomizations = true;
customization.WriteBytes(_bytes, 4);
}
public void Load(CharacterEquipment equipment, CharacterEquipMask mask = CharacterEquipMask.All)
{
WriteEquipment = mask;
equipment.WriteBytes(_bytes, 4 + CharacterCustomization.CustomizationBytes);
}
public string ToBase64()
=> Convert.ToBase64String(_bytes);
{
fixed (void* ptr = &this)
{
return Convert.ToBase64String(new ReadOnlySpan<byte>(ptr, sizeof(CharacterSave)));
}
}
private static void CheckSize(int length, int requiredLength)
{
@ -275,119 +128,41 @@ public class CharacterSave
$"Can not parse Base64 string into CharacterSave:\n\tInvalid value {value} in byte {idx}, should be in [{min},{max}].");
}
private static void CheckCharacterMask(byte val1, byte val2)
public static CharacterSave FromString(string data)
{
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 LoadCharacter(Character a)
{
WriteCustomizations = true;
Load(new CharacterCustomization(a));
Load(new CharacterEquipment(a));
SetHatState = true;
SetVisorState = true;
SetWeaponState = true;
StateFlags = (byte)((a.IsHatVisible() ? 0x00 : 0x01) | (a.IsVisorToggled() ? 0x10 : 0x00) | (a.IsWeaponHidden() ? 0x02 : 0x00));
IsWet = a.IsWet();
Alpha = a.Alpha();
}
public void Apply(Character a)
{
Glamourer.RevertableDesigns.Add(a);
if (WriteCustomizations)
Customizations.Write(a.Address);
if (WriteEquipment != CharacterEquipMask.None)
Equipment.Write(a.Address, WriteEquipment, WriteEquipment);
a.SetWetness(IsWet);
a.Alpha() = Alpha;
if (SetHatState)
a.SetHatVisible(HatState);
if (SetVisorState)
a.SetVisorToggled(VisorState);
if (SetWeaponState)
a.SetWeaponHidden(!WeaponState);
}
public void ApplyOnlyEquipment(Character a)
{
var oldState = _bytes[1];
WriteCustomizations = false;
SetHatState = false;
SetVisorState = false;
SetWeaponState = false;
Apply(a);
_bytes[1] = oldState;
}
public void ApplyOnlyCustomizations(Character a)
{
var oldState = _bytes[1];
SetHatState = false;
SetVisorState = false;
SetWeaponState = false;
var oldEquip = WriteEquipment;
WriteEquipment = CharacterEquipMask.None;
Apply(a);
_bytes[1] = oldState;
WriteEquipment = oldEquip;
}
public void Load(string base64)
{
var bytes = Convert.FromBase64String(base64);
switch (bytes[0])
var bytes = Convert.FromBase64String(data);
var ret = new CharacterSave();
fixed (byte* ptr = bytes)
{
case 1:
CheckSize(bytes.Length, TotalSizeVersion1);
CheckRange(2, bytes[1], 0, 1);
Alpha = 1.0f;
bytes[0] = CurrentVersion;
break;
case 2:
CheckSize(bytes.Length, TotalSizeVersion2);
CheckRange(2, bytes[1], 0, 0x3F);
break;
default: throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid Version {bytes[0]}.");
}
CheckCharacterMask(bytes[2], bytes[3]);
bytes.CopyTo(_bytes, 0);
}
public static CharacterSave FromString(string base64)
{
var ret = new CharacterSave();
ret.Load(base64);
return ret;
}
public unsafe ref CharacterCustomization Customizations
{
get
{
fixed (byte* ptr = _bytes)
switch (bytes[0])
{
return ref *(CharacterCustomization*)(ptr + 4);
case 1:
CheckSize(bytes.Length, TotalSizeVersion1);
CheckRange(2, bytes[1], 0, 1);
Functions.MemCpyUnchecked(&ret, ptr, TotalSizeVersion1);
ret.Version = CurrentVersion;
ret.Alpha = 1f;
break;
case 2:
CheckSize(bytes.Length, TotalSizeVersion2);
CheckRange(2, bytes[1], 0, 0x3F);
Functions.MemCpyUnchecked(&ret, ptr, TotalSizeVersion2 - 1);
ret.Flags &= ~SaveFlags.HatState;
if ((bytes.Last() & 0x01) != 0)
ret.Flags |= SaveFlags.HatState;
if ((bytes.Last() & 0x02) != 0)
ret.Flags |= SaveFlags.WeaponState;
if ((bytes.Last() & 0x04) != 0)
ret.Flags |= SaveFlags.VisorState;
break;
case 3:
CheckSize(bytes.Length, sizeof(CharacterSave));
Functions.MemCpyUnchecked(&ret, ptr, sizeof(CharacterSave));
break;
default: throw new Exception($"Can not parse Base64 string into CharacterSave:\n\tInvalid Version {bytes[0]}.");
}
}
}
public CharacterEquipment Equipment
{
get
{
var ret = new CharacterEquipment();
ret.FromBytes(_bytes, 4 + CharacterCustomization.CustomizationBytes);
return ret;
}
return ret;
}
}

View file

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

View file

@ -1,22 +1,16 @@
using Glamourer.FileSystem;
using System;
namespace Glamourer.Designs
namespace Glamourer.Designs;
public class Design
{
public class Design : IFileSystemBase
{
public Folder Parent { get; set; }
public string Name { get; set; }
public string Name { get; }
public bool ReadOnly;
public CharacterSave Data { get; set; }
public DateTimeOffset CreationDate { get; }
public DateTimeOffset LastUpdateDate { get; }
public CharacterSave Data { get; }
internal Design(Folder parent, string name)
{
Parent = parent;
Name = name;
Data = new CharacterSave();
}
public override string ToString()
=> Name;
}
public override string ToString()
=> Name;
}

View file

@ -3,157 +3,155 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using Glamourer.FileSystem;
using Newtonsoft.Json;
namespace Glamourer.Designs
namespace Glamourer.Designs;
public class DesignManager
{
public class DesignManager
{
public const string FileName = "Designs.json";
private readonly FileInfo _saveFile;
public SortedList<string, CharacterSave> Designs = null!;
public FileSystem.FileSystem FileSystem { get; } = new();
public DesignManager()
{
var saveFolder = new DirectoryInfo(Dalamud.PluginInterface.GetPluginConfigDirectory());
if (!saveFolder.Exists)
Directory.CreateDirectory(saveFolder.FullName);
_saveFile = new FileInfo(Path.Combine(saveFolder.FullName, FileName));
LoadFromFile();
}
private void BuildStructure()
{
FileSystem.Clear();
var anyChanges = false;
foreach (var (path, save) in Designs.ToArray())
{
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))
continue;
Designs.Remove(path);
Designs[fixedPath] = save;
anyChanges = true;
PluginLog.Debug($"Problem loading saved designs, {path} was renamed to {fixedPath}.");
}
catch (Exception e)
{
PluginLog.Error($"Problem loading saved designs, {path} was removed because:\n{e}");
Designs.Remove(path);
}
}
if (anyChanges)
SaveToFile();
}
private bool UpdateRoot(string oldPath, Design child)
{
var newPath = child.FullName();
if (string.Equals(newPath, oldPath, StringComparison.InvariantCultureIgnoreCase))
return false;
Designs.Remove(oldPath);
Designs[child.FullName()] = child.Data;
return true;
}
private void UpdateChild(string oldRootPath, string newRootPath, Design child)
{
var newPath = child.FullName();
var oldPath = $"{oldRootPath}{newPath.Remove(0, newRootPath.Length)}";
Designs.Remove(oldPath);
Designs[newPath] = child.Data;
}
public void DeleteAllChildren(IFileSystemBase root, bool deleteEmpty)
{
if (root is Folder f)
foreach (var child in f.AllLeaves(SortMode.Lexicographical))
Designs.Remove(child.FullName());
var fullPath = root.FullName();
root.Parent.RemoveChild(root, deleteEmpty);
Designs.Remove(fullPath);
SaveToFile();
}
public void UpdateAllChildren(string oldPath, IFileSystemBase root)
{
var changes = false;
switch (root)
{
case Design d:
changes |= UpdateRoot(oldPath, d);
break;
case Folder f:
{
var newRootPath = root.FullName();
if (!string.Equals(oldPath, newRootPath, StringComparison.InvariantCultureIgnoreCase))
{
changes = true;
foreach (var descendant in f.AllLeaves(SortMode.Lexicographical).Where(l => l is Design).Cast<Design>())
UpdateChild(oldPath, newRootPath, descendant);
}
break;
}
}
if (changes)
SaveToFile();
}
public void SaveToFile()
{
try
{
var data = JsonConvert.SerializeObject(Designs, Formatting.Indented);
File.WriteAllText(_saveFile.FullName, data);
}
catch (Exception e)
{
PluginLog.Error($"Could not write to save file {_saveFile.FullName}:\n{e}");
}
}
public void LoadFromFile()
{
_saveFile.Refresh();
SortedList<string, CharacterSave>? designs = null;
if (_saveFile.Exists)
try
{
var data = File.ReadAllText(_saveFile.FullName);
designs = JsonConvert.DeserializeObject<SortedList<string, CharacterSave>>(data);
}
catch (Exception e)
{
PluginLog.Error($"Could not load save file {_saveFile.FullName}:\n{e}");
}
if (designs == null)
{
Designs = new SortedList<string, CharacterSave>();
SaveToFile();
}
else
{
Designs = designs;
}
BuildStructure();
}
}
//public const string FileName = "Designs.json";
//private readonly FileInfo _saveFile;
//
//public SortedList<string, CharacterSave> Designs = null!;
//public FileSystem.FileSystem FileSystem { get; } = new();
//
//public DesignManager()
//{
// var saveFolder = new DirectoryInfo(Dalamud.PluginInterface.GetPluginConfigDirectory());
// if (!saveFolder.Exists)
// Directory.CreateDirectory(saveFolder.FullName);
//
// _saveFile = new FileInfo(Path.Combine(saveFolder.FullName, FileName));
//
// LoadFromFile();
//}
//
//private void BuildStructure()
//{
// FileSystem.Clear();
// var anyChanges = false;
// foreach (var (path, save) in Designs.ToArray())
// {
// 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))
// continue;
//
// Designs.Remove(path);
// Designs[fixedPath] = save;
// anyChanges = true;
// PluginLog.Debug($"Problem loading saved designs, {path} was renamed to {fixedPath}.");
// }
// catch (Exception e)
// {
// PluginLog.Error($"Problem loading saved designs, {path} was removed because:\n{e}");
// Designs.Remove(path);
// }
// }
//
// if (anyChanges)
// SaveToFile();
//}
//
//private bool UpdateRoot(string oldPath, Design child)
//{
// var newPath = child.FullName();
// if (string.Equals(newPath, oldPath, StringComparison.InvariantCultureIgnoreCase))
// return false;
//
// Designs.Remove(oldPath);
// Designs[child.FullName()] = child.Data;
// return true;
//}
//
//private void UpdateChild(string oldRootPath, string newRootPath, Design child)
//{
// var newPath = child.FullName();
// var oldPath = $"{oldRootPath}{newPath.Remove(0, newRootPath.Length)}";
// Designs.Remove(oldPath);
// Designs[newPath] = child.Data;
//}
//
//public void DeleteAllChildren(IFileSystemBase root, bool deleteEmpty)
//{
// if (root is Folder f)
// foreach (var child in f.AllLeaves(SortMode.Lexicographical))
// Designs.Remove(child.FullName());
// var fullPath = root.FullName();
// root.Parent.RemoveChild(root, deleteEmpty);
// Designs.Remove(fullPath);
//
// SaveToFile();
//}
//
//public void UpdateAllChildren(string oldPath, IFileSystemBase root)
//{
// var changes = false;
// switch (root)
// {
// case Design d:
// changes |= UpdateRoot(oldPath, d);
// break;
// case Folder f:
// {
// var newRootPath = root.FullName();
// if (!string.Equals(oldPath, newRootPath, StringComparison.InvariantCultureIgnoreCase))
// {
// changes = true;
// foreach (var descendant in f.AllLeaves(SortMode.Lexicographical).Where(l => l is Design).Cast<Design>())
// UpdateChild(oldPath, newRootPath, descendant);
// }
//
// break;
// }
// }
//
// if (changes)
// SaveToFile();
//}
//
//public void SaveToFile()
//{
// try
// {
// var data = JsonConvert.SerializeObject(Designs, Formatting.Indented);
// File.WriteAllText(_saveFile.FullName, data);
// }
// catch (Exception e)
// {
// PluginLog.Error($"Could not write to save file {_saveFile.FullName}:\n{e}");
// }
//}
//
//public void LoadFromFile()
//{
// _saveFile.Refresh();
// SortedList<string, CharacterSave>? designs = null;
// if (_saveFile.Exists)
// try
// {
// var data = File.ReadAllText(_saveFile.FullName);
// designs = JsonConvert.DeserializeObject<SortedList<string, CharacterSave>>(data);
// }
// catch (Exception e)
// {
// PluginLog.Error($"Could not load save file {_saveFile.FullName}:\n{e}");
// }
//
// if (designs == null)
// {
// Designs = new SortedList<string, CharacterSave>();
// SaveToFile();
// }
// else
// {
// Designs = designs;
// }
//
// BuildStructure();
//}
}

View file

@ -1,193 +1,188 @@
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 Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.PlayerWatch;
namespace Glamourer.Designs
namespace Glamourer.Designs;
public class FixedDesigns : IDisposable
{
public class FixedDesigns : IDisposable
//public class FixedDesign
//{
// public string Name;
// public JobGroup Jobs;
// public Design Design;
// public bool Enabled;
//
// public GlamourerConfig.FixedDesign ToSave()
// => new()
// {
// Name = Name,
// Path = Design.FullName(),
// Enabled = Enabled,
// JobGroups = Jobs.Id,
// };
//
// public FixedDesign(string name, Design design, bool enabled, JobGroup jobs)
// {
// Name = name;
// Design = design;
// Enabled = enabled;
// Jobs = jobs;
// }
//}
//
//public List<FixedDesign> Data;
//public Dictionary<string, List<FixedDesign>> EnabledDesigns;
//public readonly IReadOnlyDictionary<ushort, JobGroup> JobGroups;
//
//public bool EnableDesign(FixedDesign design)
//{
// var changes = !design.Enabled;
//
// if (!EnabledDesigns.TryGetValue(design.Name, out var designs))
// {
// EnabledDesigns[design.Name] = new List<FixedDesign> { design };
// // TODO
// changes = true;
// }
// else if (!designs.Contains(design))
// {
// designs.Add(design);
// changes = true;
// }
//
// design.Enabled = true;
// // TODO
// //if (Glamourer.Config.ApplyFixedDesigns)
// //{
// // var character =
// // CharacterFactory.Convert(Dalamud.Objects.FirstOrDefault(o
// // => o.ObjectKind == ObjectKind.Player && o.Name.ToString() == design.Name));
// // if (character != null)
// // OnPlayerChange(character);
// //}
//
// return changes;
//}
//
//public bool DisableDesign(FixedDesign design)
//{
// if (!design.Enabled)
// return false;
//
// design.Enabled = false;
// if (!EnabledDesigns.TryGetValue(design.Name, out var designs))
// return false;
// if (!designs.Remove(design))
// return false;
//
// if (designs.Count == 0)
// {
// EnabledDesigns.Remove(design.Name);
// // TODO
// }
//
// return true;
//}
//
//public FixedDesigns(DesignManager designs)
//{
// JobGroups = GameData.JobGroups(Dalamud.GameData);
// Data = new List<FixedDesign>(Glamourer.Config.FixedDesigns.Count);
// EnabledDesigns = new Dictionary<string, List<FixedDesign>>(Glamourer.Config.FixedDesigns.Count);
// var changes = false;
// for (var i = 0; i < Glamourer.Config.FixedDesigns.Count; ++i)
// {
// var save = Glamourer.Config.FixedDesigns[i];
// if (designs.FileSystem.Find(save.Path, out var d) && d is Design design)
// {
// if (!JobGroups.TryGetValue((ushort)save.JobGroups, out var jobGroup))
// jobGroup = JobGroups[1];
// Data.Add(new FixedDesign(save.Name, design, save.Enabled, jobGroup));
// if (save.Enabled)
// changes |= EnableDesign(Data.Last());
// }
// else
// {
// PluginLog.Warning($"{save.Path} does not exist anymore, removing {save.Name} from fixed designs.");
// Glamourer.Config.FixedDesigns.RemoveAt(i--);
// changes = true;
// }
// }
//
// if (changes)
// Glamourer.Config.Save();
//}
//
//private void OnPlayerChange(Character character)
//{
// //var name = character.Name.ToString();
// //if (!EnabledDesigns.TryGetValue(name, out var designs))
// // return;
// //
// //var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(character.ClassJob.Id));
// //if (design == null)
// // return;
// //
// //PluginLog.Debug("Redrawing {CharacterName} with {DesignName} for job {JobGroup}.", name, design.Design.FullName(),
// // design.Jobs.Name);
// //design.Design.Data.Apply(character);
// //Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(character);
// //Glamourer.Penumbra.RedrawObject(character, RedrawType.Redraw, false);
//}
//
//public void Add(string name, Design design, JobGroup group, bool enabled = false)
//{
// Data.Add(new FixedDesign(name, design, enabled, group));
// Glamourer.Config.FixedDesigns.Add(Data.Last().ToSave());
//
// if (enabled)
// EnableDesign(Data.Last());
//
// Glamourer.Config.Save();
//}
//
//public void Remove(FixedDesign design)
//{
// var idx = Data.IndexOf(design);
// if (idx < 0)
// return;
//
// Data.RemoveAt(idx);
// Glamourer.Config.FixedDesigns.RemoveAt(idx);
// if (design.Enabled)
// {
// EnabledDesigns.Remove(design.Name);
// // TODO
// }
//
// Glamourer.Config.Save();
//}
//
//public void Move(FixedDesign design, int newIdx)
//{
// if (newIdx < 0)
// newIdx = 0;
// if (newIdx >= Data.Count)
// newIdx = Data.Count - 1;
//
// var idx = Data.IndexOf(design);
// if (idx < 0 || idx == newIdx)
// return;
//
// Data.RemoveAt(idx);
// Data.Insert(newIdx, design);
// Glamourer.Config.FixedDesigns.RemoveAt(idx);
// Glamourer.Config.FixedDesigns.Insert(newIdx, design.ToSave());
// Glamourer.Config.Save();
//}
//
public void Dispose()
{
public class FixedDesign
{
public string Name;
public JobGroup Jobs;
public Design Design;
public bool Enabled;
public GlamourerConfig.FixedDesign ToSave()
=> new()
{
Name = Name,
Path = Design.FullName(),
Enabled = Enabled,
JobGroups = Jobs.Id,
};
public FixedDesign(string name, Design design, bool enabled, JobGroup jobs)
{
Name = name;
Design = design;
Enabled = enabled;
Jobs = jobs;
}
}
public List<FixedDesign> Data;
public Dictionary<string, List<FixedDesign>> EnabledDesigns;
public readonly IReadOnlyDictionary<ushort, JobGroup> JobGroups;
public bool EnableDesign(FixedDesign design)
{
var changes = !design.Enabled;
if (!EnabledDesigns.TryGetValue(design.Name, out var designs))
{
EnabledDesigns[design.Name] = new List<FixedDesign> { design };
Glamourer.PlayerWatcher.AddPlayerToWatch(design.Name);
changes = true;
}
else if (!designs.Contains(design))
{
designs.Add(design);
changes = true;
}
design.Enabled = true;
if (Glamourer.Config.ApplyFixedDesigns)
{
var character =
CharacterFactory.Convert(Dalamud.Objects.FirstOrDefault(o
=> o.ObjectKind == ObjectKind.Player && o.Name.ToString() == design.Name));
if (character != null)
OnPlayerChange(character);
}
return changes;
}
public bool DisableDesign(FixedDesign design)
{
if (!design.Enabled)
return false;
design.Enabled = false;
if (!EnabledDesigns.TryGetValue(design.Name, out var designs))
return false;
if (!designs.Remove(design))
return false;
if (designs.Count == 0)
{
EnabledDesigns.Remove(design.Name);
Glamourer.PlayerWatcher.RemovePlayerFromWatch(design.Name);
}
return true;
}
public FixedDesigns(DesignManager designs)
{
JobGroups = GameData.JobGroups(Dalamud.GameData);
Data = new List<FixedDesign>(Glamourer.Config.FixedDesigns.Count);
EnabledDesigns = new Dictionary<string, List<FixedDesign>>(Glamourer.Config.FixedDesigns.Count);
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)
{
if (!JobGroups.TryGetValue((ushort) save.JobGroups, out var jobGroup))
jobGroup = JobGroups[1];
Data.Add(new FixedDesign(save.Name, design, save.Enabled, jobGroup));
if (save.Enabled)
changes |= EnableDesign(Data.Last());
}
else
{
PluginLog.Warning($"{save.Path} does not exist anymore, removing {save.Name} from fixed designs.");
Glamourer.Config.FixedDesigns.RemoveAt(i--);
changes = true;
}
}
if (changes)
Glamourer.Config.Save();
}
private void OnPlayerChange(Character character)
{
//var name = character.Name.ToString();
//if (!EnabledDesigns.TryGetValue(name, out var designs))
// return;
//
//var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(character.ClassJob.Id));
//if (design == null)
// return;
//
//PluginLog.Debug("Redrawing {CharacterName} with {DesignName} for job {JobGroup}.", name, design.Design.FullName(),
// design.Jobs.Name);
//design.Design.Data.Apply(character);
//Glamourer.PlayerWatcher.UpdatePlayerWithoutEvent(character);
//Glamourer.Penumbra.RedrawObject(character, RedrawType.Redraw, false);
}
public void Add(string name, Design design, JobGroup group, bool enabled = false)
{
Data.Add(new FixedDesign(name, design, enabled, group));
Glamourer.Config.FixedDesigns.Add(Data.Last().ToSave());
if (enabled)
EnableDesign(Data.Last());
Glamourer.Config.Save();
}
public void Remove(FixedDesign design)
{
var idx = Data.IndexOf(design);
if (idx < 0)
return;
Data.RemoveAt(idx);
Glamourer.Config.FixedDesigns.RemoveAt(idx);
if (design.Enabled)
{
EnabledDesigns.Remove(design.Name);
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();
}
//Glamourer.Config.FixedDesigns = Data.Select(d => d.ToSave()).ToList();
//Glamourer.Config.Save();
}
}

View file

@ -1,42 +1,40 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types;
namespace Glamourer.Designs
namespace Glamourer.Designs;
public class RevertableDesigns
{
public class RevertableDesigns
public readonly Dictionary<string, CharacterSave> Saves = new();
public bool Add(Character actor)
{
public readonly ConcurrentDictionary<string, CharacterSave> Saves = new();
//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 Add(Character actor)
{
var name = actor.Name.ToString();
if (Saves.TryGetValue(name, out var save))
return false;
public bool RevertByNameWithoutApplication(string actorName)
{
if (!Saves.ContainsKey(actorName))
return false;
save = new CharacterSave();
save.LoadCharacter(actor);
Saves[name] = save;
return true;
}
Saves.Remove(actorName);
return true;
}
public bool RevertByNameWithoutApplication(string actorName)
{
if (!Saves.ContainsKey(actorName))
return false;
Saves.Remove(actorName, out _);
return true;
}
public bool Revert(Character actor)
{
if (!Saves.TryGetValue(actor.Name.ToString(), out var save))
return false;
save.Apply(actor);
Saves.Remove(actor.Name.ToString(), out _);
return true;
}
public bool Revert(Character actor)
{
//if (!Saves.TryGetValue(actor.Name.ToString(), out var save))
// return false;
//
//save.Apply(actor);
//Saves.Remove(actor.Name.ToString());
return true;
}
}

View file

@ -1,153 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Glamourer.FileSystem
{
public class FileSystem
{
public Folder Root { get; } = Folder.CreateRoot();
public void Clear()
=> Root.Children.Clear();
// Find a specific child by its path from Root.
// Returns true if the folder was found, and false if not.
// The out parameter will contain the furthest existing folder.
public bool Find(string path, out IFileSystemBase child)
{
var split = Split(path);
var folder = Root;
child = Root;
foreach (var part in split)
{
if (!folder.FindChild(part, out var c))
{
child = folder;
return false;
}
child = c;
if (c is not Folder f)
return part == split.Last();
folder = f;
}
return true;
}
public Folder CreateAllFolders(IEnumerable<string> names)
{
var last = Root;
foreach (var name in names)
last = last.FindOrCreateSubFolder(name).Item1;
return last;
}
public (Folder, string) CreateAllFolders(string path)
{
if (!path.Any())
return (Root, string.Empty);
var split = Split(path);
if (split.Length == 1)
return (Root, path);
return (CreateAllFolders(split.Take(split.Length - 1)), split.Last());
}
public bool Rename(IFileSystemBase child, string newName)
{
if (ReferenceEquals(child, Root))
throw new InvalidOperationException("Can not rename root.");
newName = FixName(newName);
if (child.Name == newName)
return false;
if (child.Parent.FindChild(newName, out var preExisting))
{
if (MergeIfFolders(child, preExisting, false))
return true;
throw new Exception($"Can not rename {child.Name} in {child.Parent.FullName()} to {newName} because {newName} already exists.");
}
var parent = child.Parent;
parent.RemoveChildIgnoreEmpty(child);
child.Name = newName;
parent.FindOrAddChild(child);
return true;
}
public bool Move(IFileSystemBase child, Folder newParent, bool deleteEmpty)
{
var oldParent = child.Parent;
if (ReferenceEquals(newParent, oldParent))
return false;
// Moving into its own subfolder or itself is not allowed.
if (child.IsFolder(out var f)
&& (ReferenceEquals(newParent, f)
|| newParent.FullName().StartsWith(f.FullName(), StringComparison.InvariantCultureIgnoreCase)))
return false;
if (newParent.FindChild(child.Name, out var conflict))
{
if (MergeIfFolders(child, conflict, deleteEmpty))
return true;
throw new Exception($"Can not move {child.Name} into {newParent.FullName()} because {conflict.FullName()} already exists.");
}
oldParent.RemoveChild(child, deleteEmpty);
newParent.FindOrAddChild(child);
return true;
}
public bool Merge(Folder source, Folder target, bool deleteEmpty)
{
if (ReferenceEquals(source, target))
return false;
if (!source.Children.Any())
{
if (deleteEmpty)
{
source.Parent.RemoveChild(source, true);
return true;
}
return false;
}
while (source.Children.Count > 0)
Move(source.Children.First(), target, deleteEmpty); // Can throw.
source.Parent.RemoveChild(source, deleteEmpty);
return true;
}
private bool MergeIfFolders(IFileSystemBase source, IFileSystemBase target, bool deleteEmpty)
{
if (source is Folder childF && target.IsFolder(out var preF))
{
Merge(childF, preF, deleteEmpty);
return true;
}
return false;
}
private static string[] Split(string path)
=> path.Split(new[]
{
'/',
}, StringSplitOptions.RemoveEmptyEntries);
private static string FixName(string name)
=> name.Replace('/', '\\');
}
}

View file

@ -1,56 +0,0 @@
using System;
using Dalamud.Logging;
using ImGuiNET;
namespace Glamourer.FileSystem
{
public static class FileSystemImGui
{
public const string DraggedObjectLabel = "FSDrag";
private static unsafe bool IsDropping(string name)
=> ImGui.AcceptDragDropPayload(name).NativePtr != null;
private static IFileSystemBase? _draggedObject;
public static bool DragDropTarget(FileSystem fs, IFileSystemBase child, out string oldPath, out IFileSystemBase? draggedChild)
{
oldPath = string.Empty;
draggedChild = null;
var ret = false;
if (!ImGui.BeginDragDropTarget())
return ret;
if (IsDropping(DraggedObjectLabel))
{
if (_draggedObject != null)
try
{
oldPath = _draggedObject.FullName();
draggedChild = _draggedObject;
ret = fs.Move(_draggedObject, child.IsFolder(out var folder) ? folder : child.Parent, false);
}
catch (Exception e)
{
PluginLog.Error($"Could not drag {_draggedObject.Name} onto {child.FullName()}:\n{e}");
}
_draggedObject = null;
}
ImGui.EndDragDropTarget();
return ret;
}
public static void DragDropSource(IFileSystemBase child)
{
if (!ImGui.BeginDragDropSource())
return;
ImGui.SetDragDropPayload(DraggedObjectLabel, IntPtr.Zero, 0);
ImGui.Text($"Moving {child.Name}...");
_draggedObject = child;
ImGui.EndDragDropSource();
}
}
}

View file

@ -1,178 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace Glamourer.FileSystem
{
public enum SortMode
{
FoldersFirst = 0x00,
Lexicographical = 0x01,
}
public class Folder : IFileSystemBase
{
public Folder Parent { get; set; }
public string Name { get; set; }
public readonly List<IFileSystemBase> Children = new();
public Folder(Folder parent, string name)
{
Parent = parent;
Name = name;
}
public override string ToString()
=> this.FullName();
// Return the number of all leaves with this folder in their path.
public int TotalDescendantLeaves()
{
var sum = 0;
foreach (var child in Children)
{
switch (child)
{
case Folder f:
sum += f.TotalDescendantLeaves();
break;
case Link l:
sum += l.Data is Folder fl ? fl.TotalDescendantLeaves() : 1;
break;
default:
++sum;
break;
}
}
return sum;
}
// Return all descendant mods in the specified order.
public IEnumerable<IFileSystemBase> AllLeaves(SortMode mode)
{
return GetSortedEnumerator(mode).SelectMany(f =>
{
if (f.IsFolder(out var folder))
return folder.AllLeaves(mode);
return new[]
{
f,
};
});
}
public IEnumerable<IFileSystemBase> AllChildren(SortMode mode)
=> GetSortedEnumerator(mode);
// Get an enumerator for actually sorted objects instead of folder-first objects.
private IEnumerable<IFileSystemBase> GetSortedEnumerator(SortMode mode)
{
switch (mode)
{
case SortMode.FoldersFirst:
foreach (var child in Children.Where(c => c.IsFolder()))
yield return child;
foreach (var child in Children.Where(c => c.IsLeaf()))
yield return child;
break;
case SortMode.Lexicographical:
foreach (var child in Children)
yield return child;
break;
default: throw new InvalidEnumArgumentException();
}
}
internal static Folder CreateRoot()
=> new(null!, "");
// Find a subfolder by name. Returns true and sets folder to it if it exists.
public bool FindChild(string name, out IFileSystemBase ret)
{
var idx = Search(name);
ret = idx >= 0 ? Children[idx] : this;
return idx >= 0;
}
// Checks if an equivalent child to child already exists and returns its index.
// If it does not exist, inserts child as a child and returns the new index.
// Also sets this as childs parent.
public int FindOrAddChild(IFileSystemBase child)
{
var idx = Search(child);
if (idx >= 0)
return idx;
idx = ~idx;
Children.Insert(idx, child);
child.Parent = this;
return idx;
}
// Checks if an equivalent child to child already exists and throws if it does.
// If it does not exist, inserts child as a child and returns the new index.
// Also sets this as childs parent.
public int AddChild(IFileSystemBase child)
{
var idx = Search(child);
if (idx >= 0)
throw new Exception("Could not add child: Child of that name already exists.");
idx = ~idx;
Children.Insert(idx, child);
child.Parent = this;
return idx;
}
// Checks if a subfolder with the given name already exists and returns it and its index.
// If it does not exists, creates and inserts it and returns the new subfolder and its index.
public (Folder, int) FindOrCreateSubFolder(string name)
{
var subFolder = new Folder(this, name);
var idx = FindOrAddChild(subFolder);
var child = Children[idx];
if (!child.IsFolder(out var folder))
throw new Exception($"The child {name} already exists in {this.FullName()} but is not a folder.");
return (folder, idx);
}
// Remove child if it exists.
// If this folder is empty afterwards and deleteEmpty is true, remove it from its parent.
public void RemoveChild(IFileSystemBase child, bool deleteEmpty)
{
RemoveChildIgnoreEmpty(child);
if (deleteEmpty)
CheckEmpty();
}
private void CheckEmpty()
{
if (Children.Count == 0)
Parent?.RemoveChild(this, true);
}
// Remove a child but do not remove this folder from its parent if it is empty afterwards.
internal void RemoveChildIgnoreEmpty(IFileSystemBase folder)
{
var idx = Search(folder);
if (idx < 0)
return;
Children[idx].Parent = null!;
Children.RemoveAt(idx);
}
private int Search(string name)
=> Children.BinarySearch(new FileSystemObject(name), FolderStructureComparer.Default);
private int Search(IFileSystemBase child)
=> Children.BinarySearch(child, FolderStructureComparer.Default);
}
}

View file

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Glamourer.FileSystem
{
internal class FolderStructureComparer : IComparer<IFileSystemBase>
{
// Compare only the direct folder names since this is only used inside an enumeration of children of one folder.
public static int Cmp(IFileSystemBase? x, IFileSystemBase? y)
=> ReferenceEquals(x, y) ? 0 : string.Compare(x?.Name, y?.Name, StringComparison.InvariantCultureIgnoreCase);
public int Compare(IFileSystemBase? x, IFileSystemBase? y)
=> Cmp(x, y);
internal static readonly FolderStructureComparer Default = new();
}
public interface IFileSystemBase
{
public Folder Parent { get; set; }
public string Name { get; set; }
}
public static class FileSystemExtensions
{
public static string FullName(this IFileSystemBase data)
=> data.Parent?.Name.Any() ?? false ? $"{data.Parent.FullName()}/{data.Name}" : data.Name;
public static bool IsLeaf(this IFileSystemBase data)
=> data is not Folder && data is not Link { Data: Folder };
public static bool IsFolder(this IFileSystemBase data)
=> data.IsFolder(out _);
public static bool IsFolder(this IFileSystemBase data, out Folder folder)
{
switch (data)
{
case Folder f:
folder = f;
return true;
case Link { Data: Folder fl }:
folder = fl;
return true;
default:
folder = null!;
return false;
}
}
}
public class FileSystemObject : IFileSystemBase
{
public FileSystemObject(string name)
=> Name = name;
public Folder Parent { get; set; } = null!;
public string Name { get; set; }
public string FullName()
=> Name;
}
}

View file

@ -1,20 +0,0 @@
using Glamourer.Designs;
namespace Glamourer.FileSystem
{
public class Link : IFileSystemBase
{
public Folder Parent { get; set; }
public string Name { get; set; }
public IFileSystemBase Data { get; }
public Link(Folder parent, string name, IFileSystemBase data)
{
Parent = parent;
Name = name;
Data = data;
}
}
}

View file

@ -1,262 +1,173 @@
using System;
using System.Linq;
using System.Collections;
using System.ComponentModel.Design;
using System.Net.Sockets;
using System.Reflection;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using System.Runtime.InteropServices.ComTypes;
using Dalamud.Data;
using Dalamud.Game.ClientState.JobGauge.Enums;
using Dalamud.Game.Command;
using Dalamud.Hooking;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.Attributes;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Component.Excel;
using Glamourer.Api;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.FileSystem;
using Glamourer.Gui;
using ImGuiNET;
using Penumbra.GameData.ByteString;
using Lumina.Data.Parsing;
using Microsoft.VisualBasic.CompilerServices;
using OtterGui.Table;
using Penumbra.GameData.Enums;
using Penumbra.PlayerWatch;
using Penumbra.GameData.Structs;
using static FFXIVClientStructs.FFXIV.Client.UI.Misc.RaptureMacroModule;
using static System.Collections.Specialized.BitVector32;
using static System.Reflection.Metadata.BlobBuilder;
using Race = Lumina.Excel.GeneratedSheets.Race;
namespace Glamourer;
public unsafe class FixedDesignManager : IDisposable
{
public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, uint* data);
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A",
DetourName = nameof(FlagSlotForUpdateDetour))]
public Hook<FlagSlotForUpdateDelegate>? FlagSlotForUpdateHook;
public readonly FixedDesigns FixedDesigns;
public FixedDesignManager(DesignManager designs)
{
SignatureHelper.Initialise(this);
FixedDesigns = new FixedDesigns(designs);
if (Glamourer.Config.ApplyFixedDesigns)
Enable();
}
public void Enable()
{
FlagSlotForUpdateHook?.Enable();
Glamourer.Penumbra.CreatingCharacterBase += ApplyFixedDesign;
}
public void Disable()
{
FlagSlotForUpdateHook?.Disable();
Glamourer.Penumbra.CreatingCharacterBase -= ApplyFixedDesign;
}
public void Dispose()
{
FlagSlotForUpdateHook?.Dispose();
}
private void ApplyFixedDesign(IntPtr addr, IntPtr customize, IntPtr equipData)
{
var human = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)addr;
if (human->GameObject.ObjectKind is (byte)ObjectKind.EventNpc or (byte)ObjectKind.BattleNpc or (byte)ObjectKind.Player
&& human->ModelCharaId == 0)
{
var name = new Utf8String(human->GameObject.Name).ToString();
if (FixedDesigns.EnabledDesigns.TryGetValue(name, out var designs))
{
var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(human->ClassJob));
if (design != null)
{
if (design.Design.Data.WriteCustomizations)
*(CharacterCustomization*)customize = design.Design.Data.Customizations;
var data = (uint*)equipData;
for (var i = 0u; i < 10; ++i)
{
var slot = i.ToEquipSlot();
if (design.Design.Data.WriteEquipment.Fits(slot))
data[i] = slot switch
{
EquipSlot.Head => design.Design.Data.Equipment.Head.Value,
EquipSlot.Body => design.Design.Data.Equipment.Body.Value,
EquipSlot.Hands => design.Design.Data.Equipment.Hands.Value,
EquipSlot.Legs => design.Design.Data.Equipment.Legs.Value,
EquipSlot.Feet => design.Design.Data.Equipment.Feet.Value,
EquipSlot.Ears => design.Design.Data.Equipment.Ears.Value,
EquipSlot.Neck => design.Design.Data.Equipment.Neck.Value,
EquipSlot.Wrists => design.Design.Data.Equipment.Wrists.Value,
EquipSlot.RFinger => design.Design.Data.Equipment.RFinger.Value,
EquipSlot.LFinger => design.Design.Data.Equipment.LFinger.Value,
_ => 0,
};
}
}
}
}
}
private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slotIdx, uint* data)
{
ulong ret;
var slot = slotIdx.ToEquipSlot();
try
{
if (slot != EquipSlot.Unknown)
{
var gameObject =
(FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject);
if (gameObject != null)
{
var name = new Utf8String(gameObject->GameObject.Name).ToString();
if (FixedDesigns.EnabledDesigns.TryGetValue(name, out var designs))
{
var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(gameObject->ClassJob));
if (design != null && design.Design.Data.WriteEquipment.Fits(slot))
*data = slot switch
{
EquipSlot.Head => design.Design.Data.Equipment.Head.Value,
EquipSlot.Body => design.Design.Data.Equipment.Body.Value,
EquipSlot.Hands => design.Design.Data.Equipment.Hands.Value,
EquipSlot.Legs => design.Design.Data.Equipment.Legs.Value,
EquipSlot.Feet => design.Design.Data.Equipment.Feet.Value,
EquipSlot.Ears => design.Design.Data.Equipment.Ears.Value,
EquipSlot.Neck => design.Design.Data.Equipment.Neck.Value,
EquipSlot.Wrists => design.Design.Data.Equipment.Wrists.Value,
EquipSlot.RFinger => design.Design.Data.Equipment.RFinger.Value,
EquipSlot.LFinger => design.Design.Data.Equipment.LFinger.Value,
_ => 0,
};
}
}
}
}
finally
{
ret = FlagSlotForUpdateHook!.Original(drawObject, slotIdx, data);
}
return ret;
}
}
public class Glamourer : IDalamudPlugin
{
private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],<Name for Save>";
private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],<Name for Save>";
private const string MainCommandString = "/glamourer";
private const string ApplyCommandString = "/glamour";
public string Name
=> "Glamourer";
public static GlamourerConfig Config = null!;
public static IPlayerWatcher PlayerWatcher = null!;
public static ICustomizationManager Customization = null!;
public static FixedDesignManager FixedDesignManager = null!;
private readonly Interface _interface;
public readonly DesignManager Designs;
public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
public static RevertableDesigns RevertableDesigns = new();
public readonly GlamourerIpc GlamourerIpc;
public static readonly string CommitHash =
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "Unknown";
public static string Version = string.Empty;
public static GlamourerConfig Config = null!;
public static PenumbraAttach Penumbra = null!;
public Glamourer(DalamudPluginInterface pluginInterface)
public static ICustomizationManager Customization = null!;
public static RedrawManager RedrawManager = null!;
public static RestrictedGear RestrictedGear = null!;
private readonly WindowSystem _windowSystem = new("Glamourer");
private readonly Interface _interface;
//public readonly DesignManager Designs;
//public static RevertableDesigns RevertableDesigns = new();
//public readonly GlamourerIpc GlamourerIpc;
public unsafe 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);
GlamourerIpc = new GlamourerIpc(Dalamud.ClientState, Dalamud.Objects, Dalamud.PluginInterface);
FixedDesignManager = new FixedDesignManager(Designs);
if (!Config.ApplyFixedDesigns)
PlayerWatcher.Disable();
Customization = CustomizationManager.Create(Dalamud.PluginInterface, Dalamud.GameData, Dalamud.ClientState.ClientLanguage);
RestrictedGear = GameData.RestrictedGear(Dalamud.GameData);
var m = global::Penumbra.GameData.GameData.GetIdentifier(Dalamud.GameData, Dalamud.ClientState.ClientLanguage);
Config = GlamourerConfig.Load();
Penumbra = new PenumbraAttach(Config.AttachToPenumbra);
Dalamud.Commands.AddHandler("/glamourer", new CommandInfo(OnGlamourer)
//Designs = new DesignManager();
//GlamourerIpc = new GlamourerIpc(Dalamud.ClientState, Dalamud.Objects, Dalamud.PluginInterface);
RedrawManager = new RedrawManager();
Dalamud.Commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer)
{
HelpMessage = "Open or close the Glamourer window.",
});
Dalamud.Commands.AddHandler("/glamour", new CommandInfo(OnGlamour)
Dalamud.Commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour)
{
HelpMessage = $"Use Glamourer Functions: {HelpString}",
});
_interface = new Interface(this);
_windowSystem.AddWindow(_interface);
Dalamud.PluginInterface.UiBuilder.Draw += _windowSystem.Draw;
var x = 0x00011000u;
//FixedDesignManager.Flag((Human*)((Actor)Dalamud.ClientState.LocalPlayer?.Address).Pointer->GameObject.DrawObject, 0, &x);
}
public void Dispose()
{
RedrawManager.Dispose();
Penumbra.Dispose();
Dalamud.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_interface.Dispose();
//GlamourerIpc.Dispose();
Dalamud.Commands.RemoveHandler(ApplyCommandString);
Dalamud.Commands.RemoveHandler(MainCommandString);
}
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}");
}
}
=> _interface.Toggle();
//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()
@ -265,73 +176,62 @@ public class Glamourer : IDalamudPlugin
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()
{
FixedDesignManager.Dispose();
Penumbra.Dispose();
PlayerWatcher.Dispose();
_interface.Dispose();
GlamourerIpc.Dispose();
Dalamud.Commands.RemoveHandler("/glamour");
Dalamud.Commands.RemoveHandler("/glamourer");
//arguments = arguments.Trim();
//if (!arguments.Any())
//{
// PrintHelp();
// return;
//}
//
//var split = arguments.Split(new[]
//{
// ',',
//}, 3, StringSplitOptions.RemoveEmptyEntries);
//
//if (split.Length < 2)
//{
// PrintHelp();
// return;
//}
//
//var player = GetPlayer(split[1]) as Character;
//if (player == null)
//{
// Dalamud.Chat.Print($"Could not find object for {split[1]} or it was not a Character.");
// return;
//}
//
//switch (split[0].ToLowerInvariant())
//{
// case "copy":
// CopyToClipboard(player);
// return;
// case "apply":
// {
// if (split.Length < 3)
// {
// Dalamud.Chat.Print("Applying requires a name for the save to be applied or 'clipboard'.");
// return;
// }
//
// ApplyCommand(player, split[2]);
//
// return;
// }
// case "save":
// {
// if (split.Length < 3)
// {
// Dalamud.Chat.Print("Saving requires a name for the save.");
// return;
// }
//
// SaveCommand(player, split[2]);
// return;
// }
// default:
// PrintHelp();
// return;
//}
}
}

View file

@ -77,17 +77,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3">
<Private>false</Private>
</PackageReference>
<PackageReference Include="System.Memory" Version="4.5.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj" />
<ProjectReference Include="..\..\Penumbra\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj" />
</ItemGroup>
<ItemGroup>
@ -105,7 +100,17 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" />
</Exec>
<PropertyGroup>
<InformationalVersion>$(GitCommitHash)</InformationalVersion>
</PropertyGroup>
</Target>
<ItemGroup>
<None Update="Glamourer.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

View file

@ -0,0 +1,464 @@
using System;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui;
internal partial class Interface
{
private class ActorTab
{
private ObjectManager.ActorData _data = new(string.Empty, string.Empty, Actor.Null, false, Actor.Null);
private CharacterSave _character = new();
private Actor _nextSelect = Actor.Null;
public void Draw()
{
using var tab = ImRaii.TabItem("Actors");
if (!tab)
return;
DrawActorSelector();
if (_data.Label.Length == 0)
return;
ImGui.SameLine();
if (_data.Actor.IsHuman)
DrawActorPanel();
else
DrawMonsterPanel();
}
private void DrawActorPanel()
{
using var group = ImRaii.Group();
if (DrawCustomization(_character.Customize, _character.Equipment, !_data.Modifiable))
{
Glamourer.RedrawManager.Set(_data.Actor.Address, _character);
Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
}
if (ImGui.Button("Set Machinist Goggles"))
{
Glamourer.RedrawManager.ChangeEquip(_data.Actor.Address, EquipSlot.Head, new CharacterArmor(265, 1, 0));
}
}
private void DrawMonsterPanel()
{
using var group = ImRaii.Group();
var currentModel = (uint)_data.Actor.ModelId;
var models = GameData.Models(Dalamud.GameData);
var currentData = models.Models.TryGetValue(currentModel, out var c) ? c.FirstName : $"#{currentModel}";
using var combo = ImRaii.Combo("Model Id", currentData);
if (!combo)
return;
foreach (var (id, data) in models.Models)
{
if (ImGui.Selectable(data.FirstName, id == currentModel) || id == currentModel)
{
_data.Actor.SetModelId((int)id);
Glamourer.Penumbra.RedrawObject(_data.Actor.Character, RedrawType.Redraw, true);
}
ImGuiUtil.HoverTooltip(data.AllNames);
}
}
private LowerString _actorFilter = LowerString.Empty;
private void DrawActorSelector()
{
using var group = ImRaii.Group();
var oldSpacing = ImGui.GetStyle().ItemSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.SetNextItemWidth(_actorSelectorWidth);
LowerString.InputWithHint("##actorFilter", "Filter...", ref _actorFilter, 64);
using (var child = ImRaii.Child("##actorSelector", new Vector2(_actorSelectorWidth, -ImGui.GetFrameHeight()), true))
{
if (!child)
return;
_data.Actor = Actor.Null;
_data.GPose = Actor.Null;
_data.Modifiable = false;
style.Push(ImGuiStyleVar.ItemSpacing, oldSpacing);
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeight());
var remainder = ImGuiClip.FilteredClippedDraw(ObjectManager.GetEnumerator(), skips, CheckFilter, DrawSelectable);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
style.Pop();
}
DrawSelectionButtons();
}
private void UpdateSelection(ObjectManager.ActorData data)
{
_data = data;
_character.Load(_data.Actor);
}
private bool CheckFilter(ObjectManager.ActorData data)
{
if (_nextSelect && _nextSelect == data.Actor || data.Label == _data.Label)
UpdateSelection(data);
return data.Label.Contains(_actorFilter.Lower, StringComparison.OrdinalIgnoreCase);
}
private void DrawSelectable(ObjectManager.ActorData data)
{
var equal = data.Label == _data.Label;
if (ImGui.Selectable(data.Label, equal) && !equal)
UpdateSelection(data);
}
private void DrawSelectionButtons()
{
_nextSelect = Actor.Null;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
var buttonWidth = new Vector2(_actorSelectorWidth / 2, 0);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth
, "Select the local player character.", !ObjectManager.Player, true))
_nextSelect = _inGPose ? ObjectManager.GPosePlayer : ObjectManager.Player;
ImGui.SameLine();
Actor targetActor = Dalamud.Targets.Target?.Address;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth,
"Select the current target, if it is in the list.", _inGPose || !targetActor, true))
_nextSelect = targetActor;
}
}
private readonly ActorTab _actorTab = new();
}
//internal partial class Interface
//{
// private readonly CharacterSave _currentSave = new();
// private string _newDesignName = string.Empty;
// private bool _keyboardFocus;
// private bool _holdShift;
// private bool _holdCtrl;
// private const string DesignNamePopupLabel = "Save Design As...";
// private const uint RedHeaderColor = 0xFF1818C0;
// private const uint GreenHeaderColor = 0xFF18C018;
//
// private void DrawPlayerHeader()
// {
// var color = _player == null ? RedHeaderColor : GreenHeaderColor;
// var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
// using var c = ImRaii.PushColor(ImGuiCol.Text, color)
// .Push(ImGuiCol.Button, buttonColor)
// .Push(ImGuiCol.ButtonHovered, buttonColor)
// .Push(ImGuiCol.ButtonActive, buttonColor);
// using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
// .Push(ImGuiStyleVar.FrameRounding, 0);
// ImGui.Button($"{_currentLabel}##playerHeader", -Vector2.UnitX * 0.0001f);
// }
//
// private static void DrawCopyClipboardButton(CharacterSave save)
// {
// ImGui.PushFont(UiBuilder.IconFont);
// if (ImGui.Button(FontAwesomeIcon.Clipboard.ToIconString()))
// ImGui.SetClipboardText(save.ToBase64());
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip("Copy customization code to clipboard.");
// }
//
// private static unsafe void ConditionalApply(CharacterSave save, FFXIVClientStructs.FFXIV.Client.Game.Character.Character* player)
// {
// //if (ImGui.GetIO().KeyShift)
// // save.ApplyOnlyCustomizations(player);
// //else if (ImGui.GetIO().KeyCtrl)
// // save.ApplyOnlyEquipment(player);
// //else
// // save.Apply(player);
// }
//
// private static unsafe void ConditionalApply(CharacterSave save, Character player)
// => ConditionalApply(save, (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)player.Address);
//
// private static CharacterSave ConditionalCopy(CharacterSave save, bool shift, bool ctrl)
// {
// var copy = save.Copy();
// if (shift)
// {
// copy.Load(new CharacterEquipment());
// copy.SetHatState = false;
// copy.SetVisorState = false;
// copy.SetWeaponState = false;
// copy.WriteEquipment = CharacterEquipMask.None;
// }
// else if (ctrl)
// {
// copy.Load(CharacterCustomization.Default);
// copy.SetHatState = false;
// copy.SetVisorState = false;
// copy.SetWeaponState = false;
// copy.WriteCustomizations = false;
// }
//
// return copy;
// }
//
// private bool DrawApplyClipboardButton()
// {
// ImGui.PushFont(UiBuilder.IconFont);
// var applyButton = ImGui.Button(FontAwesomeIcon.Paste.ToIconString()) && _player != null;
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip(
// "Apply customization code from clipboard.\nHold Shift to apply only customizations.\nHold Control to apply only equipment.");
//
// if (!applyButton)
// return false;
//
// try
// {
// var text = ImGui.GetClipboardText();
// if (!text.Any())
// return false;
//
// var save = CharacterSave.FromString(text);
// ConditionalApply(save, _player!);
// }
// catch (Exception e)
// {
// PluginLog.Information($"{e}");
// return false;
// }
//
// return true;
// }
//
// private void DrawSaveDesignButton()
// {
// ImGui.PushFont(UiBuilder.IconFont);
// if (ImGui.Button(FontAwesomeIcon.Save.ToIconString()))
// OpenDesignNamePopup(DesignNameUse.SaveCurrent);
//
// ImGui.PopFont();
// ImGuiUtil.HoverTooltip("Save the current design.\nHold Shift to save only customizations.\nHold Control to save only equipment.");
//
// DrawDesignNamePopup(DesignNameUse.SaveCurrent);
// }
//
// private void DrawTargetPlayerButton()
// {
// if (ImGui.Button("Target Player"))
// Dalamud.Targets.SetTarget(_player);
// }
//
// private unsafe void DrawApplyToPlayerButton(CharacterSave save)
// {
// if (!ImGui.Button("Apply to Self"))
// return;
//
// var player = _inGPose
// ? (Character?)Dalamud.Objects[GPoseObjectId]
// : Dalamud.ClientState.LocalPlayer;
// var fallback = _inGPose ? Dalamud.ClientState.LocalPlayer : null;
// if (player == null)
// return;
//
// ConditionalApply(save, (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)player.Address);
// if (_inGPose)
// ConditionalApply(save, (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)fallback!.Address);
// Glamourer.Penumbra.UpdateCharacters(player, fallback);
// }
//
//
// private static unsafe FFXIVClientStructs.FFXIV.Client.Game.Character.Character* TransformToCustomizable(
// FFXIVClientStructs.FFXIV.Client.Game.Character.Character* actor)
// {
// if (actor == null)
// return null;
//
// if (actor->ModelCharaId == 0)
// return actor;
//
// actor->ModelCharaId = 0;
// CharacterCustomization.Default.Write(actor);
// return actor;
// }
//
// private static unsafe FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Convert(GameObject? actor)
// {
// return actor switch
// {
// null => null,
// PlayerCharacter p => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)p.Address,
// BattleChara b => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)b.Address,
// _ => actor.ObjectKind switch
// {
// ObjectKind.BattleNpc => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor.Address,
// ObjectKind.Companion => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor.Address,
// ObjectKind.Retainer => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor.Address,
// ObjectKind.EventNpc => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)actor.Address,
// _ => null,
// },
// };
// }
//
// private unsafe void DrawApplyToTargetButton(CharacterSave save)
// {
// if (!ImGui.Button("Apply to Target"))
// return;
//
// var player = TransformToCustomizable(Convert(Dalamud.Targets.Target));
// if (player == null)
// return;
//
// var fallBackCharacter = _gPoseActors.TryGetValue(new Utf8String(player->GameObject.Name).ToString(), out var f) ? f : null;
// ConditionalApply(save, player);
// if (fallBackCharacter != null)
// ConditionalApply(save, fallBackCharacter!);
// //Glamourer.Penumbra.UpdateCharacters(player, fallBackCharacter);
// }
//
// private void DrawRevertButton()
// {
// if (!ImGuiUtil.DrawDisabledButton("Revert", Vector2.Zero, string.Empty, _player == null))
// return;
//
// Glamourer.RevertableDesigns.Revert(_player!);
// var fallBackCharacter = _gPoseActors.TryGetValue(_player!.Name.ToString(), out var f) ? f : null;
// if (fallBackCharacter != null)
// Glamourer.RevertableDesigns.Revert(fallBackCharacter);
// Glamourer.Penumbra.UpdateCharacters(_player, fallBackCharacter);
// }
//
// private void SaveNewDesign(CharacterSave save)
// {
// try
// {
// var (folder, name) = _designs.FileSystem.CreateAllFolders(_newDesignName);
// if (!name.Any())
// return;
//
// var newDesign = new Design(folder, name) { Data = save };
// folder.AddChild(newDesign);
// _designs.Designs[newDesign.FullName()] = save;
// _designs.SaveToFile();
// }
// catch (Exception e)
// {
// PluginLog.Error($"Could not save new design {_newDesignName}:\n{e}");
// }
// }
//
// private unsafe void DrawMonsterPanel()
// {
// if (DrawApplyClipboardButton())
// Glamourer.Penumbra.UpdateCharacters(_player!);
//
// ImGui.SameLine();
// if (ImGui.Button("Convert to Character"))
// {
// //TransformToCustomizable(_player);
// _currentLabel = _currentLabel.Replace("(Monster)", "(NPC)");
// Glamourer.Penumbra.UpdateCharacters(_player!);
// }
//
// if (!_inGPose)
// {
// ImGui.SameLine();
// DrawTargetPlayerButton();
// }
//
// var currentModel = ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)_player!.Address)->ModelCharaId;
// using var combo = ImRaii.Combo("Model Id", currentModel.ToString());
// if (!combo)
// return;
//
// foreach (var (id, _) in _models.Skip(1))
// {
// if (!ImGui.Selectable($"{id:D6}##models", id == currentModel) || id == currentModel)
// continue;
//
// ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)_player!.Address)->ModelCharaId = 0;
// Glamourer.Penumbra.UpdateCharacters(_player!);
// }
// }
//
// private void DrawPlayerPanel()
// {
// DrawCopyClipboardButton(_currentSave);
// ImGui.SameLine();
// var changes = !_currentSave.WriteProtected && DrawApplyClipboardButton();
// ImGui.SameLine();
// DrawSaveDesignButton();
// ImGui.SameLine();
// DrawApplyToPlayerButton(_currentSave);
// if (!_inGPose)
// {
// ImGui.SameLine();
// DrawApplyToTargetButton(_currentSave);
// if (_player != null && !_currentSave.WriteProtected)
// {
// ImGui.SameLine();
// DrawTargetPlayerButton();
// }
// }
//
// var data = _currentSave;
// if (!_currentSave.WriteProtected)
// {
// ImGui.SameLine();
// DrawRevertButton();
// }
// else
// {
// ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.8f);
// data = data.Copy();
// }
//
// if (DrawCustomization(ref data.Customizations) && _player != null)
// {
// Glamourer.RevertableDesigns.Add(_player);
// _currentSave.Customizations.Write(_player.Address);
// changes = true;
// }
//
// changes |= DrawEquip(data.Equipment);
// changes |= DrawMiscellaneous(data, _player);
//
// if (_player != null && changes)
// Glamourer.Penumbra.UpdateCharacters(_player);
// if (_currentSave.WriteProtected)
// ImGui.PopStyleVar();
// }
//
// private unsafe void DrawActorPanel()
// {
// using var group = ImRaii.Group();
// DrawPlayerHeader();
// using var child = ImRaii.Child("##playerData", -Vector2.One, true);
// if (!child)
// return;
//
// if (_player == null || ((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)_player.Address)->ModelCharaId == 0)
// DrawPlayerPanel();
// else
// DrawMonsterPanel();
// }
//}

View file

@ -0,0 +1,418 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Logging;
using Glamourer.Customization;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Gui;
internal partial class Interface
{
private static byte _tempStorage;
private static CustomizationId _tempType;
private static bool DrawCustomization(CharacterCustomization customize, CharacterEquip equip, bool locked)
{
if (!ImGui.CollapsingHeader("Character Customization"))
return false;
var ret = DrawRaceGenderSelector(customize, equip, locked);
var set = Glamourer.Customization.GetList(customize.Clan, customize.Gender);
foreach (var id in set.Order[CharaMakeParams.MenuType.Percentage])
ret |= PercentageSelector(set, id, customize, locked);
Functions.IteratePairwise(set.Order[CharaMakeParams.MenuType.IconSelector], c => DrawIconSelector(set, c, customize, locked),
ImGui.SameLine);
ret |= DrawMultiIconSelector(set, customize, locked);
foreach (var id in set.Order[CharaMakeParams.MenuType.ListSelector])
ret |= DrawListSelector(set, id, customize, locked);
Functions.IteratePairwise(set.Order[CharaMakeParams.MenuType.ColorPicker], c => DrawColorPicker(set, c, customize, locked),
ImGui.SameLine);
ret |= Checkbox(set.Option(CustomizationId.HighlightsOnFlag), customize.HighlightsOn, b => customize.HighlightsOn = b, locked);
var xPos = _inputIntSize + _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
ImGui.SameLine(xPos);
ret |= Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {set.Option(CustomizationId.FacePaint)}",
customize.FacePaintReversed, b => customize.FacePaintReversed = b, locked);
ret |= Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}",
customize.SmallIris, b => customize.SmallIris = b, locked);
if (customize.Race != Race.Hrothgar)
{
ImGui.SameLine(xPos);
ret |= Checkbox(set.Option(CustomizationId.LipColor), customize.Lipstick, b => customize.Lipstick = b, locked);
}
return ret;
}
private static bool DrawRaceGenderSelector(CharacterCustomization customize, CharacterEquip equip, bool locked)
{
var ret = DrawGenderSelector(customize, equip, locked);
ImGui.SameLine();
using var group = ImRaii.Group();
ret |= DrawRaceCombo(customize, equip, locked);
var gender = Glamourer.Customization.GetName(CustomName.Gender);
var clan = Glamourer.Customization.GetName(CustomName.Clan);
ImGui.TextUnformatted($"{gender} & {clan}");
return ret;
}
private static bool DrawGenderSelector(CharacterCustomization customize, CharacterEquip equip, bool locked)
{
using var font = ImRaii.PushFont(UiBuilder.IconFont);
var icon = customize.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus;
var restricted = customize.Race == Race.Hrothgar;
if (restricted)
icon = FontAwesomeIcon.MarsDouble;
if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, restricted || locked, true))
return false;
var gender = customize.Gender == Gender.Male ? Gender.Female : Gender.Male;
return customize.ChangeGender(gender, locked ? CharacterEquip.Null : equip);
}
private static bool DrawRaceCombo(CharacterCustomization customize, CharacterEquip equip, bool locked)
{
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
ImGui.SetNextItemWidth(_raceSelectorWidth);
using var combo = ImRaii.Combo("##subRaceCombo", customize.ClanName());
if (!combo)
return false;
if (locked)
ImGui.CloseCurrentPopup();
var ret = false;
foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown
{
if (ImGui.Selectable(CustomizeExtensions.ClanName(subRace, customize.Gender), subRace == customize.Clan))
ret |= customize.ChangeRace(subRace, equip);
}
return ret;
}
private static bool Checkbox(string label, bool current, Action<bool> setter, bool locked)
{
var tmp = current;
var ret = false;
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
if (ImGui.Checkbox($"##{label}", ref tmp) && tmp == current && !locked)
{
setter(tmp);
ret = true;
}
alpha.Pop();
ImGui.SameLine();
ImGui.TextUnformatted(label);
return ret;
}
private static bool PercentageSelector(CustomizationSet set, CustomizationId id, CharacterCustomization customization, bool locked)
{
using var bigGroup = ImRaii.Group();
using var _ = ImRaii.PushId((int)id);
int value = id == _tempType ? _tempStorage : customization[id];
var count = set.Count(id);
ImGui.SetNextItemWidth(_comboSelectorSize);
var (min, max) = locked ? (value, value) : (0, count - 1);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
if (ImGui.SliderInt("##slider", ref value, min, max, string.Empty, ImGuiSliderFlags.AlwaysClamp) && !locked)
{
_tempStorage = (byte)value;
_tempType = id;
}
var ret = ImGui.IsItemDeactivatedAfterEdit();
ImGui.SameLine();
ret |= InputInt("##input", id, --value, min, max, locked);
alpha.Pop();
ImGui.SameLine();
ImGui.TextUnformatted(set.OptionName[(int)id]);
if (ret)
customization[id] = _tempStorage;
return ret;
}
private static bool InputInt(string label, CustomizationId id, int startValue, int minValue, int maxValue, bool locked)
{
var tmp = startValue + 1;
ImGui.SetNextItemWidth(_inputIntSize);
if (ImGui.InputInt(label, ref tmp, 1, 1, ImGuiInputTextFlags.EnterReturnsTrue)
&& !locked
&& tmp != startValue + 1
&& tmp >= minValue
&& tmp <= maxValue)
{
_tempType = id;
_tempStorage = (byte)(tmp - 1);
}
var ret = ImGui.IsItemDeactivatedAfterEdit() && !locked;
if (!locked)
ImGuiUtil.HoverTooltip($"Input Range: [{minValue}, {maxValue}]");
return ret;
}
private static bool DrawIconSelector(CustomizationSet set, CustomizationId id, CharacterCustomization customize, bool locked)
{
const string popupName = "Style Picker";
using var bigGroup = ImRaii.Group();
using var _ = ImRaii.PushId((int)id);
var count = set.Count(id);
var label = set.Option(id);
var current = set.DataByValue(id, _tempType == id ? _tempStorage : customize[id], out var custom);
if (current < 0)
{
label = $"{label} (Custom #{customize[id]})";
current = 0;
custom = set.Data(id, 0);
}
var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize) && !locked)
ImGui.OpenPopup(popupName);
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
ImGui.SameLine();
using var group = ImRaii.Group();
var (min, max) = locked ? (current, current) : (1, count);
var ret = InputInt("##text", id, current, min, max, locked);
if (ret)
customize[id] = set.Data(id, _tempStorage).Value;
ImGui.TextUnformatted($"{label} ({custom.Value.Value})");
ret |= DrawIconPickerPopup(popupName, set, id, customize);
return ret;
}
private static bool DrawIconPickerPopup(string label, CustomizationSet set, CustomizationId id, CharacterCustomization customize)
{
using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize);
if (!popup)
return false;
var ret = false;
var count = set.Count(id);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
for (var i = 0; i < count; ++i)
{
var custom = set.Data(id, i);
var icon = Glamourer.Customization.GetIcon(custom.IconId);
using var group = ImRaii.Group();
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
{
customize[id] = custom.Value;
ret = true;
ImGui.CloseCurrentPopup();
}
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
var text = custom.Value.ToString();
var textWidth = ImGui.CalcTextSize(text).X;
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (_iconSize.X - textWidth + 2 * ImGui.GetStyle().FramePadding.X) / 2);
ImGui.TextUnformatted(text);
group.Dispose();
if (i % 8 != 7)
ImGui.SameLine();
}
return ret;
}
private static bool DrawColorPicker(CustomizationSet set, CustomizationId id, CharacterCustomization customize, bool locked)
{
const string popupName = "Color Picker";
using var _ = ImRaii.PushId((int)id);
var ret = false;
var count = set.Count(id);
var label = set.Option(id);
var (current, custom) = GetCurrentCustomization(set, id, customize);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
if (ImGui.ColorButton($"{current + 1}##color", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None, _framedIconSize)
&& !locked)
ImGui.OpenPopup(popupName);
ImGui.SameLine();
using (var group = ImRaii.Group())
{
var (min, max) = locked ? (current, current) : (1, count);
if (InputInt("##text", id, current, min, max, locked))
{
customize[id] = set.Data(id, current).Value;
ret = true;
}
ImGui.TextUnformatted(label);
}
return ret | DrawColorPickerPopup(popupName, set, id, customize);
}
private static (int, Customization.Customization) GetCurrentCustomization(CustomizationSet set, CustomizationId id,
CharacterCustomization customize)
{
var current = set.DataByValue(id, customize[id], out var custom);
if (set.IsAvailable(id) && current < 0)
{
PluginLog.Warning($"Read invalid customization value {customize[id]} for {id}.");
current = 0;
custom = set.Data(id, 0);
}
return (current, custom!.Value);
}
private static bool DrawColorPickerPopup(string label, CustomizationSet set, CustomizationId id, CharacterCustomization customize)
{
using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize);
if (!popup)
return false;
var ret = false;
var count = set.Count(id);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
for (var i = 0; i < count; ++i)
{
var custom = set.Data(id, i);
if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)))
{
customize[id] = custom.Value;
ret = true;
ImGui.CloseCurrentPopup();
}
if (i % 8 != 7)
ImGui.SameLine();
}
return ret;
}
private static bool DrawMultiIconSelector(CustomizationSet set, CharacterCustomization customize, bool locked)
{
using var bigGroup = ImRaii.Group();
using var _ = ImRaii.PushId((int)CustomizationId.FacialFeaturesTattoos);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
var ret = DrawMultiIcons(set, customize, locked);
ImGui.SameLine();
using var group = ImRaii.Group();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() + 3 * ImGui.GetStyle().ItemSpacing.Y / 2);
int value = customize[CustomizationId.FacialFeaturesTattoos];
var (min, max) = locked ? (value, value) : (1, 256);
if (InputInt(string.Empty, CustomizationId.FacialFeaturesTattoos, value, min, max, locked))
{
customize[CustomizationId.FacialFeaturesTattoos] = (byte)value;
ret = true;
}
ImGui.TextUnformatted(set.Option(CustomizationId.FacialFeaturesTattoos));
return ret;
}
private static bool DrawMultiIcons(CustomizationSet set, CharacterCustomization customize, bool locked)
{
using var _ = ImRaii.Group();
var face = customize.Face;
if (set.Faces.Count < face)
face = 1;
var ret = false;
var count = set.Count(CustomizationId.FacialFeaturesTattoos);
for (var i = 0; i < count; ++i)
{
var enabled = customize.FacialFeature(i);
var feature = set.FacialFeature(face, i);
var icon = i == count - 1
? LegacyTattoo ?? Glamourer.Customization.GetIcon(feature.IconId)
: Glamourer.Customization.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X,
Vector4.Zero, enabled ? Vector4.One : RedTint)
&& !locked)
{
customize.FacialFeature(i, !enabled);
ret = true;
}
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 1f, !locked);
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
if (i % 4 != 3)
ImGui.SameLine();
}
return ret;
}
private static bool DrawListSelector(CustomizationSet set, CustomizationId id, CharacterCustomization customize, bool locked)
{
using var _ = ImRaii.PushId((int)id);
using var bigGroup = ImRaii.Group();
var ret = false;
int current = customize[id];
var count = set.Count(id);
ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale);
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, locked);
using (var combo = ImRaii.Combo("##combo", $"{set.Option(id)} #{current + 1}"))
{
if (combo)
for (var i = 0; i < count; ++i)
{
if (!ImGui.Selectable($"{set.Option(id)} #{i + 1}##combo", i == current) || i == current || locked)
continue;
customize[id] = (byte)i;
ret = true;
}
}
ImGui.SameLine();
var (min, max) = locked ? (current, current) : (1, count);
if (InputInt("##text", id, current, min, max, locked))
{
customize[id] = (byte)current;
ret = true;
}
ImGui.SameLine();
alpha.Pop();
ImGui.TextUnformatted(set.Option(id));
return ret;
}
}

View file

@ -0,0 +1,104 @@
using System;
using System.Numerics;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
namespace Glamourer.Gui;
internal partial class Interface
{
private static void Checkmark(string label, string tooltip, bool value, Action<bool> setter)
{
if (ImGuiUtil.Checkbox(label, tooltip, value, setter))
Glamourer.Config.Save();
}
private static void ChangeAndSave<T>(T value, T currentValue, Action<T> setter) where T : IEquatable<T>
{
if (value.Equals(currentValue))
return;
setter(value);
Glamourer.Config.Save();
}
private static void DrawColorPicker(string name, string tooltip, uint value, uint defaultValue, Action<uint> setter)
{
const ImGuiColorEditFlags flags = ImGuiColorEditFlags.AlphaPreviewHalf | ImGuiColorEditFlags.NoInputs;
var tmp = ImGui.ColorConvertU32ToFloat4(value);
if (ImGui.ColorEdit4($"##{name}", ref tmp, flags))
ChangeAndSave(ImGui.ColorConvertFloat4ToU32(tmp), value, setter);
ImGui.SameLine();
if (ImGui.Button($"Default##{name}"))
ChangeAndSave(defaultValue, value, setter);
ImGuiUtil.HoverTooltip(
$"Reset to default: #{defaultValue & 0xFF:X2}{(defaultValue >> 8) & 0xFF:X2}{(defaultValue >> 16) & 0xFF:X2}{defaultValue >> 24:X2}");
ImGui.SameLine();
ImGui.Text(name);
ImGuiUtil.HoverTooltip(tooltip);
}
private static void DrawRestorePenumbraButton()
{
const string buttonLabel = "Re-Register Penumbra";
if (!Glamourer.Config.AttachToPenumbra)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f);
ImGui.Button(buttonLabel);
return;
}
if (ImGui.Button(buttonLabel))
Glamourer.Penumbra.Reattach(true);
ImGuiUtil.HoverTooltip(
"If Penumbra did not register the functions for some reason, pressing this button might help restore functionality.");
}
private static void DrawSettingsTab()
{
using var tab = ImRaii.TabItem("Settings");
if (!tab)
return;
var cfg = Glamourer.Config;
ImGui.Dummy(_spacing);
Checkmark("Folders First", "Sort Folders before all designs instead of lexicographically.", cfg.FoldersFirst,
v => cfg.FoldersFirst = v);
Checkmark("Color Designs", "Color the names of designs in the selector using the colors from below for the given cases.",
cfg.ColorDesigns,
v => cfg.ColorDesigns = v);
Checkmark("Show Locks", "Write-protected Designs show a lock besides their name in the selector.", cfg.ShowLocks,
v => cfg.ShowLocks = v);
Checkmark("Attach to Penumbra",
"Allows you to right-click items in the Changed Items tab of a mod in Penumbra to apply them to your player character.",
cfg.AttachToPenumbra,
v =>
{
cfg.AttachToPenumbra = v;
if (v)
Glamourer.Penumbra.Reattach(true);
else
Glamourer.Penumbra.Unattach();
});
ImGui.SameLine();
DrawRestorePenumbraButton();
Checkmark("Apply Fixed Designs",
"Automatically apply fixed designs to characters and redraw them when anything changes.",
cfg.ApplyFixedDesigns,
v => { cfg.ApplyFixedDesigns = v; });
ImGui.Dummy(_spacing);
DrawColorPicker("Customization Color", "The color for designs that only apply their character customization.",
cfg.CustomizationColor, GlamourerConfig.DefaultCustomizationColor, c => cfg.CustomizationColor = c);
DrawColorPicker("Equipment Color", "The color for designs that only apply some or all of their equipment slots and stains.",
cfg.EquipmentColor, GlamourerConfig.DefaultEquipmentColor, c => cfg.EquipmentColor = c);
DrawColorPicker("State Color", "The color for designs that only apply some state modification.",
cfg.StateColor, GlamourerConfig.DefaultStateColor, c => cfg.StateColor = c);
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Numerics;
using System.Reflection;
using Dalamud.Interface;
using Glamourer.Customization;
using ImGuiNET;
namespace Glamourer.Gui;
internal partial class Interface
{
private static readonly ImGuiScene.TextureWrap? LegacyTattoo = GetLegacyTattooIcon();
private static readonly Vector4 RedTint = new(0.6f, 0.3f, 0.3f, 1f);
private static Vector2 _iconSize = Vector2.Zero;
private static Vector2 _framedIconSize = Vector2.Zero;
private static Vector2 _spacing = Vector2.Zero;
private static float _actorSelectorWidth;
private static float _inputIntSize;
private static float _comboSelectorSize;
private static float _raceSelectorWidth;
private static bool _inGPose;
private static void UpdateState()
{
// General
_inGPose = ObjectManager.IsInGPose();
_spacing = _spacing with { Y = ImGui.GetTextLineHeightWithSpacing() / 2 };
_actorSelectorWidth = 200 * ImGuiHelpers.GlobalScale;
// Customize
_iconSize = new Vector2(ImGui.GetTextLineHeightWithSpacing() * 2);
_framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding;
_inputIntSize = 2 * _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X;
_comboSelectorSize = 4 * _framedIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
_raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X;
// _itemComboWidth = 6 * _actualIconSize.X + 4 * ImGui.GetStyle().ItemSpacing.X - ColorButtonWidth + 1;
}
private static ImGuiScene.TextureWrap? GetLegacyTattooIcon()
{
using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream("Glamourer.LegacyTattoo.raw");
if (resource != null)
{
var rawImage = new byte[resource.Length];
var length = resource.Read(rawImage, 0, (int)resource.Length);
if (length != resource.Length)
return null;
return Dalamud.PluginInterface.UiBuilder.LoadImageRaw(rawImage, 192, 192, 4);
}
return null;
}
}

View file

@ -1,110 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Reflection;
using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.Designs;
using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Raii;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using OtterGui.Widgets;
namespace Glamourer.Gui;
internal partial class Interface : IDisposable
internal partial class Interface : Window, IDisposable
{
public const float SelectorWidth = 200;
public const float MinWindowWidth = 675;
public const int GPoseObjectId = 201;
private const string PluginName = "Glamourer";
private readonly string _glamourerHeader;
private readonly IReadOnlyDictionary<byte, Stain> _stains;
private readonly IReadOnlyDictionary<uint, ModelChara> _models;
private readonly IObjectIdentifier _identifier;
private readonly Dictionary<EquipSlot, (ComboWithFilter<Item>, ComboWithFilter<Stain>)> _combos;
private readonly ImGuiScene.TextureWrap? _legacyTattooIcon;
private readonly Dictionary<EquipSlot, string> _equipSlotNames;
private readonly DesignManager _designs;
private readonly Glamourer _plugin;
private bool _visible;
private bool _inGPose;
private readonly Glamourer _plugin;
public Interface(Glamourer plugin)
: base(GetLabel())
{
_plugin = plugin;
_designs = plugin.Designs;
_glamourerHeader = Glamourer.Version.Length > 0
? $"{PluginName} v{Glamourer.Version}###{PluginName}Main"
: $"{PluginName}###{PluginName}Main";
_plugin = plugin;
Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true;
Dalamud.PluginInterface.UiBuilder.Draw += Draw;
Dalamud.PluginInterface.UiBuilder.OpenConfigUi += ToggleVisibility;
_equipSlotNames = GetEquipSlotNames();
_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(Dalamud.GameData);
_combos = equip.ToDictionary(kvp => kvp.Key, kvp => CreateCombos(kvp.Key, kvp.Value, stainCombo));
_legacyTattooIcon = GetLegacyTattooIcon();
Dalamud.PluginInterface.UiBuilder.OpenConfigUi += Toggle;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(675, 675),
MaximumSize = ImGui.GetIO().DisplaySize,
};
}
public void ToggleVisibility()
=> _visible = !_visible;
public override void Draw()
{
using var tabBar = ImRaii.TabBar("##Tabs");
if (!tabBar)
return;
UpdateState();
_actorTab.Draw();
DrawSettingsTab();
// DrawSaves();
// DrawFixedDesignsTab();
// DrawRevertablesTab();
}
public void Dispose()
{
_legacyTattooIcon?.Dispose();
Dalamud.PluginInterface.UiBuilder.Draw -= Draw;
Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= ToggleVisibility;
Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= Toggle;
}
private void Draw()
{
if (!_visible)
return;
ImGui.SetNextWindowSizeConstraints(Vector2.One * MinWindowWidth * ImGui.GetIO().FontGlobalScale,
Vector2.One * 5000 * ImGui.GetIO().FontGlobalScale);
if (!ImGui.Begin(_glamourerHeader, ref _visible))
{
ImGui.End();
return;
}
try
{
using var tabBar = ImRaii.TabBar("##tabBar");
if (!tabBar)
return;
_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;
_percentageSize = _comboSelectorSize;
_inputIntSize = 2 * _actualIconSize.X + ImGui.GetStyle().ItemSpacing.X;
_raceSelectorWidth = _inputIntSize + _percentageSize - _actualIconSize.X;
_itemComboWidth = 6 * _actualIconSize.X + 4 * ImGui.GetStyle().ItemSpacing.X - ColorButtonWidth + 1;
DrawPlayerTab();
DrawSaves();
DrawFixedDesignsTab();
DrawConfigTab();
DrawRevertablesTab();
}
finally
{
ImGui.End();
}
}
private static string GetLabel()
=> Glamourer.Version.Length == 0
? "Glamourer###GlamourerConfigWindow"
: $"Glamourer v{Glamourer.Version}###GlamourerConfigWindow";
}
//public const float SelectorWidth = 200;
//public const float MinWindowWidth = 675;
//public const int GPoseObjectId = 201;
//private const string PluginName = "Glamourer";
//private readonly string _glamourerHeader;
//
//private readonly IReadOnlyDictionary<byte, Stain> _stains;
//private readonly IReadOnlyDictionary<uint, ModelCeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeehara> _models;
//private readonly IObjectIdentifier _identifier;
//private readonly Dictionary<EquipSlot, (ComboWithFilter<Item>, ComboWithFilter<Stain>)> _combos;
//private readonly ImGuiScene.TextureWrap? _legacyTattooIcon;
//private readonly Dictionary<EquipSlot, string> _equipSlotNames;
//private readonly DesignManager _designs;
//private readonly Glamourer _plugin;
//
//private bool _visible;
//private bool _inGPose;
//
//public Interface(Glamourer plugin)
//{
// _plugin = plugin;
// _designs = plugin.Designs;
// _glamourerHeader = Glamourer.Version.Length > 0
// ? $"{PluginName} v{Glamourer.Version}###{PluginName}Main"
// : $"{PluginName}###{PluginName}Main";
// Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true;
// Dalamud.PluginInterface.UiBuilder.Draw += Draw;
// Dalamud.PluginInterface.UiBuilder.OpenConfigUi += ToggleVisibility;
//
// _equipSlotNames = GetEquipSlotNames();
//
// _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(Dalamud.GameData);
// _combos = equip.ToDictionary(kvp => kvp.Key, kvp => CreateCombos(kvp.Key, kvp.Value, stainCombo));
// _legacyTattooIcon = GetLegacyTattooIcon();
//}
//
//public void ToggleVisibility()
// => _visible = !_visible;
//
//
//private void Draw()
//{
// if (!_visible)
// return;
//
// ImGui.SetNextWindowSizeConstraints(Vector2.One * MinWindowWidth * ImGui.GetIO().FontGlobalScale,
// Vector2.One * 5000 * ImGui.GetIO().FontGlobalScale);
// if (!ImGui.Begin(_glamourerHeader, ref _visible))
// {
// ImGui.End();
// return;
// }
//
// try
// {
// using var tabBar = ImRaii.TabBar("##tabBar");
// if (!tabBar)
// return;
//
// _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;
// _percentageSize = _comboSelectorSize;
// _inputIntSize = 2 * _actualIconSize.X + ImGui.GetStyle().ItemSpacing.X;
// _raceSelectorWidth = _inputIntSize + _percentageSize - _actualIconSize.X;
// _itemComboWidth = 6 * _actualIconSize.X + 4 * ImGui.GetStyle().ItemSpacing.X - ColorButtonWidth + 1;
//
// DrawPlayerTab();
// DrawSaves();
// DrawFixedDesignsTab();
// DrawConfigTab();
// DrawRevertablesTab();
// }
// finally
// {
// ImGui.End();
// }
//}

View file

@ -1,308 +1,291 @@
using System;
using System.Linq;
using System.Numerics;
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.Logging;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.FileSystem;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Structs;
using Penumbra.PlayerWatch;

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

View file

@ -1,223 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
using Dalamud.Logging;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.PlayerWatch;
namespace Glamourer.Gui;
internal partial class Interface
{
public const int CharacterScreenIndex = 240;
public const int ExamineScreenIndex = 241;
public const int FittingRoomIndex = 242;
public const int DyePreviewIndex = 243;
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(CharacterScreenIndex - GPoseObjectId);
private void DrawPlayerFilter()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
ImGui.SetNextItemWidth(SelectorWidth * ImGui.GetIO().FontGlobalScale);
if (ImGui.InputTextWithHint("##playerFilter", "Filter Players...", ref _playerFilter, 32))
_playerFilterLower = _playerFilter.ToLowerInvariant();
}
private void DrawGPoseSelectable(Character player)
{
var playerName = player.Name.ToString();
if (!playerName.Any())
return;
_gPoseActors[playerName] = null;
DrawSelectable(player, $"{playerName} (GPose)", true);
}
private static string GetLabel(Character player, string playerName, int num)
{
if (player.ObjectKind == ObjectKind.Player)
return num == 1 ? playerName : $"{playerName} #{num}";
if (player.ModelType() == 0)
return num == 1 ? $"{playerName} (NPC)" : $"{playerName} #{num} (NPC)";
return num == 1 ? $"{playerName} (Monster)" : $"{playerName} #{num} (Monster)";
}
private void DrawPlayerSelectable(Character player, int idx = 0)
{
var (playerName, modifiable) = idx switch
{
CharacterScreenIndex => ("Character Screen Actor", false),
ExamineScreenIndex => ("Examine Screen Actor", false),
FittingRoomIndex => ("Fitting Room Actor", false),
DyePreviewIndex => ("Dye Preview Actor", false),
_ => (player.Name.ToString(), true),
};
if (!playerName.Any())
return;
if (_playerNames.TryGetValue(playerName, out var num))
_playerNames[playerName] = ++num;
else
_playerNames[playerName] = num = 1;
if (_gPoseActors.ContainsKey(playerName))
{
_gPoseActors[playerName] = player;
return;
}
var label = GetLabel(player, playerName, num);
DrawSelectable(player, label, modifiable);
}
private void DrawSelectable(Character player, string label, bool modifiable)
{
if (!_playerFilterLower.Any() || label.ToLowerInvariant().Contains(_playerFilterLower))
if (ImGui.Selectable(label, _currentLabel == label))
{
_currentLabel = label;
_currentSave.LoadCharacter(player);
_player = player;
_currentSave.WriteProtected = !modifiable;
return;
}
if (_currentLabel != label)
return;
try
{
_currentSave.LoadCharacter(player);
_player = player;
_currentSave.WriteProtected = !modifiable;
}
catch (Exception e)
{
PluginLog.Error($"Could not load character {player.Name}s information:\n{e}");
}
}
private void DrawSelectionButtons()
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
using var font = ImRaii.PushFont(UiBuilder.IconFont);
Character? select = null;
var buttonWidth = Vector2.UnitX * SelectorWidth / 2;
if (ImGui.Button(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth))
select = Dalamud.ClientState.LocalPlayer;
font.Pop();
ImGuiUtil.HoverTooltip("Select the local player character.");
ImGui.SameLine();
font.Push(UiBuilder.IconFont);
if (_inGPose)
{
style.Push(ImGuiStyleVar.Alpha, 0.5f);
ImGui.Button(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth);
style.Pop();
}
else
{
if (ImGui.Button(FontAwesomeIcon.HandPointer.ToIconString(), buttonWidth))
select = CharacterFactory.Convert(Dalamud.Targets.Target);
}
font.Pop();
ImGuiUtil.HoverTooltip("Select the current target, if it is in the list.");
if (select == null)
return;
try
{
_currentSave.LoadCharacter(select);
_player = select;
_currentLabel = _player.Name.ToString();
_currentSave.WriteProtected = false;
}
catch (Exception e)
{
PluginLog.Error($"Could not load character {select.Name}s information:\n{e}");
}
}
private void DrawPlayerSelector()
{
ImGui.BeginGroup();
DrawPlayerFilter();
if (!ImGui.BeginChild("##playerSelector",
new Vector2(SelectorWidth * ImGui.GetIO().FontGlobalScale, -ImGui.GetFrameHeight() - 1), true))
{
ImGui.EndChild();
ImGui.EndGroup();
return;
}
_playerNames.Clear();
_gPoseActors.Clear();
for (var i = GPoseObjectId; i < GPoseObjectId + 48; ++i)
{
var player = CharacterFactory.Convert(Dalamud.Objects[i]);
if (player == null)
break;
DrawGPoseSelectable(player);
}
for (var i = 0; i < GPoseObjectId; ++i)
{
var player = CharacterFactory.Convert(Dalamud.Objects[i]);
if (player != null)
DrawPlayerSelectable(player);
}
for (var i = CharacterScreenIndex; i < Dalamud.Objects.Length; ++i)
{
var player = CharacterFactory.Convert(Dalamud.Objects[i]);
if (player != null)
DrawPlayerSelectable(player, i);
}
using (var _ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero))
{
ImGui.EndChild();
}
DrawSelectionButtons();
ImGui.EndGroup();
}
private void DrawPlayerTab()
{
using var tab = ImRaii.TabItem("Current Players");
_player = null;
if (!tab)
return;
DrawPlayerSelector();
if (_currentLabel.Length == 0)
return;
ImGui.SameLine();
DrawActorPanel();
}
}

View file

@ -1,114 +0,0 @@
using System;
using System.Numerics;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
namespace Glamourer.Gui
{
internal partial class Interface
{
private static void DrawConfigCheckMark(string label, string tooltip, bool value, Action<bool> setter)
{
if (DrawCheckMark(label, value, setter))
Glamourer.Config.Save();
ImGuiUtil.HoverTooltip(tooltip);
}
private static void ChangeAndSave<T>(T value, T currentValue, Action<T> setter) where T : IEquatable<T>
{
if (value.Equals(currentValue))
return;
setter(value);
Glamourer.Config.Save();
}
private static void DrawColorPicker(string name, string tooltip, uint value, uint defaultValue, Action<uint> setter)
{
const ImGuiColorEditFlags flags = ImGuiColorEditFlags.AlphaPreviewHalf | ImGuiColorEditFlags.NoInputs;
var tmp = ImGui.ColorConvertU32ToFloat4(value);
if (ImGui.ColorEdit4($"##{name}", ref tmp, flags))
ChangeAndSave(ImGui.ColorConvertFloat4ToU32(tmp), value, setter);
ImGui.SameLine();
if (ImGui.Button($"Default##{name}"))
ChangeAndSave(defaultValue, value, setter);
ImGuiUtil.HoverTooltip(
$"Reset to default: #{defaultValue & 0xFF:X2}{(defaultValue >> 8) & 0xFF:X2}{(defaultValue >> 16) & 0xFF:X2}{defaultValue >> 24:X2}");
ImGui.SameLine();
ImGui.Text(name);
ImGuiUtil.HoverTooltip(tooltip);
}
private static void DrawRestorePenumbraButton()
{
const string buttonLabel = "Re-Register Penumbra";
if (!Glamourer.Config.AttachToPenumbra)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f);
ImGui.Button(buttonLabel);
return;
}
if (ImGui.Button(buttonLabel))
Glamourer.Penumbra.Reattach(true);
ImGuiUtil.HoverTooltip(
"If Penumbra did not register the functions for some reason, pressing this button might help restore functionality.");
}
private static void DrawConfigTab()
{
using var tab = ImRaii.TabItem("Config");
if (!tab)
return;
var cfg = Glamourer.Config;
ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeightWithSpacing() / 2);
DrawConfigCheckMark("Folders First", "Sort Folders before all designs instead of lexicographically.", cfg.FoldersFirst,
v => cfg.FoldersFirst = v);
DrawConfigCheckMark("Color Designs", "Color the names of designs in the selector using the colors from below for the given cases.",
cfg.ColorDesigns,
v => cfg.ColorDesigns = v);
DrawConfigCheckMark("Show Locks", "Write-protected Designs show a lock besides their name in the selector.", cfg.ShowLocks,
v => cfg.ShowLocks = v);
DrawConfigCheckMark("Attach to Penumbra",
"Allows you to right-click items in the Changed Items tab of a mod in Penumbra to apply them to your player character.",
cfg.AttachToPenumbra,
v =>
{
cfg.AttachToPenumbra = v;
if (v)
Glamourer.Penumbra.Reattach(true);
else
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.",
cfg.CustomizationColor, GlamourerConfig.DefaultCustomizationColor, c => cfg.CustomizationColor = c);
DrawColorPicker("Equipment Color", "The color for designs that only apply some or all of their equipment slots and stains.",
cfg.EquipmentColor, GlamourerConfig.DefaultEquipmentColor, c => cfg.EquipmentColor = c);
DrawColorPicker("State Color", "The color for designs that only apply some state modification.",
cfg.StateColor, GlamourerConfig.DefaultStateColor, c => cfg.StateColor = c);
}
}
}

View file

@ -1,478 +0,0 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Logging;
using Glamourer.Customization;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui
{
internal partial class Interface
{
private static bool DrawColorPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value)
{
value = default;
using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize);
if (!popup)
return false;
var ret = false;
var count = set.Count(id);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
for (var i = 0; i < count; ++i)
{
var custom = set.Data(id, i);
if (ImGui.ColorButton((i + 1).ToString(), ImGui.ColorConvertU32ToFloat4(custom.Color)))
{
value = custom;
ret = true;
ImGui.CloseCurrentPopup();
}
if (i % 8 != 7)
ImGui.SameLine();
}
return ret;
}
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, 1, ImGuiInputTextFlags.EnterReturnsTrue) && tmp != value + 1 && tmp >= minValue && tmp <= maxValue)
{
value = tmp - 1;
ret = true;
}
ImGuiUtil.HoverTooltip($"Input Range: [{minValue}, {maxValue}]");
return ret;
}
private static (int, Customization.Customization) GetCurrentCustomization(ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
var current = set.DataByValue(id, customization[id], out var custom);
if (set.IsAvailable(id) && current < 0)
{
PluginLog.Warning($"Read invalid customization value {customization[id]} for {id}.");
current = 0;
custom = set.Data(id, 0);
}
return (current, custom!.Value);
}
private bool DrawColorPicker(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
var ret = false;
var count = set.Count(id);
var (current, custom) = GetCurrentCustomization(ref customization, id, set);
var popupName = $"Color Picker##{id}";
if (ImGui.ColorButton($"{current + 1}##color_{id}", ImGui.ColorConvertU32ToFloat4(custom.Color), ImGuiColorEditFlags.None,
_actualIconSize))
ImGui.OpenPopup(popupName);
ImGui.SameLine();
using (var _ = ImRaii.Group())
{
if (InputInt($"##text_{id}", ref current, 1, count))
{
customization[id] = set.Data(id, current).Value;
ret = true;
}
ImGui.Text(label);
ImGuiUtil.HoverTooltip(tooltip);
}
if (!DrawColorPickerPopup(popupName, set, id, out var newCustom))
return ret;
customization[id] = newCustom.Value;
ret = true;
return ret;
}
private bool DrawListSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
using var bigGroup = ImRaii.Group();
var ret = false;
int current = customization[id];
var count = set.Count(id);
ImGui.SetNextItemWidth(_comboSelectorSize * ImGui.GetIO().FontGlobalScale);
if (ImGui.BeginCombo($"##combo_{id}", $"{set.Option(id)} #{current + 1}"))
{
for (var i = 0; i < count; ++i)
{
if (ImGui.Selectable($"{set.Option(id)} #{i + 1}##combo", i == current) && i != current)
{
customization[id] = (byte) i;
ret = true;
}
}
ImGui.EndCombo();
}
ImGui.SameLine();
if (InputInt($"##text_{id}", ref current, 1, count))
{
customization[id] = (byte) current;
ret = true;
}
ImGui.SameLine();
ImGui.Text(label);
ImGuiUtil.HoverTooltip(tooltip);
return ret;
}
private static readonly Vector4 NoColor = new(1f, 1f, 1f, 1f);
private static readonly Vector4 RedColor = new(0.6f, 0.3f, 0.3f, 1f);
private bool DrawMultiSelector(ref CharacterCustomization customization, CustomizationSet set)
{
using var bigGroup = ImRaii.Group();
var ret = false;
var count = set.Count(CustomizationId.FacialFeaturesTattoos);
using (var _ = ImRaii.Group())
{
var face = customization.Face;
if (set.Faces.Count < face)
face = 1;
for (var i = 0; i < count; ++i)
{
var enabled = customization.FacialFeature(i);
var feature = set.FacialFeature(face, i);
var icon = i == count - 1
? _legacyTattooIcon ?? Glamourer.Customization.GetIcon(feature.IconId)
: Glamourer.Customization.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int) ImGui.GetStyle().FramePadding.X,
Vector4.Zero,
enabled ? NoColor : RedColor))
{
ret = true;
customization.FacialFeature(i, !enabled);
}
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
if (i % 4 != 3)
ImGui.SameLine();
}
}
ImGui.SameLine();
using var group = ImRaii.Group();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetTextLineHeightWithSpacing() + 3 * ImGui.GetStyle().ItemSpacing.Y / 2);
int value = customization[CustomizationId.FacialFeaturesTattoos];
if (InputInt($"##{CustomizationId.FacialFeaturesTattoos}", ref value, 1, 256))
{
customization[CustomizationId.FacialFeaturesTattoos] = (byte) value;
ret = true;
}
ImGui.TextUnformatted(set.Option(CustomizationId.FacialFeaturesTattoos));
return ret;
}
private bool DrawIconPickerPopup(string label, CustomizationSet set, CustomizationId id, out Customization.Customization value)
{
value = default;
using var popup = ImRaii.Popup(label, ImGuiWindowFlags.AlwaysAutoResize);
if (!popup)
return false;
var ret = false;
var count = set.Count(id);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
for (var i = 0; i < count; ++i)
{
var custom = set.Data(id, i);
var icon = Glamourer.Customization.GetIcon(custom.IconId);
ImGui.BeginGroup();
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
{
value = custom;
ret = true;
ImGui.CloseCurrentPopup();
}
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
var text = custom.Value.ToString();
var textWidth = ImGui.CalcTextSize(text).X;
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (_iconSize.X - textWidth + 2 * ImGui.GetStyle().FramePadding.X)/2);
ImGui.Text(text);
ImGui.EndGroup();
if (i % 8 != 7)
ImGui.SameLine();
}
return ret;
}
private bool DrawIconSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
using var bigGroup = ImRaii.Group();
var ret = false;
var count = set.Count(id);
var current = set.DataByValue(id, customization[id], out var custom);
if (current < 0)
{
label = $"{label} (Custom #{customization[id]})";
current = 0;
custom = set.Data(id, 0);
}
var popupName = $"Style Picker##{id}";
var icon = Glamourer.Customization.GetIcon(custom!.Value.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
ImGui.OpenPopup(popupName);
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
ImGui.SameLine();
using var group = ImRaii.Group();
if (InputInt($"##text_{id}", ref current, 1, count))
{
customization[id] = set.Data(id, current).Value;
ret = true;
}
if (DrawIconPickerPopup(popupName, set, id, out var newCustom))
{
customization[id] = newCustom.Value;
ret = true;
}
ImGui.TextUnformatted($"{label} ({custom.Value.Value})");
ImGuiUtil.HoverTooltip(tooltip);
return ret;
}
private bool DrawPercentageSelector(string label, string tooltip, ref CharacterCustomization customization, CustomizationId id,
CustomizationSet set)
{
using var bigGroup = ImRaii.Group();
var ret = false;
int value = customization[id];
var count = set.Count(id);
ImGui.SetNextItemWidth(_percentageSize * ImGui.GetIO().FontGlobalScale);
if (ImGui.SliderInt($"##slider_{id}", ref value, 0, count - 1, "") && value != customization[id])
{
customization[id] = (byte) value;
ret = true;
}
ImGui.SameLine();
--value;
if (InputInt($"##input_{id}", ref value, 0, count - 1))
{
customization[id] = (byte) (value + 1);
ret = true;
}
ImGui.SameLine();
ImGui.TextUnformatted(label);
ImGuiUtil.HoverTooltip(tooltip);
return ret;
}
private bool DrawRaceSelector(ref CharacterCustomization customization)
{
using var group = ImRaii.Group();
var ret = false;
ImGui.SetNextItemWidth(_raceSelectorWidth);
if (ImGui.BeginCombo("##subRaceCombo", ClanName(customization.Clan, customization.Gender)))
{
for (var i = 0; i < (int) SubRace.Veena; ++i)
{
if (ImGui.Selectable(ClanName((SubRace) i + 1, customization.Gender), (int) customization.Clan == i + 1))
{
var race = (SubRace) i + 1;
ret |= ChangeRace(ref customization, race);
}
}
ImGui.EndCombo();
}
ImGui.TextUnformatted(
$"{Glamourer.Customization.GetName(CustomName.Gender)} & {Glamourer.Customization.GetName(CustomName.Clan)}");
return ret;
}
private bool DrawGenderSelector(ref CharacterCustomization customization)
{
var ret = false;
using var font = ImRaii.PushFont(UiBuilder.IconFont);
var icon = customization.Gender == Gender.Male ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus;
var restricted = false;
if (customization.Race == Race.Hrothgar)
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.25f);
icon = FontAwesomeIcon.MarsDouble;
restricted = true;
}
if (ImGui.Button(icon.ToIconString(), _actualIconSize) && !restricted)
{
var gender = customization.Gender == Gender.Male ? Gender.Female : Gender.Male;
ret = ChangeGender(ref customization, gender);
}
if (restricted)
ImGui.PopStyleVar();
return ret;
}
private bool DrawPicker(CustomizationSet set, CustomizationId id, ref CharacterCustomization customization)
{
if (!set.IsAvailable(id))
return false;
switch (set.Type(id))
{
case CharaMakeParams.MenuType.ColorPicker: return DrawColorPicker(set.OptionName[(int) id], "", ref customization, id, set);
case CharaMakeParams.MenuType.ListSelector: return DrawListSelector(set.OptionName[(int) id], "", ref customization, id, set);
case CharaMakeParams.MenuType.IconSelector: return DrawIconSelector(set.OptionName[(int) id], "", ref customization, id, set);
case CharaMakeParams.MenuType.MultiIconSelector: return DrawMultiSelector(ref customization, set);
case CharaMakeParams.MenuType.Percentage:
return DrawPercentageSelector(set.OptionName[(int) id], "", ref customization, id, set);
}
return false;
}
private static 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 static readonly CustomizationId[] AllCustomizations = GetCustomizationOrder();
private bool DrawCustomization(ref CharacterCustomization custom)
{
if (!ImGui.CollapsingHeader("Character Customization"))
return false;
var ret = DrawGenderSelector(ref custom);
ImGui.SameLine();
ret |= DrawRaceSelector(ref custom);
var set = Glamourer.Customization.GetList(custom.Clan, custom.Gender);
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.Percentage))
ret |= DrawPicker(set, id, ref custom);
var odd = true;
foreach (var id in AllCustomizations.Where((c, _) => set.Type(c) == CharaMakeParams.MenuType.IconSelector))
{
ret |= DrawPicker(set, id, ref custom);
if (odd)
ImGui.SameLine();
odd = !odd;
}
if (!odd)
ImGui.NewLine();
ret |= DrawPicker(set, CustomizationId.FacialFeaturesTattoos, ref custom);
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.ListSelector))
ret |= DrawPicker(set, id, ref custom);
odd = true;
foreach (var id in AllCustomizations.Where(c => set.Type(c) == CharaMakeParams.MenuType.ColorPicker))
{
ret |= DrawPicker(set, id, ref custom);
if (odd)
ImGui.SameLine();
odd = !odd;
}
if (!odd)
ImGui.NewLine();
var tmp = custom.HighlightsOn;
if (ImGui.Checkbox(set.Option(CustomizationId.HighlightsOnFlag), ref tmp) && tmp != custom.HighlightsOn)
{
custom.HighlightsOn = tmp;
ret = true;
}
var xPos = _inputIntSize + _actualIconSize.X + 3 * ImGui.GetStyle().ItemSpacing.X;
ImGui.SameLine(xPos);
tmp = custom.FacePaintReversed;
if (ImGui.Checkbox($"{Glamourer.Customization.GetName(CustomName.Reverse)} {set.Option(CustomizationId.FacePaint)}", ref tmp)
&& tmp != custom.FacePaintReversed)
{
custom.FacePaintReversed = tmp;
ret = true;
}
tmp = custom.SmallIris;
if (ImGui.Checkbox($"{Glamourer.Customization.GetName(CustomName.IrisSmall)} {Glamourer.Customization.GetName(CustomName.IrisSize)}",
ref tmp)
&& tmp != custom.SmallIris)
{
custom.SmallIris = tmp;
ret = true;
}
if (custom.Race != Race.Hrothgar)
{
tmp = custom.Lipstick;
ImGui.SameLine(xPos);
if (ImGui.Checkbox(set.Option(CustomizationId.LipColor), ref tmp) && tmp != custom.Lipstick)
{
custom.Lipstick = tmp;
ret = true;
}
}
return ret;
}
}
}

View file

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

View file

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

View file

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

View file

@ -6,197 +6,98 @@ using Glamourer.Structs;
using ImGuiNET;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui
{
internal partial class Interface
{
// Push the stain color to type and if it is too bright, turn the text color black.
// Return number of pushed styles.
private static int PushColor(Stain stain, ImGuiCol type = ImGuiCol.Button)
{
ImGui.PushStyleColor(type, stain.RgbaColor);
if (stain.Intensity > 127)
{
ImGui.PushStyleColor(ImGuiCol.Text, 0xFF101010);
return 2;
}
namespace Glamourer.Gui;
return 1;
}
//internal partial class Interface
//{
// // Push the stain color to type and if it is too bright, turn the text color black.
// // Return number of pushed styles.
// private static int PushColor(Stain stain, ImGuiCol type = ImGuiCol.Button)
// {
// ImGui.PushStyleColor(type, stain.RgbaColor);
// if (stain.Intensity > 127)
// {
// ImGui.PushStyleColor(ImGuiCol.Text, 0xFF101010);
// return 2;
// }
//
// return 1;
// }
//
// Go through a whole customization struct and fix up all settings that need fixing.
private static void FixUpAttributes(ref CharacterCustomization customization)
{
var set = Glamourer.Customization.GetList(customization.Clan, customization.Gender);
foreach (CustomizationId id in Enum.GetValues(typeof(CustomizationId)))
{
switch (id)
{
case CustomizationId.Race: break;
case CustomizationId.Clan: break;
case CustomizationId.BodyType: break;
case CustomizationId.Gender: break;
case CustomizationId.FacialFeaturesTattoos: break;
case CustomizationId.HighlightsOnFlag: break;
case CustomizationId.Face: break;
default:
var count = set.Count(id);
if (set.DataByValue(id, customization[id], out _) < 0)
if (count == 0)
customization[id] = 0;
else
customization[id] = set.Data(id, 0).Value;
break;
}
}
}
//
// Change a race and fix up all required customizations afterwards.
private static bool ChangeRace(ref CharacterCustomization customization, SubRace clan)
{
if (clan == customization.Clan)
return false;
var race = clan.ToRace();
customization.Race = race;
customization.Clan = clan;
if (race == Race.Hrothgar)
customization.Gender = Gender.Male;
FixUpAttributes(ref customization);
return true;
}
// Change a gender and fix up all required customizations afterwards.
private static bool ChangeGender(ref CharacterCustomization customization, Gender gender)
{
if (gender == customization.Gender)
return false;
customization.Gender = gender;
FixUpAttributes(ref customization);
return true;
}
private static string ClanName(SubRace race, Gender gender)
{
if (gender == Gender.Female)
return race switch
{
SubRace.Midlander => Glamourer.Customization.GetName(CustomName.MidlanderM),
SubRace.Highlander => Glamourer.Customization.GetName(CustomName.HighlanderM),
SubRace.Wildwood => Glamourer.Customization.GetName(CustomName.WildwoodM),
SubRace.Duskwight => Glamourer.Customization.GetName(CustomName.DuskwightM),
SubRace.Plainsfolk => Glamourer.Customization.GetName(CustomName.PlainsfolkM),
SubRace.Dunesfolk => Glamourer.Customization.GetName(CustomName.DunesfolkM),
SubRace.SeekerOfTheSun => Glamourer.Customization.GetName(CustomName.SeekerOfTheSunM),
SubRace.KeeperOfTheMoon => Glamourer.Customization.GetName(CustomName.KeeperOfTheMoonM),
SubRace.Seawolf => Glamourer.Customization.GetName(CustomName.SeawolfM),
SubRace.Hellsguard => Glamourer.Customization.GetName(CustomName.HellsguardM),
SubRace.Raen => Glamourer.Customization.GetName(CustomName.RaenM),
SubRace.Xaela => Glamourer.Customization.GetName(CustomName.XaelaM),
SubRace.Helion => Glamourer.Customization.GetName(CustomName.HelionM),
SubRace.Lost => Glamourer.Customization.GetName(CustomName.LostM),
SubRace.Rava => Glamourer.Customization.GetName(CustomName.RavaF),
SubRace.Veena => Glamourer.Customization.GetName(CustomName.VeenaF),
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
};
return race switch
{
SubRace.Midlander => Glamourer.Customization.GetName(CustomName.MidlanderF),
SubRace.Highlander => Glamourer.Customization.GetName(CustomName.HighlanderF),
SubRace.Wildwood => Glamourer.Customization.GetName(CustomName.WildwoodF),
SubRace.Duskwight => Glamourer.Customization.GetName(CustomName.DuskwightF),
SubRace.Plainsfolk => Glamourer.Customization.GetName(CustomName.PlainsfolkF),
SubRace.Dunesfolk => Glamourer.Customization.GetName(CustomName.DunesfolkF),
SubRace.SeekerOfTheSun => Glamourer.Customization.GetName(CustomName.SeekerOfTheSunF),
SubRace.KeeperOfTheMoon => Glamourer.Customization.GetName(CustomName.KeeperOfTheMoonF),
SubRace.Seawolf => Glamourer.Customization.GetName(CustomName.SeawolfF),
SubRace.Hellsguard => Glamourer.Customization.GetName(CustomName.HellsguardF),
SubRace.Raen => Glamourer.Customization.GetName(CustomName.RaenF),
SubRace.Xaela => Glamourer.Customization.GetName(CustomName.XaelaF),
SubRace.Helion => Glamourer.Customization.GetName(CustomName.HelionM),
SubRace.Lost => Glamourer.Customization.GetName(CustomName.LostM),
SubRace.Rava => Glamourer.Customization.GetName(CustomName.RavaF),
SubRace.Veena => Glamourer.Customization.GetName(CustomName.VeenaF),
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
};
}
private enum DesignNameUse
{
SaveCurrent,
NewDesign,
DuplicateDesign,
NewFolder,
FromClipboard,
}
private void DrawDesignNamePopup(DesignNameUse use)
{
if (ImGui.BeginPopup($"{DesignNamePopupLabel}{use}"))
{
if (ImGui.InputText("##designName", ref _newDesignName, 64, ImGuiInputTextFlags.EnterReturnsTrue)
&& _newDesignName.Any())
{
switch (use)
{
case DesignNameUse.SaveCurrent:
SaveNewDesign(ConditionalCopy(_currentSave, _holdShift, _holdCtrl));
break;
case DesignNameUse.NewDesign:
var empty = new CharacterSave();
empty.Load(CharacterCustomization.Default);
empty.WriteCustomizations = false;
SaveNewDesign(empty);
break;
case DesignNameUse.DuplicateDesign:
SaveNewDesign(ConditionalCopy(_selection!.Data, _holdShift, _holdCtrl));
break;
case DesignNameUse.NewFolder:
_designs.FileSystem
.CreateAllFolders($"{_newDesignName}/a"); // Filename is just ignored, but all folders are created.
break;
case DesignNameUse.FromClipboard:
try
{
var text = ImGui.GetClipboardText();
var save = CharacterSave.FromString(text);
SaveNewDesign(save);
}
catch (Exception e)
{
PluginLog.Information($"Could not save new Design from Clipboard:\n{e}");
}
break;
}
_newDesignName = string.Empty;
ImGui.CloseCurrentPopup();
}
if (_keyboardFocus)
{
ImGui.SetKeyboardFocusHere();
_keyboardFocus = false;
}
ImGui.EndPopup();
}
}
private void OpenDesignNamePopup(DesignNameUse use)
{
_newDesignName = string.Empty;
_keyboardFocus = true;
_holdCtrl = ImGui.GetIO().KeyCtrl;
_holdShift = ImGui.GetIO().KeyShift;
ImGui.OpenPopup($"{DesignNamePopupLabel}{use}");
}
}
}
//
//
// private enum DesignNameUse
// {
// SaveCurrent,
// NewDesign,
// DuplicateDesign,
// NewFolder,
// FromClipboard,
// }
//
// private void DrawDesignNamePopup(DesignNameUse use)
// {
// if (ImGui.BeginPopup($"{DesignNamePopupLabel}{use}"))
// {
// if (ImGui.InputText("##designName", ref _newDesignName, 64, ImGuiInputTextFlags.EnterReturnsTrue)
// && _newDesignName.Any())
// {
// switch (use)
// {
// case DesignNameUse.SaveCurrent:
// SaveNewDesign(ConditionalCopy(_currentSave, _holdShift, _holdCtrl));
// break;
// case DesignNameUse.NewDesign:
// var empty = new CharacterSave();
// empty.Load(CharacterCustomization.Default);
// empty.WriteCustomizations = false;
// SaveNewDesign(empty);
// break;
// case DesignNameUse.DuplicateDesign:
// SaveNewDesign(ConditionalCopy(_selection!.Data, _holdShift, _holdCtrl));
// break;
// case DesignNameUse.NewFolder:
// _designs.FileSystem
// .CreateAllFolders($"{_newDesignName}/a"); // Filename is just ignored, but all folders are created.
// break;
// case DesignNameUse.FromClipboard:
// try
// {
// var text = ImGui.GetClipboardText();
// var save = CharacterSave.FromString(text);
// SaveNewDesign(save);
// }
// catch (Exception e)
// {
// PluginLog.Information($"Could not save new Design from Clipboard:\n{e}");
// }
//
// break;
// }
//
// _newDesignName = string.Empty;
// ImGui.CloseCurrentPopup();
// }
//
// if (_keyboardFocus)
// {
// ImGui.SetKeyboardFocusHere();
// _keyboardFocus = false;
// }
//
// ImGui.EndPopup();
// }
// }
//
// private void OpenDesignNamePopup(DesignNameUse use)
// {
// _newDesignName = string.Empty;
// _keyboardFocus = true;
// _holdCtrl = ImGui.GetIO().KeyCtrl;
// _holdShift = ImGui.GetIO().KeyShift;
// ImGui.OpenPopup($"{DesignNamePopupLabel}{use}");
// }
//}

View file

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

View file

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

View file

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

179
Glamourer/ObjectManager.cs Normal file
View file

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
namespace Glamourer;
public unsafe struct Actor : IEquatable<Actor>
{
public static readonly Actor Null = new() { Pointer = null };
public FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Pointer;
public IntPtr Address
=> (IntPtr)Pointer;
public static implicit operator Actor(IntPtr? pointer)
=> new() { Pointer = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)pointer.GetValueOrDefault(IntPtr.Zero) };
public static implicit operator IntPtr(Actor actor)
=> actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer;
public Character? Character
=> Pointer == null ? null : Dalamud.Objects[Pointer->GameObject.ObjectIndex] as Character;
public bool IsAvailable
=> Pointer->GameObject.GetIsTargetable();
public bool IsHuman
=> Pointer != null && Pointer->ModelCharaId == 0;
public int ModelId
=> Pointer != null ? Pointer->ModelCharaId : 0;
public void SetModelId(int value)
{
if (Pointer != null)
Pointer->ModelCharaId = value;
}
public static implicit operator bool(Actor actor)
=> actor.Pointer != null;
public static bool operator true(Actor actor)
=> actor.Pointer != null;
public static bool operator false(Actor actor)
=> actor.Pointer == null;
public static bool operator !(Actor actor)
=> actor.Pointer == null;
public bool Equals(Actor other)
=> Pointer == other.Pointer;
public override bool Equals(object? obj)
=> obj is Actor other && Equals(other);
public override int GetHashCode()
=> ((ulong)Pointer).GetHashCode();
public static bool operator ==(Actor lhs, Actor rhs)
=> lhs.Pointer == rhs.Pointer;
public static bool operator !=(Actor lhs, Actor rhs)
=> lhs.Pointer != rhs.Pointer;
}
public static class ObjectManager
{
private const int GPosePlayerIndex = 201;
private const int CharacterScreenIndex = 240;
private const int ExamineScreenIndex = 241;
private const int FittingRoomIndex = 242;
private const int DyePreviewIndex = 243;
private static readonly Dictionary<string, int> _nameCounters = new();
private static readonly Dictionary<string, Actor> _gPoseActors = new(CharacterScreenIndex - GPosePlayerIndex);
public static bool IsInGPose()
=> Dalamud.Objects[GPosePlayerIndex] != null;
public static Actor GPosePlayer
=> Dalamud.Objects[GPosePlayerIndex]?.Address;
public static Actor Player
=> Dalamud.ClientState.LocalPlayer?.Address;
public record struct ActorData(string Label, string Name, Actor Actor, bool Modifiable, Actor GPose);
public static IEnumerable<ActorData> GetEnumerator()
{
_nameCounters.Clear();
_gPoseActors.Clear();
for (var i = GPosePlayerIndex; i < CharacterScreenIndex; ++i)
{
var character = Dalamud.Objects[i];
if (character == null)
break;
var name = character.Name.TextValue;
_gPoseActors[name] = character.Address;
yield return new ActorData(GetLabel(character, name, 0, true), name, character.Address, true, Actor.Null);
}
var actor = Dalamud.Objects[CharacterScreenIndex];
if (actor != null)
yield return new ActorData("Character Screen Actor", string.Empty, actor.Address, false, Actor.Null);
actor = Dalamud.Objects[ExamineScreenIndex];
if (actor != null)
yield return new ActorData("Examine Screen Actor", string.Empty, actor.Address, false, Actor.Null);
actor = Dalamud.Objects[FittingRoomIndex];
if (actor != null)
yield return new ActorData("Fitting Room Actor", string.Empty, actor.Address, false, Actor.Null);
actor = Dalamud.Objects[DyePreviewIndex];
if (actor != null)
yield return new ActorData("Dye Preview Actor", string.Empty, actor.Address, false, Actor.Null);
for (var i = 0; i < GPosePlayerIndex; ++i)
{
var character = Dalamud.Objects[i];
if (character == null
|| character.ObjectKind is not (ObjectKind.Player or ObjectKind.BattleNpc or ObjectKind.EventNpc or ObjectKind.Companion
or ObjectKind.Retainer))
continue;
var name = character.Name.TextValue;
if (name.Length == 0)
continue;
if (_nameCounters.TryGetValue(name, out var num))
_nameCounters[name] = ++num;
else
_nameCounters[name] = num = 1;
if (!_gPoseActors.TryGetValue(name, out var gPose))
gPose = Actor.Null;
yield return new ActorData(GetLabel(character, name, num, false), name, character.Address, true, gPose);
}
for (var i = DyePreviewIndex + 1; i < Dalamud.Objects.Length; ++i)
{
var character = Dalamud.Objects[i];
if (character == null
|| !((Actor)character.Address).IsAvailable
|| character.ObjectKind is not (ObjectKind.Player or ObjectKind.BattleNpc or ObjectKind.EventNpc or ObjectKind.Companion
or ObjectKind.Retainer))
continue;
var name = character.Name.TextValue;
if (name.Length == 0)
continue;
if (_nameCounters.TryGetValue(name, out var num))
_nameCounters[name] = ++num;
else
_nameCounters[name] = num = 1;
if (!_gPoseActors.TryGetValue(name, out var gPose))
gPose = Actor.Null;
yield return new ActorData(GetLabel(character, name, num, false), name, character.Address, true, gPose);
}
}
private static unsafe string GetLabel(GameObject player, string playerName, int num, bool gPose)
{
if (player.ObjectKind == ObjectKind.Player)
return gPose ? $"{playerName} (GPose)" : num == 1 ? playerName : $"{playerName} #{num}";
if (((FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)player!.Address)->ModelCharaId == 0)
return gPose ? $"{playerName} (GPose, NPC)" : num == 1 ? $"{playerName} (NPC)" : $"{playerName} #{num} (NPC)";
return gPose ? $"{playerName} (GPose, Monster)" : num == 1 ? $"{playerName} (Monster)" : $"{playerName} #{num} (Monster)";
}
}

211
Glamourer/RedrawManager.cs Normal file
View file

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer;
public unsafe class RedrawManager : IDisposable
{
public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, CharacterArmor* data);
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A")]
public Hook<FlagSlotForUpdateDelegate> FlagSlotForUpdateHook = null!;
private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slot, CharacterArmor* data)
{
return FlagSlotForUpdateHook.Original(drawObject, slot, data);
}
public delegate void LoadWeaponDelegate(IntPtr characterOffset, uint slot, ulong data, byte unk);
[Signature("E8 ?? ?? ?? ?? 44 8B 9F")]
public Hook<LoadWeaponDelegate> LoadWeaponHook = null!;
private void LoadWeaponDetour(IntPtr characterOffset, uint slot, ulong data, byte unk)
{
const int offset = 0xD8 * 8;
PluginLog.Information($"0x{characterOffset:X}, 0x{characterOffset - offset:X}, {slot}, {data:16X}, {unk}");
LoadWeaponHook.Original(characterOffset, slot, data, unk);
}
//[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A",
// DetourName = nameof(FlagSlotForUpdateDetour))]
//public Hook<FlagSlotForUpdateDelegate>? FlagSlotForUpdateHook;
//
// public readonly FixedDesigns FixedDesigns;
//
private readonly Dictionary<string, CharacterSave> _currentRedraws = new(32);
public RedrawManager()
{
SignatureHelper.Initialise(this);
// FixedDesigns = new FixedDesigns(designs);
Glamourer.Penumbra.CreatingCharacterBase += OnCharacterRedraw;
FlagSlotForUpdateHook.Enable();
LoadWeaponHook.Enable();
//
// if (Glamourer.Config.ApplyFixedDesigns)
// Enable();
}
public void Dispose()
{
FlagSlotForUpdateHook.Dispose();
LoadWeaponHook.Dispose();
Glamourer.Penumbra.CreatingCharacterBase -= OnCharacterRedraw;
//FlagSlotForUpdateHook?.Dispose();
}
public void Set(Character* actor, CharacterSave save)
{
var name = GetName(actor);
if (name.Length == 0)
return;
_currentRedraws[name] = save;
}
public void Set(IntPtr actor, CharacterSave save)
=> Set((Character*)actor, save);
public void Revert(Character* actor)
=> _currentRedraws.Remove(GetName(actor));
public void Revert(IntPtr actor)
=> Revert((Character*)actor);
private static string GetName(Character* actor)
{
return string.Concat(new Utf8String(actor->GameObject.Name)
.Select(c => (char)c)
.Append(actor->GameObject.ObjectKind == (byte)ObjectKind.Pc ? (char)actor->HomeWorld : (char)actor->GameObject.ObjectIndex));
}
private void Cleanup(object? _, ushort _1)
=> _currentRedraws.Clear();
public void ChangeEquip(Human* actor, EquipSlot slot, CharacterArmor item)
=> Flag(actor, slot.ToIndex(), &item);
public void ChangeEquip(Character* character, EquipSlot slot, CharacterArmor item)
=> ChangeEquip((Human*)character->GameObject.DrawObject, slot, item);
public void ChangeEquip(IntPtr character, EquipSlot slot, CharacterArmor item)
=> ChangeEquip((Character*)character, slot, item);
private void OnCharacterRedraw(IntPtr addr, IntPtr modelId, IntPtr customize, IntPtr equipData)
{
var name = GetName((Character*)addr);
if (_currentRedraws.TryGetValue(name, out var save))
{
*(CustomizationData*)customize = *(CustomizationData*)save.Customize.Address;
var equip = (CharacterEquip)equipData;
var newEquip = save.Equipment;
for (var i = 0; i < 10; ++i)
equip[i] = newEquip[i];
}
//*(uint*)modelId = 0;
//var human = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)addr;
//if (human->GameObject.ObjectKind is (byte)ObjectKind.EventNpc or (byte)ObjectKind.BattleNpc or (byte)ObjectKind.Player
// && human->ModelCharaId == 0)
//{
// var name = new Utf8String(human->GameObject.Name).ToString();
// if (FixedDesigns.EnabledDesigns.TryGetValue(name, out var designs))
// {
// var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(human->ClassJob));
// if (design != null)
// {
// if (design.Design.Data.WriteCustomizations)
// *(CharacterCustomization*)customize = design.Design.Data.Customizations;
//
// var data = (uint*)equipData;
// for (var i = 0u; i < 10; ++i)
// {
// var slot = i.ToEquipSlot();
// if (design.Design.Data.WriteEquipment.Fits(slot))
// data[i] = slot switch
// {
// EquipSlot.Head => design.Design.Data.Equipment.Head.Value,
// EquipSlot.Body => design.Design.Data.Equipment.Body.Value,
// EquipSlot.Hands => design.Design.Data.Equipment.Hands.Value,
// EquipSlot.Legs => design.Design.Data.Equipment.Legs.Value,
// EquipSlot.Feet => design.Design.Data.Equipment.Feet.Value,
// EquipSlot.Ears => design.Design.Data.Equipment.Ears.Value,
// EquipSlot.Neck => design.Design.Data.Equipment.Neck.Value,
// EquipSlot.Wrists => design.Design.Data.Equipment.Wrists.Value,
// EquipSlot.RFinger => design.Design.Data.Equipment.RFinger.Value,
// EquipSlot.LFinger => design.Design.Data.Equipment.LFinger.Value,
// _ => 0,
// };
// }
// }
// }
//}
}
//
// private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slotIdx, uint* data)
// {
// ulong ret;
// var slot = slotIdx.ToEquipSlot();
// try
// {
// if (slot != EquipSlot.Unknown)
// {
// var gameObject =
// (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject);
// if (gameObject != null)
// {
// var name = new Utf8String(gameObject->GameObject.Name).ToString();
// if (FixedDesigns.EnabledDesigns.TryGetValue(name, out var designs))
// {
// var design = designs.OrderBy(d => d.Jobs.Count).FirstOrDefault(d => d.Jobs.Fits(gameObject->ClassJob));
// if (design != null && design.Design.Data.WriteEquipment.Fits(slot))
// *data = slot switch
// {
// EquipSlot.Head => design.Design.Data.Equipment.Head.Value,
// EquipSlot.Body => design.Design.Data.Equipment.Body.Value,
// EquipSlot.Hands => design.Design.Data.Equipment.Hands.Value,
// EquipSlot.Legs => design.Design.Data.Equipment.Legs.Value,
// EquipSlot.Feet => design.Design.Data.Equipment.Feet.Value,
// EquipSlot.Ears => design.Design.Data.Equipment.Ears.Value,
// EquipSlot.Neck => design.Design.Data.Equipment.Neck.Value,
// EquipSlot.Wrists => design.Design.Data.Equipment.Wrists.Value,
// EquipSlot.RFinger => design.Design.Data.Equipment.RFinger.Value,
// EquipSlot.LFinger => design.Design.Data.Equipment.LFinger.Value,
// _ => 0,
// };
// }
// }
// }
// }
// finally
// {
// ret = FlagSlotForUpdateHook!.Original(drawObject, slotIdx, data);
// }
//
// return ret;
// }
//
// public void UpdateSlot(Human* drawObject, EquipSlot slot, CharacterArmor data)
// {
// var idx = slot.ToIndex();
// if (idx >= 10)
// return;
//
// FlagSlotForUpdateDetour(drawObject, idx, (uint*)&data);
// }
}