Initial Commit

This commit is contained in:
Ottermandias 2021-07-30 17:23:15 +02:00
commit 164f304cf6
38 changed files with 2796 additions and 0 deletions

102
.editorconfig Normal file
View file

@ -0,0 +1,102 @@
[*.proto]
indent_style=tab
indent_size=tab
tab_width=4
[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}]
indent_style=space
indent_size=4
tab_width=4
[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}]
indent_style=space
indent_size=2
tab_width=2
[*]
# Microsoft .NET properties
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_style_var_elsewhere=true:suggestion
csharp_style_var_for_built_in_types=true:suggestion
csharp_style_var_when_type_is_apparent=true:suggestion
dotnet_diagnostic.cs8632.severity=none
dotnet_diagnostic.cs8669.severity=none
dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:suggestion
dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:suggestion
dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:suggestion
dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion
dotnet_style_predefined_type_for_member_access=true:suggestion
dotnet_style_qualification_for_event=false:suggestion
dotnet_style_qualification_for_field=false:suggestion
dotnet_style_qualification_for_method=false:suggestion
dotnet_style_qualification_for_property=false:suggestion
dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion
# ReSharper properties
resharper_align_first_arg_by_paren=false
resharper_align_multiline_argument=true
resharper_align_multiline_binary_expressions_chain=false
resharper_align_multline_type_parameter_constrains=true
resharper_blank_lines_after_control_transfer_statements=1
resharper_blank_lines_around_single_line_type=0
resharper_braces_for_for=required_for_multiline
resharper_braces_for_foreach=required_for_multiline
resharper_braces_for_while=required_for_multiline
resharper_constructor_or_destructor_body=expression_body
resharper_csharp_align_multiline_argument=false
resharper_csharp_align_multiple_declaration=true
resharper_csharp_empty_block_style=together
resharper_csharp_insert_final_newline=true
resharper_csharp_max_line_length=144
resharper_csharp_wrap_before_binary_opsign=true
resharper_csharp_wrap_for_stmt_header_style=wrap_if_long
resharper_csharp_wrap_parameters_style=wrap_if_long
resharper_indent_nested_foreach_stmt=true
resharper_indent_nested_for_stmt=true
resharper_indent_nested_while_stmt=true
resharper_int_align=true
resharper_int_align_binary_expressions=false
resharper_int_align_parameters=false
resharper_keep_existing_embedded_arrangement=false
resharper_keep_existing_expr_member_arrangement=false
resharper_keep_existing_initializer_arrangement=false
resharper_keep_existing_switch_expression_arrangement=false
resharper_local_function_body=expression_body
resharper_max_enum_members_on_line=1
resharper_max_initializer_elements_on_line=1
resharper_method_or_operator_body=expression_body
resharper_outdent_binary_ops=true
resharper_outdent_dots=false
resharper_place_attribute_on_same_line=false
resharper_place_constructor_initializer_on_same_line=false
resharper_place_expr_accessor_on_single_line=true
resharper_place_expr_method_on_single_line=false
resharper_place_expr_property_on_single_line=false
resharper_place_simple_case_statement_on_same_line=if_owner_is_single_line
resharper_place_simple_embedded_statement_on_same_line=false
resharper_place_simple_enum_on_single_line=true
resharper_place_simple_switch_expression_on_single_line=true
resharper_space_within_single_line_array_initializer_braces=true
resharper_trailing_comma_in_multiline_lists=true
resharper_wrap_array_initializer_style=chop_always
resharper_wrap_before_arrow_with_expressions=true
resharper_wrap_chained_binary_expressions=chop_if_long
# ReSharper inspection severities
resharper_arrange_constructor_or_destructor_body_highlighting=hint
resharper_arrange_local_function_body_highlighting=hint
resharper_arrange_method_or_operator_body_highlighting=hint
resharper_arrange_missing_parentheses_highlighting=hint
resharper_arrange_redundant_parentheses_highlighting=hint
resharper_arrange_this_qualifier_highlighting=hint
resharper_arrange_type_member_modifiers_highlighting=hint
resharper_arrange_type_modifiers_highlighting=hint
resharper_built_in_type_reference_style_for_member_access_highlighting=hint
resharper_built_in_type_reference_style_highlighting=hint
resharper_compare_of_floats_by_equality_operator_highlighting=none
resharper_redundant_base_qualifier_highlighting=warning
resharper_suggest_var_or_type_built_in_types_highlighting=hint
resharper_suggest_var_or_type_elsewhere_highlighting=hint
resharper_suggest_var_or_type_simple_types_highlighting=hint

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
bin/
obj/
.vs/

View file

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

View file

@ -0,0 +1,23 @@
using System;
namespace Glamourer
{
[Flags]
public enum ActorEquipMask : ushort
{
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,
}
}

View file

@ -0,0 +1,124 @@
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Customization
{
[Sheet("CharaMakeParams")]
public class CharaMakeParams : ExcelRow
{
public const int NumMenus = 28;
public const int NumVoices = 12;
public const int NumGraphics = 10;
public const int MaxNumValues = 100;
public const int NumFaces = 8;
public const int NumFeatures = 7;
public const int NumEquip = 3;
public enum MenuType
{
ListSelector = 0,
IconSelector = 1,
ColorPicker = 2,
DoubleColorPicker = 3,
MultiIconSelector = 4,
Percentage = 5,
}
public struct Menu
{
public uint Id;
public byte InitVal;
public MenuType Type;
public byte Size;
public byte LookAt;
public uint Mask;
public CustomizationId Customization;
public uint[] Values;
public byte[] Graphic;
}
public struct FacialFeatures
{
public uint[] Icons;
}
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];
public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces];
public CharaMakeType.UnkStruct3347Struct[] Equip { get; set; } = new CharaMakeType.UnkStruct3347Struct[NumEquip];
public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language)
{
RowId = parser.Row;
SubRowId = parser.SubRow;
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);
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);
Menus[i].Values = new uint[Menus[i].Size];
switch (Menus[i].Type)
{
case MenuType.ColorPicker:
case MenuType.DoubleColorPicker:
case MenuType.Percentage:
break;
default:
for (var j = 0; j < Menus[i].Size; ++j)
Menus[i].Values[j] = parser.ReadColumn<uint>(3 + (7 + j) * NumMenus + i);
break;
}
Menus[i].Graphic = new byte[NumGraphics];
for (var j = 0; j < NumGraphics; ++j)
Menus[i].Graphic[j] = parser.ReadColumn<byte>(3 + (MaxNumValues + 7 + j) * NumMenus + i);
}
for (var i = 0; i < NumVoices; ++i)
Voices[i] = parser.ReadColumn<byte>(3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + i);
for (var i = 0; i < NumFaces; ++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);
}
for (var i = 0; i < NumEquip; ++i)
{
Equip[i] = new CharaMakeType.UnkStruct3347Struct
{
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),
};
}
}
}
}

View file

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

View file

@ -0,0 +1,32 @@
using System.Runtime.InteropServices;
namespace Glamourer.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)
{
Id = id;
Value = value;
IconId = data;
Color = data;
CustomizeId = customizeId;
}
}
}

View file

@ -0,0 +1,70 @@
using System;
namespace Glamourer.Customization
{
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 CharaMakeParams.MenuType ToType(this CustomizationId customizationId, bool isHrothgar = false)
=> 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.TailEarShape => CharaMakeParams.MenuType.IconSelector,
CustomizationId.BustSize => CharaMakeParams.MenuType.Percentage,
CustomizationId.FacePaint => CharaMakeParams.MenuType.IconSelector,
CustomizationId.FacePaintColor => CharaMakeParams.MenuType.ColorPicker,
CustomizationId.LipColor => isHrothgar ? CharaMakeParams.MenuType.IconSelector : CharaMakeParams.MenuType.ColorPicker,
_ => throw new ArgumentOutOfRangeException(nameof(customizationId), customizationId, null),
};
}
}

View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
{
public class CustomizationManager : ICustomizationManager
{
private static CustomizationOptions? _options;
private CustomizationManager()
{ }
public static ICustomizationManager Create(DalamudPluginInterface pi)
{
_options ??= new CustomizationOptions(pi);
return new CustomizationManager();
}
public IReadOnlyList<Race> Races
=> CustomizationOptions.Races;
public IReadOnlyList<SubRace> Clans
=> CustomizationOptions.Clans;
public IReadOnlyList<Gender> Genders
=> CustomizationOptions.Genders;
public CustomizationSet GetList(SubRace clan, Gender gender)
=> _options!.GetList(clan, gender);
public ImGuiScene.TextureWrap GetIcon(uint iconId)
=> _options!.GetIcon(iconId);
}
}

View file

@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud;
using Dalamud.Plugin;
using Glamourer.Util;
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Customization
{
public class CustomizationOptions
{
internal static readonly Race[] Races = ((Race[]) Enum.GetValues(typeof(Race))).Skip(1).ToArray();
internal static readonly SubRace[] Clans = ((SubRace[]) Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
internal static readonly Gender[] Genders =
{
Gender.Male,
Gender.Female,
};
internal CustomizationSet GetList(SubRace race, Gender gender)
=> _list[ToIndex(race, gender)];
internal ImGuiScene.TextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id);
private static readonly int ListSize = Clans.Length * Genders.Length;
private readonly CustomizationSet[] _list = new CustomizationSet[ListSize];
private readonly IconStorage _icons;
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)
{
var (skinOffset, hairOffset) = race switch
{
SubRace.Midlander => gender == Gender.Male ? (0x1200, 0x1300) : (0x0D00, 0x0E00),
SubRace.Highlander => gender == Gender.Male ? (0x1C00, 0x1D00) : (0x1700, 0x1800),
SubRace.Wildwood => gender == Gender.Male ? (0x2600, 0x2700) : (0x2100, 0x2200),
SubRace.Duskwright => gender == Gender.Male ? (0x3000, 0x3100) : (0x2B00, 0x2C00),
SubRace.Plainsfolk => gender == Gender.Male ? (0x3A00, 0x3B00) : (0x3500, 0x3600),
SubRace.Dunesfolk => gender == Gender.Male ? (0x4400, 0x4500) : (0x3F00, 0x4000),
SubRace.SeekerOfTheSun => gender == Gender.Male ? (0x4E00, 0x4F00) : (0x4900, 0x4A00),
SubRace.KeeperOfTheMoon => gender == Gender.Male ? (0x5800, 0x5900) : (0x5300, 0x5400),
SubRace.Seawolf => gender == Gender.Male ? (0x6200, 0x6300) : (0x5D00, 0x5E00),
SubRace.Hellsguard => gender == Gender.Male ? (0x6C00, 0x6D00) : (0x6700, 0x6800),
SubRace.Raen => gender == Gender.Male ? (0x7100, 0x7700) : (0x7600, 0x7200),
SubRace.Xaela => gender == Gender.Male ? (0x7B00, 0x8100) : (0x8000, 0x7C00),
SubRace.Hellion => gender == Gender.Male ? (0x8500, 0x8600) : (0x0000, 0x0000),
SubRace.Lost => gender == Gender.Male ? (0x8C00, 0x8F00) : (0x0000, 0x0000),
SubRace.Rava => gender == Gender.Male ? (0x0000, 0x0000) : (0x9E00, 0x9F00),
SubRace.Veena => gender == Gender.Male ? (0x0000, 0x0000) : (0xA800, 0xA900),
_ => throw new ArgumentOutOfRangeException(nameof(race), race, null),
};
return (CreateColorPicker(CustomizationId.SkinColor, skinOffset, 192),
CreateColorPicker(CustomizationId.HairColor, hairOffset, 192));
}
private Customization FromValueAndIndex(CustomizationId id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new Customization(id, (byte) index, value, 0)
: new Customization(id, row.FeatureID, row.Icon, (ushort) row.RowId);
}
private 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(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customization == CustomizationId.FacePaint)?.Values
.Select((v, i) => FromValueAndIndex(CustomizationId.FacePaint, v, i)).ToArray()
?? Array.Empty<Customization>();
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 = race.ToRace() == Race.Hrothgar ? HrothgarFaces(row) : 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(row),
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 = race.ToRace() == Race.Hrothgar ? set.HairStyles.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;
return set;
}
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet;
private readonly ExcelSheet<CharaMakeParams> _listSheet;
private readonly ExcelSheet<HairMakeType> _hairSheet;
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 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)
{
_cmpFile = new CmpFile(pi);
_customizeSheet = pi.Data.GetExcelSheet<CharaMakeCustomize>();
var tmp = pi.Data.Excel.GetType()!.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(typeof(CharaMakeParams))!.Invoke(pi.Data.Excel, new object?[]
{
"charamaketype",
FromClientLanguage(pi.ClientState.ClientLanguage),
null,
}) as ExcelSheet<CharaMakeParams>;
_listSheet = tmp!;
_hairSheet = pi.Data.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);
_icons = new IconStorage(pi, _list.Length * 50);
foreach (var race in Clans)
{
foreach (var gender in Genders)
_list[ToIndex(race, gender)] = GetSet(race, gender);
}
}
}
}

View file

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using Penumbra.GameData.Enums;
namespace Glamourer.Customization
{
public class CustomizationSet
{
public const int DefaultAvailable =
(1 << (int) CustomizationId.Height)
| (1 << (int) CustomizationId.Hairstyle)
| (1 << (int) CustomizationId.HighlightsOnFlag)
| (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.Viera && gender == Gender.Male
|| clan.ToRace() == Race.Hrothgar && gender == Gender.Female
? 0
: DefaultAvailable;
}
public Gender Gender { get; }
public SubRace Clan { get; }
public Race Race
=> Clan.ToRace();
private int _settingAvailable = DefaultAvailable;
internal void SetAvailable(CustomizationId id)
=> _settingAvailable |= 1 << (int) id;
public bool IsAvailable(CustomizationId id)
=> (_settingAvailable & (1 << (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 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<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 IReadOnlyDictionary<CustomizationId, string> OptionName { get; internal set; } = null!;
public Customization FacialFeature(int faceIdx, int idx)
=> FeaturesTattoos[faceIdx][idx];
public Customization Data(CustomizationId id, int idx)
{
if (idx > Count(id))
throw new IndexOutOfRangeException();
switch (id.ToType())
{
case CharaMakeParams.MenuType.Percentage: return new Customization(id, (byte) idx, 0, (ushort) idx);
case CharaMakeParams.MenuType.ListSelector: return new Customization(id, (byte) idx, 0, (ushort) idx);
}
return id switch
{
CustomizationId.Face => Faces[idx],
CustomizationId.Hairstyle => HairStyles[idx],
CustomizationId.TailEarShape => TailEarShapes[idx],
CustomizationId.FacePaint => FacePaints[idx],
CustomizationId.FacialFeaturesTattoos => FeaturesTattoos[0][idx],
CustomizationId.SkinColor => SkinColors[idx],
CustomizationId.EyeColorL => EyeColors[idx],
CustomizationId.EyeColorR => EyeColors[idx],
CustomizationId.HairColor => HairColors[idx],
CustomizationId.HighlightColor => HighlightColors[idx],
CustomizationId.TattooColor => TattooColors[idx],
CustomizationId.LipColor => idx < 96 ? LipColorsDark[idx] : LipColorsLight[idx - 96],
CustomizationId.FacePaintColor => idx < 96 ? FacePaintColorsDark[idx] : FacePaintColorsLight[idx - 96],
_ => new Customization(0, 0),
};
}
public int Count(CustomizationId id)
{
if (!IsAvailable(id))
return 0;
if (id.ToType() == CharaMakeParams.MenuType.Percentage)
return 101;
return id switch
{
CustomizationId.Face => Faces.Count,
CustomizationId.Hairstyle => HairStyles.Count,
CustomizationId.HighlightsOnFlag => 2,
CustomizationId.SkinColor => SkinColors.Count,
CustomizationId.EyeColorR => EyeColors.Count,
CustomizationId.HairColor => HairColors.Count,
CustomizationId.HighlightColor => HighlightColors.Count,
CustomizationId.FacialFeaturesTattoos => 8,
CustomizationId.TattooColor => TattooColors.Count,
CustomizationId.Eyebrows => NumEyebrows,
CustomizationId.EyeColorL => EyeColors.Count,
CustomizationId.EyeShape => NumEyeShapes,
CustomizationId.Nose => NumNoseShapes,
CustomizationId.Jaw => NumJawShapes,
CustomizationId.Mouth => NumMouthShapes,
CustomizationId.LipColor => LipColorsLight.Count + LipColorsDark.Count,
CustomizationId.TailEarShape => TailEarShapes.Count,
CustomizationId.FacePaint => FacePaints.Count,
CustomizationId.FacePaintColor => FacePaintColorsLight.Count + FacePaintColorsDark.Count,
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
};
}
}
}

View file

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

View file

@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>preview</LangVersion>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer.GameData</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DALAMUD_ROOT)\Dalamud.dll</HintPath>
<HintPath>..\libs\Dalamud.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DALAMUD_ROOT)\Lumina.dll</HintPath>
<HintPath>..\libs\Lumina.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DALAMUD_ROOT)\Lumina.Excel.dll</HintPath>
<HintPath>..\libs\Lumina.Excel.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Folder Include="Util\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>preview</LangVersion>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer.GameData</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<PropertyGroup>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DALAMUD_ROOT)\Dalamud.dll</HintPath>
<HintPath>..\libs\Dalamud.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DALAMUD_ROOT)\Lumina.dll</HintPath>
<HintPath>..\libs\Lumina.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DALAMUD_ROOT)\Lumina.Excel.dll</HintPath>
<HintPath>..\libs\Lumina.Excel.dll</HintPath>
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,45 @@
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer
{
public readonly struct Item
{
private static (SetId id, WeaponType type, ushort variant) ParseModel(EquipSlot slot, ulong data)
{
if (slot == EquipSlot.MainHand || slot == EquipSlot.OffHand)
{
var id = (SetId) (data & 0xFFFF);
var type = (WeaponType) ((data >> 16) & 0xFFFF);
var variant = (ushort) ((data >> 32) & 0xFFFF);
return (id, type, variant);
}
else
{
var id = (SetId) (data & 0xFFFF);
var variant = (byte) ((data >> 16) & 0xFF);
return (id, new WeaponType(), variant);
}
}
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;
}
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
namespace Glamourer
{
public static class GameData
{
private static Dictionary<byte, Stain>? _stains;
private static Dictionary<EquipSlot, List<Item>>? _itemsBySlot;
public static IReadOnlyDictionary<byte, Stain> Stains(DalamudPluginInterface pi)
{
if (_stains != null)
return _stains;
var sheet = pi.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.Stain>();
_stains = sheet.Where(s => s.Color != 0).ToDictionary(s => (byte) s.RowId, s => new Stain((byte) s.RowId, s));
return _stains;
}
public static IReadOnlyDictionary<EquipSlot, List<Item>> ItemsBySlot(DalamudPluginInterface pi)
{
if (_itemsBySlot != null)
return _itemsBySlot;
var sheet = pi.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.Item>();
Item EmptySlot(EquipSlot slot)
=> new(sheet.First(), "Nothing", slot);
_itemsBySlot = new Dictionary<EquipSlot, List<Item>>()
{
[EquipSlot.Head] = new(200) { EmptySlot(EquipSlot.Head) },
[EquipSlot.Body] = new(200) { EmptySlot(EquipSlot.Body) },
[EquipSlot.Hands] = new(200) { EmptySlot(EquipSlot.Hands) },
[EquipSlot.Legs] = new(200) { EmptySlot(EquipSlot.Legs) },
[EquipSlot.Feet] = new(200) { EmptySlot(EquipSlot.Feet) },
[EquipSlot.RFinger] = new(200) { EmptySlot(EquipSlot.RFinger) },
[EquipSlot.Neck] = new(200) { EmptySlot(EquipSlot.Neck) },
[EquipSlot.MainHand] = new(200) { EmptySlot(EquipSlot.MainHand) },
[EquipSlot.OffHand] = new(200) { EmptySlot(EquipSlot.OffHand) },
[EquipSlot.Wrists] = new(200) { EmptySlot(EquipSlot.Wrists) },
[EquipSlot.Ears] = new(200) { EmptySlot(EquipSlot.Ears) },
};
foreach (var item in sheet)
{
var name = item.Name.ToString();
if (!name.Any())
continue;
var slot = (EquipSlot) item.EquipSlotCategory.Row;
if (slot == EquipSlot.Unknown)
continue;
slot = slot.ToSlot();
if (!_itemsBySlot.TryGetValue(slot, out var list))
continue;
list.Add(new Item(item, name, slot));
}
foreach (var list in _itemsBySlot.Values)
list.Sort((i1, i2) => string.Compare(i1.Name, i2.Name, StringComparison.InvariantCulture));
_itemsBySlot[EquipSlot.LFinger] = _itemsBySlot[EquipSlot.RFinger];
return _itemsBySlot;
}
}
}

View file

@ -0,0 +1,41 @@
using Penumbra.GameData.Structs;
namespace Glamourer
{
public readonly struct Stain
{
public readonly string Name;
public readonly uint RgbaColor;
private readonly uint _seColorId;
public byte R
=> (byte) (RgbaColor & 0xFF);
public byte G
=> (byte) ((RgbaColor >> 8) & 0xFF);
public byte B
=> (byte) ((RgbaColor >> 16) & 0xFF);
public byte Intensity
=> (byte) ((1 + R + G + B) / 3);
public uint SeColor
=> _seColorId & 0x00FFFFFF;
public StainId RowIndex
=> (StainId) (_seColorId >> 24);
public static uint SeColorToRgba(uint color)
=> ((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);
}
}
}

View file

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

11
Glamourer.json Normal file
View file

@ -0,0 +1,11 @@
{
"Author": "Ottermandias",
"Name": "Glamourer",
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
"InternalName": "Glamourer",
"AssemblyVersion": "1.0.0.0",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 3,
"LoadPriority": -100
}

57
Glamourer.sln Normal file
View file

@ -0,0 +1,57 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29613.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer", "Glamourer\Glamourer.csproj", "{A5439F6B-83C1-4078-9371-354A147FF554}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{383AEE76-D423-431C-893A-7AB3DEA13630}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
repo.json = repo.json
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Glamourer.GameData", "Glamourer.GameData\Glamourer.GameData.csproj", "{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A5439F6B-83C1-4078-9371-354A147FF554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Debug|x64.ActiveCfg = Debug|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Debug|x64.Build.0 = Debug|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Debug|x86.ActiveCfg = Debug|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Debug|x86.Build.0 = Debug|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Release|Any CPU.Build.0 = Release|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Release|x64.ActiveCfg = Release|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Release|x64.Build.0 = Release|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Release|x86.ActiveCfg = Release|Any CPU
{A5439F6B-83C1-4078-9371-354A147FF554}.Release|x86.Build.0 = Release|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|x64.ActiveCfg = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|x64.Build.0 = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|x86.ActiveCfg = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Debug|x86.Build.0 = Debug|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|Any CPU.Build.0 = Release|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|x64.ActiveCfg = Release|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|x64.Build.0 = Release|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|x86.ActiveCfg = Release|Any CPU
{51F4DDB0-1FA0-4629-9CFE-C55B6062907B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {37674B88-2038-4C63-A979-84404391773A}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="H:\Projects\FFPlugins\Penumbra\Penumbra\bin\Debug\net472\Penumbra.GameData.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String></wpf:ResourceDictionary>

BIN
Glamourer.zip Normal file

Binary file not shown.

23
Glamourer/CmpFile.cs Normal file
View file

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

View file

@ -0,0 +1,98 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<LangVersion>preview</LangVersion>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer</AssemblyName>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Company>SoftOtter</Company>
<Product>Glamourer</Product>
<Copyright>Copyright © 2020</Copyright>
<Deterministic>true</Deterministic>
<OutputType>Library</OutputType>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<OutputPath>bin\$(Configuration)\</OutputPath>
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
</Reference>
<Reference Include="SDL2-CS">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\SDL2-CS.dll</HintPath>
</Reference>
<Reference Include="Lumina">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(appdata)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
</Reference>
<Reference Include="Penumbra.GameData">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.GameData.dll</HintPath>
</Reference>
<Reference Include="Penumbra.Api">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.Api.dll</HintPath>
</Reference>
<Reference Include="Penumbra.PlayerWatch">
<HintPath>..\..\Penumbra\Penumbra\bin\$(Configuration)\net472\Penumbra.PlayerWatch.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.3</Version>
</PackageReference>
<PackageReference Include="System.Memory" Version="4.5.3" />
</ItemGroup>
<ItemGroup>
<None Include="Glamourer.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="if $(Configuration) == Release powershell Compress-Archive -Force $(TargetPath), $(TargetDir)$(SolutionName).json, $(TargetDir)$(SolutionName).GameData.dll $(SolutionDir)$(SolutionName).zip" />
<Exec Command="if $(Configuration) == Release powershell Copy-Item -Force $(TargetDir)$(SolutionName).json -Destination $(SolutionDir)" />
</Target>
</Project>

11
Glamourer/Glamourer.json Normal file
View file

@ -0,0 +1,11 @@
{
"Author": "Ottermandias",
"Name": "Glamourer",
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
"InternalName": "Glamourer",
"AssemblyVersion": "1.0.0.0",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 3,
"LoadPriority": -100
}

View file

@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using ImGuiNET;
namespace Glamourer.Gui
{
public class ComboWithFilter<T>
{
private readonly string _label;
private readonly string _filterLabel;
private readonly string _listLabel;
private string _currentFilter = string.Empty;
private string _currentFilterLower = string.Empty;
private bool _focus;
private readonly float _size;
private readonly IReadOnlyList<T> _items;
private readonly IReadOnlyList<string> _itemNamesLower;
private readonly Func<T, string> _itemToName;
public Action? PrePreview = null;
public Action? PostPreview = null;
public Func<T, bool>? CreateSelectable = null;
public Action? PreList = null;
public Action? PostList = null;
public float? HeightPerItem = null;
private float _heightPerItem;
public ImGuiComboFlags Flags { get; set; } = ImGuiComboFlags.None;
public int ItemsAtOnce { get; set; } = 12;
public ComboWithFilter(string label, float size, IReadOnlyList<T> items, Func<T, string> itemToName)
{
_label = label;
_filterLabel = $"##_{label}_filter";
_listLabel = $"##_{label}_list";
_itemToName = itemToName;
_items = items;
_size = size;
_itemNamesLower = _items.Select(i => _itemToName(i).ToLowerInvariant()).ToList();
}
public ComboWithFilter(string label, ComboWithFilter<T> other)
{
_label = label;
_filterLabel = $"##_{label}_filter";
_listLabel = $"##_{label}_list";
_itemToName = other._itemToName;
_items = other._items;
_itemNamesLower = other._itemNamesLower;
_size = other._size;
PrePreview = other.PrePreview;
PostPreview = other.PostPreview;
CreateSelectable = other.CreateSelectable;
PreList = other.PreList;
PostList = other.PostList;
HeightPerItem = other.HeightPerItem;
Flags = other.Flags;
}
private bool DrawList(string currentName, out int numItems, out int nodeIdx, ref T? value)
{
numItems = ItemsAtOnce;
nodeIdx = -1;
if (!ImGui.BeginChild(_listLabel, new Vector2(_size, ItemsAtOnce * _heightPerItem)))
return false;
var ret = false;
try
{
if (!_focus)
{
ImGui.SetScrollY(0);
_focus = true;
}
var scrollY = Math.Max((int) (ImGui.GetScrollY() / _heightPerItem) - 1, 0);
var restHeight = scrollY * _heightPerItem;
numItems = 0;
nodeIdx = 0;
if (restHeight > 0)
ImGui.Dummy(Vector2.UnitY * restHeight);
for (var i = scrollY; i < _items.Count; ++i)
{
if (!_itemNamesLower[i].Contains(_currentFilterLower))
continue;
++numItems;
if (numItems <= ItemsAtOnce + 2)
{
nodeIdx = i;
var item = _items[i]!;
var success = false;
if (CreateSelectable != null)
{
success = CreateSelectable(item);
}
else
{
var name = _itemToName(item);
success = ImGui.Selectable(name, name == currentName);
}
if (success)
{
value = item;
ImGui.CloseCurrentPopup();
ret = true;
}
}
}
if (numItems > ItemsAtOnce + 2)
ImGui.Dummy(Vector2.UnitY * (numItems - ItemsAtOnce - 2) * _heightPerItem);
}
finally
{
ImGui.EndChild();
}
return ret;
}
public bool Draw(string currentName, out T? value)
{
value = default;
ImGui.SetNextItemWidth(_size);
PrePreview?.Invoke();
if (!ImGui.BeginCombo(_label, currentName, Flags))
{
_focus = false;
_currentFilter = string.Empty;
_currentFilterLower = string.Empty;
PostPreview?.Invoke();
return false;
}
PostPreview?.Invoke();
_heightPerItem = HeightPerItem ?? ImGui.GetTextLineHeightWithSpacing();
var ret = false;
try
{
ImGui.SetNextItemWidth(-1);
if (ImGui.InputTextWithHint(_filterLabel, "Filter...", ref _currentFilter, 255))
_currentFilterLower = _currentFilter.ToLowerInvariant();
var isFocused = ImGui.IsItemActive();
if (!_focus)
ImGui.SetKeyboardFocusHere();
PreList?.Invoke();
ret = DrawList(currentName, out var numItems, out var nodeIdx, ref value);
PostList?.Invoke();
if (!isFocused && numItems <= 1 && nodeIdx >= 0)
{
value = _items[nodeIdx];
ret = true;
ImGui.CloseCurrentPopup();
}
}
finally
{
ImGui.EndCombo();
}
return ret;
}
}
}

154
Glamourer/Gui/ImGuiRaii.cs Normal file
View file

@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using ImGuiNET;
namespace Glamourer.Gui
{
public sealed class ImGuiRaii : IDisposable
{
private int _colorStack = 0;
private int _fontStack = 0;
private int _styleStack = 0;
private float _indentation = 0f;
private Stack<Action>? _onDispose = null;
public ImGuiRaii()
{ }
public static ImGuiRaii NewGroup()
=> new ImGuiRaii().Group();
public ImGuiRaii Group()
=> Begin(ImGui.BeginGroup, ImGui.EndGroup);
public static ImGuiRaii NewTooltip()
=> new ImGuiRaii().Tooltip();
public ImGuiRaii Tooltip()
=> Begin(ImGui.BeginTooltip, ImGui.EndTooltip);
public ImGuiRaii PushColor(ImGuiCol which, uint color)
{
ImGui.PushStyleColor(which, color);
++_colorStack;
return this;
}
public ImGuiRaii PushColor(ImGuiCol which, Vector4 color)
{
ImGui.PushStyleColor(which, color);
++_colorStack;
return this;
}
public ImGuiRaii PopColors(int n = 1)
{
var actualN = Math.Min(n, _colorStack);
if (actualN > 0)
{
ImGui.PopStyleColor(actualN);
_colorStack -= actualN;
}
return this;
}
public ImGuiRaii PushStyle(ImGuiStyleVar style, Vector2 value)
{
ImGui.PushStyleVar(style, value);
++_styleStack;
return this;
}
public ImGuiRaii PushStyle(ImGuiStyleVar style, float value)
{
ImGui.PushStyleVar(style, value);
++_styleStack;
return this;
}
public ImGuiRaii PopStyles(int n = 1)
{
var actualN = Math.Min(n, _styleStack);
if (actualN > 0)
{
ImGui.PopStyleVar(actualN);
_styleStack -= actualN;
}
return this;
}
public ImGuiRaii PushFont(ImFontPtr font)
{
ImGui.PushFont(font);
++_fontStack;
return this;
}
public ImGuiRaii PopFonts(int n = 1)
{
var actualN = Math.Min(n, _fontStack);
while (actualN-- > 0)
{
ImGui.PopFont();
--_fontStack;
}
return this;
}
public ImGuiRaii Indent(float width)
{
if (width != 0)
{
ImGui.Indent(width);
_indentation += width;
}
return this;
}
public ImGuiRaii Unindent(float width)
=> Indent(-width);
public bool Begin(Func<bool> begin, Action end)
{
if (begin())
{
_onDispose ??= new Stack<Action>();
_onDispose.Push(end);
return true;
}
return false;
}
public ImGuiRaii Begin(Action begin, Action end)
{
begin();
_onDispose ??= new Stack<Action>();
_onDispose.Push(end);
return this;
}
public void End(int n = 1)
{
var actualN = Math.Min(n, _onDispose?.Count ?? 0);
while(actualN-- > 0)
_onDispose!.Pop()();
}
public void Dispose()
{
Unindent(_indentation);
PopColors(_colorStack);
PopStyles(_styleStack);
PopFonts(_fontStack);
if (_onDispose != null)
{
End(_onDispose.Count);
_onDispose = null;
}
}
}
}

326
Glamourer/Gui/Interface.cs Normal file
View file

@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Data.LuminaExtensions;
using Dalamud.Game.ClientState.Actors;
using Dalamud.Game.ClientState.Actors.Types;
using Glamourer.Customization;
using ImGuiNET;
using Penumbra.Api;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.PlayerWatch;
using SDL2;
namespace Glamourer.Gui
{
internal class Interface : IDisposable
{
public const int GPoseActorId = 201;
private const string PluginName = "Glamourer";
private readonly string _glamourerHeader;
private const int ColorButtonWidth = 140;
private readonly IReadOnlyDictionary<byte, Stain> _stains;
private readonly IReadOnlyDictionary<EquipSlot, List<Item>> _equip;
private readonly ActorTable _actors;
private readonly IObjectIdentifier _identifier;
private readonly Dictionary<EquipSlot, (ComboWithFilter<Item>, ComboWithFilter<Stain>)> _combos;
private readonly IPlayerWatcher _playerWatcher;
private bool _visible = false;
private Actor? _player;
private static readonly Vector2 FeatureIconSize = new(80, 80);
public Interface()
{
_glamourerHeader = GlamourerPlugin.Version.Length > 0
? $"{PluginName} v{GlamourerPlugin.Version}###{PluginName}Main"
: $"{PluginName}###{PluginName}Main";
GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi += Draw;
GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi += ToggleVisibility;
_stains = GameData.Stains(GlamourerPlugin.PluginInterface);
_equip = GameData.ItemsBySlot(GlamourerPlugin.PluginInterface);
_identifier = Penumbra.GameData.GameData.GetIdentifier(GlamourerPlugin.PluginInterface);
_actors = GlamourerPlugin.PluginInterface.ClientState.Actors;
_playerWatcher = PlayerWatchFactory.Create(GlamourerPlugin.PluginInterface);
var stainCombo = new ComboWithFilter<Stain>("##StainCombo", ColorButtonWidth, _stains.Values.ToArray(),
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 * (ColorButtonWidth - ImGui.GetStyle().ScrollbarSize));
ImGui.PopStyleColor(push);
return ret;
},
ItemsAtOnce = 12,
};
_combos = _equip.ToDictionary(kvp => kvp.Key,
kvp => (new ComboWithFilter<Item>($"{kvp.Key}##Equip", 300, kvp.Value, i => i.Name) { Flags = ImGuiComboFlags.HeightLarge }
, new ComboWithFilter<Stain>($"##{kvp.Key}Stain", stainCombo))
);
}
public void ToggleVisibility(object _, object _2)
=> _visible = !_visible;
public void Dispose()
{
_playerWatcher?.Dispose();
GlamourerPlugin.PluginInterface.UiBuilder.OnBuildUi -= Draw;
GlamourerPlugin.PluginInterface.UiBuilder.OnOpenConfigUi -= ToggleVisibility;
}
private string _currentActorName = "";
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;
}
private bool DrawColorSelector(ComboWithFilter<Stain> stainCombo, EquipSlot slot, StainId stainIdx)
{
var name = string.Empty;
stainCombo.PostPreview = null;
if (_stains.TryGetValue((byte) stainIdx, out var stain))
{
name = stain.Name;
var previewPush = PushColor(stain, ImGuiCol.FrameBg);
stainCombo.PostPreview = () => ImGui.PopStyleColor(previewPush);
}
if (stainCombo.Draw(name, out var newStain) && _player != null)
{
newStain.Write(_player.Address, slot);
return true;
}
return false;
}
private bool DrawItemSelector(ComboWithFilter<Item> equipCombo, Lumina.Excel.GeneratedSheets.Item? item)
{
var currentName = item?.Name.ToString() ?? "Nothing";
if (equipCombo.Draw(currentName, out var newItem) && _player != null)
{
newItem.Write(_player.Address);
return true;
}
return false;
}
private bool DrawEquip(EquipSlot slot, ActorArmor equip)
{
var (equipCombo, stainCombo) = _combos[slot];
var ret = false;
ret = DrawColorSelector(stainCombo, slot, equip.Stain);
ImGui.SameLine();
var item = _identifier.Identify(equip.Set, new WeaponType(), equip.Variant, slot);
ret |= DrawItemSelector(equipCombo, item);
return ret;
}
private bool DrawWeapon(EquipSlot slot, ActorWeapon weapon)
{
var (equipCombo, stainCombo) = _combos[slot];
var ret = DrawColorSelector(stainCombo, slot, weapon.Stain);
ImGui.SameLine();
var item = _identifier.Identify(weapon.Set, weapon.Type, weapon.Variant, slot);
ret |= DrawItemSelector(equipCombo, item);
return ret;
}
public void UpdateActors(Actor actor)
{
var newEquip = _playerWatcher.UpdateActorWithoutEvent(actor);
GlamourerPlugin.Penumbra?.RedrawActor(actor, RedrawType.WithSettings);
var gPose = _actors[GPoseActorId];
var player = _actors[0];
if (gPose != null && actor.Address == gPose.Address && player != null)
newEquip.Write(player.Address);
}
private SubRace _currentSubRace = SubRace.Midlander;
private Gender _currentGender = Gender.Male;
private CustomizationId _currentCustomization = CustomizationId.Hairstyle;
private static readonly string[]
SubRaceNames = ((SubRace[]) Enum.GetValues(typeof(SubRace))).Skip(1).Select(s => s.ToName()).ToArray();
private void DrawStuff()
{
if (ImGui.BeginCombo("SubRace", _currentSubRace.ToString()))
{
for (var i = 0; i < SubRaceNames.Length; ++i)
{
if (ImGui.Selectable(SubRaceNames[i], (int) _currentSubRace == i + 1))
_currentSubRace = (SubRace) (i + 1);
}
ImGui.EndCombo();
}
if (ImGui.BeginCombo("Gender", _currentGender.ToName()))
{
if (ImGui.Selectable(Gender.Male.ToName(), _currentGender == Gender.Male))
_currentGender = Gender.Male;
if (ImGui.Selectable(Gender.Female.ToName(), _currentGender == Gender.Female))
_currentGender = Gender.Female;
ImGui.EndCombo();
}
var set = GlamourerPlugin.Customization.GetList(_currentSubRace, _currentGender);
if (ImGui.BeginCombo("Customization", _currentCustomization.ToString()))
{
foreach (CustomizationId customizationId in Enum.GetValues(typeof(CustomizationId)))
{
if (!set.IsAvailable(customizationId))
continue;
if (ImGui.Selectable(customizationId.ToString(), customizationId == _currentCustomization))
_currentCustomization = customizationId;
}
ImGui.EndCombo();
}
var count = set.Count(_currentCustomization);
var tmp = 0;
switch (_currentCustomization.ToType(_currentSubRace.ToRace() == Race.Hrothgar))
{
case CharaMakeParams.MenuType.ColorPicker:
{
using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.PushStyle(ImGuiStyleVar.FrameRounding, 0f);
for (var i = 0; i < count; ++i)
{
var data = set.Data(_currentCustomization, i);
ImGui.ColorButton($"{data.Value}", ImGui.ColorConvertU32ToFloat4(data.Color));
if (i % 8 != 7)
ImGui.SameLine();
}
}
break;
case CharaMakeParams.MenuType.Percentage:
ImGui.SliderInt("Percentage", ref tmp, 0, 100);
break;
case CharaMakeParams.MenuType.ListSelector:
ImGui.Combo("List", ref tmp, Enumerable.Range(0, count).Select(i => $"{_currentCustomization} #{i}").ToArray(), count);
break;
case CharaMakeParams.MenuType.IconSelector:
case CharaMakeParams.MenuType.MultiIconSelector:
{
using var raii = new ImGuiRaii().PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.PushStyle(ImGuiStyleVar.FrameRounding, 0f);
for (var i = 0; i < count; ++i)
{
var data = set.Data(_currentCustomization, i);
var texture = GlamourerPlugin.Customization.GetIcon(data.IconId);
ImGui.ImageButton(texture.ImGuiHandle, FeatureIconSize * ImGui.GetIO().FontGlobalScale);
if (ImGui.IsItemHovered())
{
using var tooltip = ImGuiRaii.NewTooltip();
ImGui.Image(texture.ImGuiHandle, new Vector2(texture.Width, texture.Height));
}
if (i % 4 != 3)
ImGui.SameLine();
}
}
break;
}
}
private void Draw()
{
ImGui.SetNextWindowSizeConstraints(Vector2.One * 600, Vector2.One * 5000);
if (!_visible || !ImGui.Begin(_glamourerHeader))
return;
try
{
if (ImGui.BeginCombo("Actor", _currentActorName))
{
var idx = 0;
foreach (var actor in GlamourerPlugin.PluginInterface.ClientState.Actors.Where(a => a.ObjectKind == ObjectKind.Player))
{
if (ImGui.Selectable($"{actor.Name}##{idx++}"))
_currentActorName = actor.Name;
}
ImGui.EndCombo();
}
_player = _actors[GPoseActorId] ?? _actors[0];
if (_player == null || !GlamourerPlugin.PluginInterface.ClientState.Condition.Any())
{
ImGui.TextColored(new Vector4(0.4f, 0.1f, 0.1f, 1f),
"No player character available.");
}
else
{
var equip = new ActorEquipment(_player);
var changes = false;
changes |= DrawWeapon(EquipSlot.MainHand, equip.MainHand);
changes |= DrawWeapon(EquipSlot.OffHand, equip.OffHand);
changes |= DrawEquip(EquipSlot.Head, equip.Head);
changes |= DrawEquip(EquipSlot.Body, equip.Body);
changes |= DrawEquip(EquipSlot.Hands, equip.Hands);
changes |= DrawEquip(EquipSlot.Legs, equip.Legs);
changes |= DrawEquip(EquipSlot.Feet, equip.Feet);
changes |= DrawEquip(EquipSlot.Ears, equip.Ears);
changes |= DrawEquip(EquipSlot.Neck, equip.Neck);
changes |= DrawEquip(EquipSlot.Wrists, equip.Wrists);
changes |= DrawEquip(EquipSlot.RFinger, equip.RFinger);
changes |= DrawEquip(EquipSlot.LFinger, equip.LFinger);
if (changes)
UpdateActors(_player);
}
DrawStuff();
}
finally
{
ImGui.End();
}
}
}
}

170
Glamourer/Main.cs Normal file
View file

@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Dalamud.Game.Command;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.Gui;
using ImGuiNET;
using Penumbra.Api;
using CommandManager = Glamourer.Managers.CommandManager;
namespace Glamourer
{
internal class Glamourer
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly CommandManager _commands;
public Glamourer(DalamudPluginInterface pi)
{
_pluginInterface = pi;
_commands = new CommandManager(_pluginInterface);
}
}
public class GlamourerPlugin : IDalamudPlugin
{
public const int RequiredPenumbraShareVersion = 1;
public string Name
=> "Glamourer";
public static DalamudPluginInterface PluginInterface = null!;
private Glamourer _glamourer = null!;
private Interface _interface = null!;
public static ICustomizationManager Customization = null!;
public static string Version = string.Empty;
public static IPenumbraApi? Penumbra;
private Dalamud.Dalamud _dalamud = null!;
private List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> _plugins = null!;
private void SetDalamud(DalamudPluginInterface pi)
{
var dalamud = (Dalamud.Dalamud?) pi.GetType()
?.GetField("dalamud", BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(pi);
_dalamud = dalamud ?? throw new Exception("Could not obtain Dalamud.");
}
private void PenumbraTooltip(object? it)
{
if (it is Lumina.Excel.GeneratedSheets.Item)
ImGui.Text("Right click to apply to current Glamourer Set. [Glamourer]");
}
private void PenumbraRightClick(MouseButton button, object? it)
{
if (button == MouseButton.Right && it is Lumina.Excel.GeneratedSheets.Item item)
{
var actors = PluginInterface.ClientState.Actors;
var player = actors[Interface.GPoseActorId] ?? actors[0];
if (player != null)
{
var writeItem = new Item(item, string.Empty);
writeItem.Write(player.Address);
_interface.UpdateActors(player);
}
}
}
private void RegisterFunctions()
{
if (Penumbra == null || !Penumbra.Valid)
return;
Penumbra!.ChangedItemTooltip += PenumbraTooltip;
Penumbra!.ChangedItemClicked += PenumbraRightClick;
}
private void UnregisterFunctions()
{
if (Penumbra == null || !Penumbra.Valid)
return;
Penumbra!.ChangedItemTooltip -= PenumbraTooltip;
Penumbra!.ChangedItemClicked -= PenumbraRightClick;
}
private void SetPlugins(DalamudPluginInterface pi)
{
var pluginManager = _dalamud?.GetType()
?.GetProperty("PluginManager", BindingFlags.Instance | BindingFlags.NonPublic)
?.GetValue(_dalamud);
if (pluginManager == null)
throw new Exception("Could not obtain plugin manager.");
var pluginsList =
(List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)>?) pluginManager
?.GetType()
?.GetProperty("Plugins", BindingFlags.Instance | BindingFlags.Public)
?.GetValue(pluginManager);
_plugins = pluginsList ?? throw new Exception("Could not obtain Dalamud.");
}
private bool GetPenumbra()
{
if (Penumbra?.Valid ?? false)
return true;
var plugin = _plugins.Find(p
=> p.Definition.InternalName == "Penumbra"
&& string.Compare(p.Definition.AssemblyVersion, "0.4.0.3", StringComparison.Ordinal) >= 0).Plugin;
var penumbra = (IPenumbraApiBase?) plugin?.GetType().GetProperty("Api", BindingFlags.Instance | BindingFlags.Public)
?.GetValue(plugin);
if (penumbra != null && penumbra.Valid && penumbra.ApiVersion >= RequiredPenumbraShareVersion)
{
Penumbra = (IPenumbraApi) penumbra!;
RegisterFunctions();
}
else
{
Penumbra = null;
}
return Penumbra != null;
}
public void Initialize(DalamudPluginInterface pluginInterface)
{
Version = Assembly.GetExecutingAssembly()?.GetName().Version.ToString() ?? "";
PluginInterface = pluginInterface;
Customization = CustomizationManager.Create(PluginInterface);
SetDalamud(PluginInterface);
SetPlugins(PluginInterface);
GetPenumbra();
PluginInterface.CommandManager.AddHandler("/glamour", new CommandInfo(OnCommand)
{
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
});
_glamourer = new Glamourer(PluginInterface);
_interface = new Interface();
}
public void OnCommand(string command, string arguments)
{
if (GetPenumbra())
Penumbra!.RedrawAll(RedrawType.WithSettings);
else
PluginLog.Information("Could not get Penumbra.");
}
public void Dispose()
{
UnregisterFunctions();
_interface?.Dispose();
PluginInterface.CommandManager.RemoveHandler("/glamour");
PluginInterface.Dispose();
}
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Plugin;
using Glamourer.SeFunctions;
namespace Glamourer.Managers
{
public class CommandManager
{
private readonly ProcessChatBox _processChatBox;
private readonly Dalamud.Game.Command.CommandManager _dalamudCommands;
private readonly IntPtr _uiModulePtr;
public CommandManager(DalamudPluginInterface pi, BaseUiObject baseUiObject, GetUiModule getUiModule, ProcessChatBox processChatBox)
{
_dalamudCommands = pi.CommandManager;
_processChatBox = processChatBox;
_uiModulePtr = getUiModule.Invoke(Marshal.ReadIntPtr(baseUiObject.Address));
}
public CommandManager(DalamudPluginInterface pi)
: this(pi, new BaseUiObject(pi.TargetModuleScanner), new GetUiModule(pi.TargetModuleScanner),
new ProcessChatBox(pi.TargetModuleScanner))
{ }
public bool Execute(string message)
{
// First try to process the command through Dalamud.
if (_dalamudCommands.ProcessCommand(message))
{
PluginLog.Verbose("Executed Dalamud command \"{Message:l}\".", message);
return true;
}
if (_uiModulePtr == IntPtr.Zero)
{
PluginLog.Error("Can not execute \"{Message:l}\" because no uiModulePtr is available.", message);
return false;
}
// Then prepare a string to send to the game itself.
var (text, length) = PrepareString(message);
var payload = PrepareContainer(text, length);
_processChatBox.Invoke(_uiModulePtr, payload, IntPtr.Zero, (byte) 0);
Marshal.FreeHGlobal(payload);
Marshal.FreeHGlobal(text);
return false;
}
private static (IntPtr, long) PrepareString(string message)
{
var bytes = Encoding.UTF8.GetBytes(message);
var mem = Marshal.AllocHGlobal(bytes.Length + 30);
Marshal.Copy(bytes, 0, mem, bytes.Length);
Marshal.WriteByte(mem + bytes.Length, 0);
return (mem, bytes.Length + 1);
}
private static IntPtr PrepareContainer(IntPtr message, long length)
{
var mem = Marshal.AllocHGlobal(400);
Marshal.WriteInt64(mem, message.ToInt64());
Marshal.WriteInt64(mem + 0x8, 64);
Marshal.WriteInt64(mem + 0x10, length);
Marshal.WriteInt64(mem + 0x18, 0);
return mem;
}
}
}

View file

@ -0,0 +1,11 @@
using Dalamud.Game;
namespace Glamourer.SeFunctions
{
public sealed class BaseUiObject : SeAddressBase
{
public BaseUiObject(SigScanner sigScanner)
: base(sigScanner, "48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 48 83 C1 10 E8")
{ }
}
}

View file

@ -0,0 +1,14 @@
using System;
using Dalamud.Game;
namespace Glamourer.SeFunctions
{
public delegate IntPtr GetUiModuleDelegate(IntPtr baseUiObj);
public sealed class GetUiModule : SeFunctionBase<GetUiModuleDelegate>
{
public GetUiModule(SigScanner sigScanner)
: base(sigScanner, "E8 ?? ?? ?? ?? 48 83 7F ?? 00 48 8B F0")
{ }
}
}

View file

@ -0,0 +1,14 @@
using System;
using Dalamud.Game;
namespace Glamourer.SeFunctions
{
public delegate IntPtr ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unk1, byte unk2);
public sealed class ProcessChatBox : SeFunctionBase<ProcessChatBoxDelegate>
{
public ProcessChatBox(SigScanner sigScanner)
: base(sigScanner, "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9")
{ }
}
}

