Support for TexTools .rgsp files for meta changes. (Racial Scaling Parameters)

This commit is contained in:
Ottermandias 2021-06-29 18:54:53 +02:00
parent d52086b69c
commit 546f6d4152
13 changed files with 424 additions and 9 deletions

View file

@ -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 ) );

View file

@ -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",
_ => "",
};
}
}
}

55
Penumbra/Game/RspEntry.cs Normal file
View file

@ -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();
}
}
}

View file

@ -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;
}
}
}

View file

@ -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 );
}
}

View file

@ -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(),
};
}

View file

@ -76,5 +76,8 @@ namespace Penumbra.Meta.Files
_ => throw new NotImplementedException(),
};
}
public static GamePath Cmp()
=> GamePath.GenerateUnchecked( "chara/xls/charamake/human.cmp" );
}
}

View file

@ -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}";
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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(),
};
}

View file

@ -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;
}
}

View file

@ -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();
}