From 546f6d4152ec130c75a247a070ac88fd280d4dfa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 29 Jun 2021 18:54:53 +0200 Subject: [PATCH] Support for TexTools .rgsp files for meta changes. (Racial Scaling Parameters) --- Penumbra/Game/Enums/Race.cs | 73 +++++++++++++++++++ Penumbra/Game/Enums/RspAttribute.cs | 68 +++++++++++++++++ Penumbra/Game/RspEntry.cs | 55 ++++++++++++++ Penumbra/Importer/TexToolsMeta.cs | 62 ++++++++++++++++ Penumbra/Meta/Files/CmpFile.cs | 72 ++++++++++++++++++ Penumbra/Meta/Files/MetaDefaults.cs | 14 ++++ Penumbra/Meta/Files/MetaFilenames.cs | 3 + Penumbra/Meta/Identifier.cs | 21 ++++++ Penumbra/Meta/MetaCollection.cs | 12 ++- Penumbra/Meta/MetaManager.cs | 2 + Penumbra/Meta/MetaManipulation.cs | 23 ++++++ Penumbra/Mod/ModResources.cs | 14 ++-- .../TabInstalled/TabInstalledDetails.cs | 14 +++- 13 files changed, 424 insertions(+), 9 deletions(-) create mode 100644 Penumbra/Game/Enums/RspAttribute.cs create mode 100644 Penumbra/Game/RspEntry.cs create mode 100644 Penumbra/Meta/Files/CmpFile.cs diff --git a/Penumbra/Game/Enums/Race.cs b/Penumbra/Game/Enums/Race.cs index d5ce82fd..cb8b694d 100644 --- a/Penumbra/Game/Enums/Race.cs +++ b/Penumbra/Game/Enums/Race.cs @@ -27,6 +27,27 @@ namespace Penumbra.Game.Enums Viera, } + public enum SubRace : byte + { + Unknown, + Midlander, + Highlander, + Wildwood, + Duskwright, + Plainsfolk, + Dunesfolk, + SeekerOfTheSun, + KeeperOfTheMoon, + Seawolf, + Hellsguard, + Raen, + Xaela, + Hellion, + Lost, + Rava, + Veena, + } + // The combined gender-race-npc numerical code as used by the game. public enum GenderRace : ushort { @@ -69,6 +90,58 @@ namespace Penumbra.Game.Enums public static class RaceEnumExtensions { + public static int ToRspIndex( this SubRace subRace ) + { + return subRace switch + { + SubRace.Midlander => 0, + SubRace.Highlander => 1, + SubRace.Wildwood => 10, + SubRace.Duskwright => 11, + SubRace.Plainsfolk => 20, + SubRace.Dunesfolk => 21, + SubRace.SeekerOfTheSun => 30, + SubRace.KeeperOfTheMoon => 31, + SubRace.Seawolf => 40, + SubRace.Hellsguard => 41, + SubRace.Raen => 50, + SubRace.Xaela => 51, + SubRace.Hellion => 60, + SubRace.Lost => 61, + SubRace.Rava => 70, + SubRace.Veena => 71, + _ => throw new InvalidEnumArgumentException(), + }; + } + + public static Race ToRace( this SubRace subRace ) + { + return subRace switch + { + SubRace.Unknown => Race.Unknown, + SubRace.Midlander => Race.Midlander, + SubRace.Highlander => Race.Highlander, + SubRace.Wildwood => Race.Elezen, + SubRace.Duskwright => Race.Elezen, + SubRace.Plainsfolk => Race.Lalafell, + SubRace.Dunesfolk => Race.Lalafell, + SubRace.SeekerOfTheSun => Race.Miqote, + SubRace.KeeperOfTheMoon => Race.Miqote, + SubRace.Seawolf => Race.Roegadyn, + SubRace.Hellsguard => Race.Roegadyn, + SubRace.Raen => Race.AuRa, + SubRace.Xaela => Race.AuRa, + SubRace.Hellion => Race.Hrothgar, + SubRace.Lost => Race.Hrothgar, + SubRace.Rava => Race.Viera, + SubRace.Veena => Race.Viera, + _ => throw new InvalidEnumArgumentException(), + }; + } + + public static bool FitsRace( this SubRace subRace, Race race ) + => subRace.ToRace() == race; + public static byte ToByte( this Gender gender, Race race ) => ( byte )( ( int )gender | ( ( int )race << 3 ) ); diff --git a/Penumbra/Game/Enums/RspAttribute.cs b/Penumbra/Game/Enums/RspAttribute.cs new file mode 100644 index 00000000..bb902061 --- /dev/null +++ b/Penumbra/Game/Enums/RspAttribute.cs @@ -0,0 +1,68 @@ +namespace Penumbra.Game.Enums +{ + public enum RspAttribute : byte + { + MaleMinSize, + MaleMaxSize, + MaleMinTail, + MaleMaxTail, + FemaleMinSize, + FemaleMaxSize, + FemaleMinTail, + FemaleMaxTail, + BustMinX, + BustMinY, + BustMinZ, + BustMaxX, + BustMaxY, + BustMaxZ, + NumAttributes, + } + + public static class RspAttributeExtensions + { + public static Gender ToGender( this RspAttribute attribute ) + { + return attribute switch + { + RspAttribute.MaleMinSize => Gender.Male, + RspAttribute.MaleMaxSize => Gender.Male, + RspAttribute.MaleMinTail => Gender.Male, + RspAttribute.MaleMaxTail => Gender.Male, + RspAttribute.FemaleMinSize => Gender.Female, + RspAttribute.FemaleMaxSize => Gender.Female, + RspAttribute.FemaleMinTail => Gender.Female, + RspAttribute.FemaleMaxTail => Gender.Female, + RspAttribute.BustMinX => Gender.Female, + RspAttribute.BustMinY => Gender.Female, + RspAttribute.BustMinZ => Gender.Female, + RspAttribute.BustMaxX => Gender.Female, + RspAttribute.BustMaxY => Gender.Female, + RspAttribute.BustMaxZ => Gender.Female, + _ => Gender.Unknown, + }; + } + + public static string ToUngenderedString( this RspAttribute attribute ) + { + return attribute switch + { + RspAttribute.MaleMinSize => "MinSize", + RspAttribute.MaleMaxSize => "MaxSize", + RspAttribute.MaleMinTail => "MinTail", + RspAttribute.MaleMaxTail => "MaxTail", + RspAttribute.FemaleMinSize => "MinSize", + RspAttribute.FemaleMaxSize => "MaxSize", + RspAttribute.FemaleMinTail => "MinTail", + RspAttribute.FemaleMaxTail => "MaxTail", + RspAttribute.BustMinX => "BustMinX", + RspAttribute.BustMinY => "BustMinY", + RspAttribute.BustMinZ => "BustMinZ", + RspAttribute.BustMaxX => "BustMaxX", + RspAttribute.BustMaxY => "BustMaxY", + RspAttribute.BustMaxZ => "BustMaxZ", + _ => "", + }; + } + } +} \ No newline at end of file diff --git a/Penumbra/Game/RspEntry.cs b/Penumbra/Game/RspEntry.cs new file mode 100644 index 00000000..0555809b --- /dev/null +++ b/Penumbra/Game/RspEntry.cs @@ -0,0 +1,55 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using Penumbra.Game.Enums; + +namespace Penumbra.Game +{ + [StructLayout( LayoutKind.Sequential, Pack = 1 )] + public readonly struct RspEntry + { + public const int ByteSize = ( int )RspAttribute.NumAttributes * 4; + + private readonly float[] Attributes; + + public RspEntry( byte[] bytes, int offset ) + { + if( offset < 0 || offset + ByteSize > bytes.Length ) + { + throw new ArgumentOutOfRangeException(); + } + + Attributes = new float[( int )RspAttribute.NumAttributes]; + using MemoryStream s = new( bytes ) { Position = offset }; + using BinaryReader br = new( s ); + for( var i = 0; i < ( int )RspAttribute.NumAttributes; ++i ) + { + Attributes[ i ] = br.ReadSingle(); + } + } + + private static int ToIndex( RspAttribute attribute ) + => attribute < RspAttribute.NumAttributes && attribute >= 0 + ? ( int )attribute + : throw new InvalidEnumArgumentException(); + + public float this[ RspAttribute attribute ] + { + get => Attributes[ ToIndex( attribute ) ]; + set => Attributes[ ToIndex( attribute ) ] = value; + } + + public byte[] ToBytes() + { + using var s = new MemoryStream( ByteSize ); + using var bw = new BinaryWriter( s ); + foreach( var attribute in Attributes ) + { + bw.Write( attribute ); + } + + return s.ToArray(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index b4cc86b4..7ab681a9 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -316,5 +316,67 @@ namespace Penumbra.Importer PluginLog.Error( $"Error while parsing .meta file:\n{e}" ); } } + + private TexToolsMeta( string filePath, uint version ) + { + FilePath = filePath; + Version = version; + } + + public static TexToolsMeta Invalid = new( string.Empty, 0 ); + + public static TexToolsMeta FromRgspFile( string filePath, byte[] data ) + { + if( data.Length != 45 && data.Length != 42 ) + { + PluginLog.Error( "Error while parsing .rgsp file:\n\tInvalid number of bytes." ); + return Invalid; + } + + using var s = new MemoryStream( data ); + using var br = new BinaryReader( s ); + var flag = br.ReadByte(); + var version = flag != 255 ? ( uint )1 : br.ReadUInt16(); + + var ret = new TexToolsMeta( filePath, version ); + + var subRace = ( SubRace )( br.ReadByte() + 1 ); + if( !Enum.IsDefined( typeof( SubRace ), subRace ) || subRace == SubRace.Unknown ) + { + PluginLog.Error( $"Error while parsing .rgsp file:\n\t{subRace} is not a valid SubRace." ); + return Invalid; + } + + var gender = br.ReadByte(); + if( gender != 1 && gender != 0 ) + { + PluginLog.Error( $"Error while parsing .rgsp file:\n\t{gender} is neither Male nor Female." ); + return Invalid; + } + + if( gender == 1 ) + { + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinSize, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxSize, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMinTail, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.FemaleMaxTail, br.ReadSingle() ) ); + + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinX, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinY, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMinZ, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxX, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxY, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.BustMaxZ, br.ReadSingle() ) ); + } + else + { + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinSize, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxSize, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMinTail, br.ReadSingle() ) ); + ret.AddIfNotDefault( MetaManipulation.Rsp( subRace, RspAttribute.MaleMaxTail, br.ReadSingle() ) ); + } + + return ret; + } } } \ No newline at end of file diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs new file mode 100644 index 00000000..e8ed5132 --- /dev/null +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using Penumbra.Game; +using Penumbra.Game.Enums; + +namespace Penumbra.Meta.Files +{ + public class CmpFile + { + private const int RacialScalingStart = 0x2A800; + + private readonly byte[] _byteData = new byte[RacialScalingStart]; + private readonly List< RspEntry > _rspEntries; + + public CmpFile( byte[] bytes ) + { + if( bytes.Length < RacialScalingStart ) + { + throw new ArgumentOutOfRangeException(); + } + + Array.Copy( bytes, _byteData, RacialScalingStart ); + var rspEntryNum = ( bytes.Length - RacialScalingStart ) / RspEntry.ByteSize; + _rspEntries = new List< RspEntry >( rspEntryNum ); + for( var i = 0; i < rspEntryNum; ++i ) + { + _rspEntries.Add( new RspEntry( bytes, RacialScalingStart + i * RspEntry.ByteSize ) ); + } + } + + public RspEntry this[ SubRace subRace ] + => _rspEntries[ subRace.ToRspIndex() ]; + + public bool Set( SubRace subRace, RspAttribute attribute, float value ) + { + var entry = _rspEntries[ subRace.ToRspIndex() ]; + var oldValue = entry[ attribute ]; + if( oldValue == value ) + { + return false; + } + + entry[ attribute ] = value; + return true; + } + + public byte[] WriteBytes() + { + using var s = new MemoryStream( RacialScalingStart + _rspEntries.Count * RspEntry.ByteSize ); + s.Write( _byteData, 0, _byteData.Length ); + foreach( var entry in _rspEntries ) + { + var bytes = entry.ToBytes(); + s.Write( bytes, 0, bytes.Length ); + } + + return s.ToArray(); + } + + private CmpFile( byte[] data, List< RspEntry > entries ) + { + _byteData = data.ToArray(); + _rspEntries = entries.ToList(); + } + + public CmpFile Clone() + => new( _byteData, _rspEntries ); + } +} \ No newline at end of file diff --git a/Penumbra/Meta/Files/MetaDefaults.cs b/Penumbra/Meta/Files/MetaDefaults.cs index df2d6cae..c84f3894 100644 --- a/Penumbra/Meta/Files/MetaDefaults.cs +++ b/Penumbra/Meta/Files/MetaDefaults.cs @@ -45,6 +45,11 @@ namespace Penumbra.Meta.Files return new EstFile( rawFile ); } + if( path.EndsWith( ".cmp" ) ) + { + return new CmpFile( rawFile.Data ); + } + throw new NotImplementedException(); } @@ -85,6 +90,9 @@ namespace Penumbra.Meta.Files => GetDefaultFile< ImcFile >( MetaFileNames.Imc( type, primarySetId, secondarySetId ), $"Could not obtain Imc file for {type}, {primarySetId} {secondarySetId}:\n" ); + private CmpFile? GetDefaultCmpFile() + => GetDefaultFile< CmpFile >( MetaFileNames.Cmp(), "Could not obtain Cmp file:\n" ); + public EqdpFile? GetNewEqdpFile( EquipSlot slot, GenderRace gr ) => GetDefaultEqdpFile( slot, gr )?.Clone(); @@ -100,6 +108,9 @@ namespace Penumbra.Meta.Files public ImcFile? GetNewImcFile( ObjectType type, ushort primarySetId, ushort secondarySetId = 0 ) => GetDefaultImcFile( type, primarySetId, secondarySetId )?.Clone(); + public CmpFile? GetNewCmpFile() + => GetDefaultCmpFile()?.Clone(); + public MetaDefaults( DalamudPluginInterface pi ) => _pi = pi; @@ -128,6 +139,8 @@ namespace Penumbra.Meta.Files MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ) == m.EstValue, + MetaType.Rsp => GetDefaultCmpFile()?[ m.RspIdentifier.SubRace ][ m.RspIdentifier.Attribute ] + == m.RspValue, _ => throw new NotImplementedException(), }; } @@ -142,6 +155,7 @@ namespace Penumbra.Meta.Files MetaType.Eqp => GetNewEqpFile(), MetaType.Eqdp => GetNewEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ), MetaType.Est => GetNewEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ), + MetaType.Rsp => GetNewCmpFile(), _ => throw new NotImplementedException(), }; } diff --git a/Penumbra/Meta/Files/MetaFilenames.cs b/Penumbra/Meta/Files/MetaFilenames.cs index 6f33e88f..b7e9cdca 100644 --- a/Penumbra/Meta/Files/MetaFilenames.cs +++ b/Penumbra/Meta/Files/MetaFilenames.cs @@ -76,5 +76,8 @@ namespace Penumbra.Meta.Files _ => throw new NotImplementedException(), }; } + + public static GamePath Cmp() + => GamePath.GenerateUnchecked( "chara/xls/charamake/human.cmp" ); } } \ No newline at end of file diff --git a/Penumbra/Meta/Identifier.cs b/Penumbra/Meta/Identifier.cs index 8af19288..b9a0d1a9 100644 --- a/Penumbra/Meta/Identifier.cs +++ b/Penumbra/Meta/Identifier.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; using Penumbra.Game.Enums; +using Penumbra.Meta.Files; // A struct for each type of meta change that contains all relevant information, @@ -15,6 +16,7 @@ namespace Penumbra.Meta Eqp = 3, Est = 4, Gmp = 5, + Rsp = 6, }; [StructLayout( LayoutKind.Explicit )] @@ -150,4 +152,23 @@ namespace Penumbra.Meta }; } } + + [StructLayout( LayoutKind.Explicit )] + public struct RspIdentifier + { + [FieldOffset( 0 )] + public ulong Value; + + [FieldOffset( 0 )] + public MetaType Type; + + [FieldOffset( 1 )] + public SubRace SubRace; + + [FieldOffset( 2 )] + public RspAttribute Attribute; + + public override string ToString() + => $"Rsp - {SubRace} - {Attribute}"; + } } \ No newline at end of file diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 012f85e9..a6648869 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using Dalamud.Plugin; using Newtonsoft.Json; using Penumbra.Importer; @@ -151,9 +152,16 @@ namespace Penumbra.Meta { DefaultData.Clear(); GroupData.Clear(); - foreach( var file in files.Where( f => f.Extension == ".meta" ) ) + Count = 0; + foreach( var file in files ) { - var metaData = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + TexToolsMeta metaData = file.Extension.ToLowerInvariant() switch + { + ".meta" => new TexToolsMeta( File.ReadAllBytes( file.FullName ) ), + ".rgsp" => TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ), + _ => TexToolsMeta.Invalid, + }; + if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 ) { continue; diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 5be982be..1b412726 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -30,6 +30,7 @@ namespace Penumbra.Meta GmpFile gmp => gmp.WriteBytes(), EstFile est => est.WriteBytes(), ImcFile imc => imc.WriteBytes(), + CmpFile cmp => cmp.WriteBytes(), _ => throw new NotImplementedException(), }; DisposeFile( CurrentFile ); @@ -158,6 +159,7 @@ namespace Penumbra.Meta MetaType.Gmp => m.Apply( ( GmpFile )file.Data ), MetaType.Est => m.Apply( ( EstFile )file.Data ), MetaType.Imc => m.Apply( ( ImcFile )file.Data ), + MetaType.Rsp => m.Apply( ( CmpFile )file.Data ), _ => throw new NotImplementedException(), }; return true; diff --git a/Penumbra/Meta/MetaManipulation.cs b/Penumbra/Meta/MetaManipulation.cs index d73c7d08..bd280e86 100644 --- a/Penumbra/Meta/MetaManipulation.cs +++ b/Penumbra/Meta/MetaManipulation.cs @@ -129,6 +129,18 @@ namespace Penumbra.Meta ImcValue = value, }; + public static MetaManipulation Rsp( SubRace subRace, RspAttribute attribute, float value ) + => new() + { + RspIdentifier = new RspIdentifier() + { + Type = MetaType.Rsp, + SubRace = subRace, + Attribute = attribute, + }, + RspValue = value, + }; + internal MetaManipulation( ulong identifier, ulong value ) : this() { @@ -160,6 +172,9 @@ namespace Penumbra.Meta [FieldOffset( 0 )] public ImcIdentifier ImcIdentifier; + [FieldOffset( 0 )] + public RspIdentifier RspIdentifier; + [FieldOffset( 8 )] public EqpEntry EqpValue; @@ -176,6 +191,9 @@ namespace Penumbra.Meta [FieldOffset( 8 )] public ImcFile.ImageChangeData ImcValue; // 6 bytes. + [FieldOffset( 8 )] + public float RspValue; + public override int GetHashCode() => Identifier.GetHashCode(); @@ -191,6 +209,7 @@ namespace Penumbra.Meta MetaType.Est => MetaFileNames.Est( EstIdentifier.ObjectType, EstIdentifier.EquipSlot, EstIdentifier.BodySlot ), MetaType.Gmp => MetaFileNames.Gmp(), MetaType.Imc => MetaFileNames.Imc( ImcIdentifier.ObjectType, ImcIdentifier.PrimaryId, ImcIdentifier.SecondaryId ), + MetaType.Rsp => MetaFileNames.Cmp(), _ => throw new InvalidEnumArgumentException(), }; } @@ -220,6 +239,9 @@ namespace Penumbra.Meta return true; } + public bool Apply( CmpFile file ) + => file.Set( RspIdentifier.SubRace, RspIdentifier.Attribute, RspValue ); + public string IdentifierString() { return Type switch @@ -229,6 +251,7 @@ namespace Penumbra.Meta MetaType.Est => EstIdentifier.ToString(), MetaType.Gmp => GmpIdentifier.ToString(), MetaType.Imc => ImcIdentifier.ToString(), + MetaType.Rsp => RspIdentifier.ToString(), _ => throw new InvalidEnumArgumentException(), }; } diff --git a/Penumbra/Mod/ModResources.cs b/Penumbra/Mod/ModResources.cs index 7f0f1292..69cf94ec 100644 --- a/Penumbra/Mod/ModResources.cs +++ b/Penumbra/Mod/ModResources.cs @@ -57,13 +57,15 @@ namespace Penumbra.Mod .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) .OrderBy( f => f.FullName ) ) { - if( file.Extension != ".meta" ) + switch( file.Extension.ToLowerInvariant() ) { - tmpFiles.Add( file ); - } - else - { - tmpMetas.Add( file ); + case ".meta": + case ".rgsp": + tmpMetas.Add( file ); + break; + default: + tmpFiles.Add( file ); + break; } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 6da965f1..38e3525a 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using Dalamud.Interface; @@ -774,10 +775,21 @@ namespace Penumbra.UI ImGui.Text( manip.ImcIdentifier.Variant.ToString() ); break; } + case MetaType.Rsp: + { + ImGui.Text( manip.RspIdentifier.Attribute.ToUngenderedString() ); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.Text( manip.RspIdentifier.SubRace.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.RspIdentifier.Attribute.ToGender().ToString() ); + break; + } } ImGui.TableSetColumnIndex( 9 ); - ImGui.Text( manip.Value.ToString() ); + ImGui.Text( manip.Type == MetaType.Rsp ? manip.RspValue.ToString( CultureInfo.InvariantCulture ) : manip.Value.ToString() ); ImGui.TableNextRow(); }