View file

@ -0,0 +1,20 @@
using System;
using Dalamud.Game;
using Dalamud.Plugin;
namespace Glamourer.SeFunctions
{
public class SeAddressBase
{
public readonly IntPtr Address;
public SeAddressBase(SigScanner sigScanner, string signature, int offset = 0)
{
Address = sigScanner.GetStaticAddressFromSig(signature);
if (Address != IntPtr.Zero)
Address += offset;
var baseOffset = (ulong) Address.ToInt64() - (ulong) sigScanner.Module.BaseAddress.ToInt64();
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{baseOffset:X16}.");
}
}
}

View file

@ -0,0 +1,75 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Hooking;
using Dalamud.Plugin;
namespace Glamourer.SeFunctions
{
public class SeFunctionBase<T> where T : Delegate
{
public IntPtr Address;
protected T? FuncDelegate;
public SeFunctionBase(SigScanner sigScanner, int offset)
{
Address = sigScanner.Module.BaseAddress + offset;
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{offset:X16}.");
}
public SeFunctionBase(SigScanner sigScanner, string signature, int offset = 0)
{
Address = sigScanner.ScanText(signature);
if (Address != IntPtr.Zero)
Address += offset;
var baseOffset = (ulong) Address.ToInt64() - (ulong) sigScanner.Module.BaseAddress.ToInt64();
PluginLog.Debug($"{GetType().Name} address 0x{Address.ToInt64():X16}, baseOffset 0x{baseOffset:X16}.");
}
public T? Delegate()
{
if (FuncDelegate != null)
return FuncDelegate;
if (Address != IntPtr.Zero)
{
FuncDelegate = Marshal.GetDelegateForFunctionPointer<T>(Address);
return FuncDelegate;
}
PluginLog.Error($"Trying to generate delegate for {GetType().Name}, but no pointer available.");
return null;
}
public dynamic? Invoke(params dynamic[] parameters)
{
if (FuncDelegate != null)
return FuncDelegate.DynamicInvoke(parameters);
if (Address != IntPtr.Zero)
{
FuncDelegate = Marshal.GetDelegateForFunctionPointer<T>(Address);
return FuncDelegate!.DynamicInvoke(parameters);
}
else
{
PluginLog.Error($"Trying to call {GetType().Name}, but no pointer available.");
return null;
}
}
public Hook<T>? CreateHook(T detour)
{
if (Address != IntPtr.Zero)
{
var hook = new Hook<T>(Address, detour);
hook.Enable();
PluginLog.Debug($"Hooked onto {GetType().Name} at address 0x{Address.ToInt64():X16}.");
return hook;
}
PluginLog.Error($"Trying to create Hook for {GetType().Name}, but no pointer available.");
return null;
}
}
}

201
LICENSE Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

0
README.md Normal file
View file

19
repo.json Normal file
View file

@ -0,0 +1,19 @@
[
{
"Author": "Ottermandias",
"Name": "Glamourer",
"Description": "Adds functionality to change appearance of actors. Requires Penumbra to be installed and activated to work.",
"InternalName": "Glamourer",
"AssemblyVersion": "0.0.1.0",
"TestingAssemblyVersion": "0.0.1.0",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 3,
"IsHide": "False",
"IsTestingExclusive": "false",
"DownloadCount": 1,
"LastUpdate": 1618608322,
"DownloadLinkTesting": "https://github.com/Ottermandias/Glamourer/raw/main/GatherBuddy.zip",
"DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/raw/main/GatherBuddy.zip",
}
]