mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 02:07:25 +01:00
Initial Commit
This commit is contained in:
commit
164f304cf6
38 changed files with 2796 additions and 0 deletions
102
.editorconfig
Normal file
102
.editorconfig
Normal 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
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
168
Glamourer.GameData/ActorEquipExtensions.cs
Normal file
168
Glamourer.GameData/ActorEquipExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Glamourer.GameData/ActorEquipMask.cs
Normal file
23
Glamourer.GameData/ActorEquipMask.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
124
Glamourer.GameData/Customization/CharaMakeParams.cs
Normal file
124
Glamourer.GameData/Customization/CharaMakeParams.cs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Glamourer.GameData/Customization/CmpFile.cs
Normal file
23
Glamourer.GameData/Customization/CmpFile.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Glamourer.GameData/Customization/Customization.cs
Normal file
32
Glamourer.GameData/Customization/Customization.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Glamourer.GameData/Customization/CustomizationId.cs
Normal file
70
Glamourer.GameData/Customization/CustomizationId.cs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
35
Glamourer.GameData/Customization/CustomizationManager.cs
Normal file
35
Glamourer.GameData/Customization/CustomizationManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
264
Glamourer.GameData/Customization/CustomizationOptions.cs
Normal file
264
Glamourer.GameData/Customization/CustomizationOptions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
Glamourer.GameData/Customization/CustomizationSet.cs
Normal file
140
Glamourer.GameData/Customization/CustomizationSet.cs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Glamourer.GameData/Customization/ICustomizationManager.cs
Normal file
16
Glamourer.GameData/Customization/ICustomizationManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
61
Glamourer.GameData/Glamourer - Backup.GameData.csproj
Normal file
61
Glamourer.GameData/Glamourer - Backup.GameData.csproj
Normal 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>
|
||||
61
Glamourer.GameData/Glamourer.GameData.csproj
Normal file
61
Glamourer.GameData/Glamourer.GameData.csproj
Normal 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>
|
||||
45
Glamourer.GameData/Item.cs
Normal file
45
Glamourer.GameData/Item.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Glamourer.GameData/Main.cs
Normal file
73
Glamourer.GameData/Main.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Glamourer.GameData/Stain.cs
Normal file
41
Glamourer.GameData/Stain.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Glamourer.GameData/Util/IconStorage.cs
Normal file
56
Glamourer.GameData/Util/IconStorage.cs
Normal 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
11
Glamourer.json
Normal 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
57
Glamourer.sln
Normal 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
|
||||
4
Glamourer.sln.DotSettings.user
Normal file
4
Glamourer.sln.DotSettings.user
Normal 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"><AssemblyExplorer>
|
||||
<Assembly Path="H:\Projects\FFPlugins\Penumbra\Penumbra\bin\Debug\net472\Penumbra.GameData.dll" />
|
||||
</AssemblyExplorer></s:String></wpf:ResourceDictionary>
|
||||
BIN
Glamourer.zip
Normal file
BIN
Glamourer.zip
Normal file
Binary file not shown.
23
Glamourer/CmpFile.cs
Normal file
23
Glamourer/CmpFile.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Glamourer/Glamourer.csproj
Normal file
98
Glamourer/Glamourer.csproj
Normal 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
11
Glamourer/Glamourer.json
Normal 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
|
||||
}
|
||||
178
Glamourer/Gui/ComboWithFilter.cs
Normal file
178
Glamourer/Gui/ComboWithFilter.cs
Normal 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
154
Glamourer/Gui/ImGuiRaii.cs
Normal 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
326
Glamourer/Gui/Interface.cs
Normal 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
170
Glamourer/Main.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Glamourer/Managers/CommandManager.cs
Normal file
73
Glamourer/Managers/CommandManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Glamourer/SeFunctions/BaseUiObject.cs
Normal file
11
Glamourer/SeFunctions/BaseUiObject.cs
Normal 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")
|
||||
{ }
|
||||
}
|
||||
}
|
||||
14
Glamourer/SeFunctions/GetUiModule.cs
Normal file
14
Glamourer/SeFunctions/GetUiModule.cs
Normal 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")
|
||||
{ }
|
||||
}
|
||||
}
|
||||
14
Glamourer/SeFunctions/ProcessChatBox.cs
Normal file
14
Glamourer/SeFunctions/ProcessChatBox.cs
Normal 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")
|
||||
{ }
|
||||
}
|
||||
}
|
||||
20
Glamourer/SeFunctions/SeAddressBase.cs
Normal file
20
Glamourer/SeFunctions/SeAddressBase.cs
Normal 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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
75
Glamourer/SeFunctions/SeFunctionBase.cs
Normal file
75
Glamourer/SeFunctions/SeFunctionBase.cs
Normal 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
201
LICENSE
Normal 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
0
README.md
Normal file
19
repo.json
Normal file
19
repo.json
Normal 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",
|
||||
}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue