From 3391a8ce714acf095f58d6e2ecc45f23d7af1410 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 26 Nov 2022 01:54:09 +0100 Subject: [PATCH] Add functions to re-export meta changes to TexTools .meta and .rgsp formats. --- Penumbra.GameData/Data/GamePathParser.cs | 1 - Penumbra/Import/TexToolsMeta.Export.cs | 242 ++++++++++++++++++ Penumbra/Import/TexToolsMeta.cs | 5 +- Penumbra/Meta/Files/ImcFile.cs | 17 +- .../Meta/Manipulations/MetaManipulation.cs | 12 + Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs | 97 +++++++ Penumbra/UI/Classes/ModEditWindow.Meta.cs | 33 ++- 7 files changed, 383 insertions(+), 24 deletions(-) create mode 100644 Penumbra/Import/TexToolsMeta.Export.cs diff --git a/Penumbra.GameData/Data/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs index 4725ed1f..58817c28 100644 --- a/Penumbra.GameData/Data/GamePathParser.cs +++ b/Penumbra.GameData/Data/GamePathParser.cs @@ -7,7 +7,6 @@ using System.Text.RegularExpressions; using Dalamud.Logging; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.String; namespace Penumbra.GameData.Data; diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs new file mode 100644 index 00000000..03aae64a --- /dev/null +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Import; + +public partial class TexToolsMeta +{ + public static Dictionary< string, byte[] > ConvertToTexTools( IEnumerable< MetaManipulation > manips ) + { + var ret = new Dictionary< string, byte[] >(); + foreach( var group in manips.GroupBy( ManipToPath ) ) + { + if( group.Key.Length == 0 ) + { + continue; + } + + var bytes = group.Key.EndsWith( ".rgsp" ) + ? WriteRgspFile( group.Key, group ) + : WriteMetaFile( group.Key, group ); + if( bytes.Length == 0 ) + { + continue; + } + + ret.Add( group.Key, bytes ); + } + + return ret; + } + + private static byte[] WriteRgspFile( string path, IEnumerable< MetaManipulation > manips ) + { + var list = manips.GroupBy( m => m.Rsp.Attribute ).ToDictionary( m => m.Key, m => m.Last().Rsp ); + using var m = new MemoryStream( 45 ); + using var b = new BinaryWriter( m ); + // Version + b.Write( byte.MaxValue ); + b.Write( ( ushort )2 ); + + var race = list.First().Value.SubRace; + var gender = list.First().Value.Attribute.ToGender(); + b.Write( ( byte )(race - 1) ); // offset by one due to Unknown + b.Write( ( byte )(gender - 1) ); // offset by one due to Unknown + + void Add( params RspAttribute[] attributes ) + { + foreach( var attribute in attributes ) + { + var value = list.TryGetValue( attribute, out var tmp ) ? tmp.Entry : CmpFile.GetDefault( race, attribute ); + b.Write( value ); + } + } + + if( gender == Gender.Male ) + { + Add( RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail ); + } + else + { + Add( RspAttribute.FemaleMinSize, RspAttribute.FemaleMaxSize, RspAttribute.FemaleMinTail, RspAttribute.FemaleMaxTail ); + Add( RspAttribute.BustMinX, RspAttribute.BustMinY, RspAttribute.BustMinZ, RspAttribute.BustMaxX, RspAttribute.BustMaxY, RspAttribute.BustMaxZ ); + } + + return m.GetBuffer(); + } + + private static byte[] WriteMetaFile( string path, IEnumerable< MetaManipulation > manips ) + { + var filteredManips = manips.GroupBy( m => m.ManipulationType ).ToDictionary( p => p.Key, p => p.Select( x => x ) ); + + using var m = new MemoryStream(); + using var b = new BinaryWriter( m ); + + // Header + // Current TT Metadata version. + b.Write( 2u ); + + // Null-terminated ASCII path. + var utf8Path = Encoding.ASCII.GetBytes( path ); + b.Write( utf8Path ); + b.Write( ( byte )0 ); + + // Number of Headers + b.Write( ( uint )filteredManips.Count ); + // Current TT Size of Headers + b.Write( ( uint )12 ); + + // Start of Header Entries for some reason, which is absolutely useless. + var headerStart = b.BaseStream.Position + 4; + b.Write( ( uint )headerStart ); + + var offset = ( uint )( b.BaseStream.Position + 12 * filteredManips.Count ); + foreach( var (header, data) in filteredManips ) + { + b.Write( ( uint )header ); + b.Write( offset ); + + var size = WriteData( b, offset, header, data ); + b.Write( size ); + offset += size; + } + + return m.ToArray(); + } + + private static uint WriteData( BinaryWriter b, uint offset, MetaManipulation.Type type, IEnumerable< MetaManipulation > manips ) + { + var oldPos = b.BaseStream.Position; + b.Seek( ( int )offset, SeekOrigin.Begin ); + + switch( type ) + { + case MetaManipulation.Type.Imc: + var allManips = manips.ToList(); + var baseFile = new ImcFile( allManips[ 0 ].Imc ); + foreach( var manip in allManips ) + { + manip.Imc.Apply( baseFile ); + } + + var partIdx = allManips[ 0 ].Imc.ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? ImcFile.PartIndex( allManips[ 0 ].Imc.EquipSlot ) + : 0; + + for( var i = 0; i <= baseFile.Count; ++i ) + { + var entry = baseFile.GetEntry( partIdx, i ); + b.Write( entry.MaterialId ); + b.Write( entry.DecalId ); + b.Write( entry.AttributeAndSound ); + b.Write( entry.VfxId ); + b.Write( entry.MaterialAnimationId ); + } + + break; + case MetaManipulation.Type.Eqdp: + foreach( var manip in manips ) + { + b.Write( ( uint )Names.CombinedRace( manip.Eqdp.Gender, manip.Eqdp.Race ) ); + var entry = ( byte )(( ( uint )manip.Eqdp.Entry >> Eqdp.Offset( manip.Eqdp.Slot ) ) & 0x03); + b.Write( entry ); + } + + break; + case MetaManipulation.Type.Eqp: + foreach( var manip in manips ) + { + var bytes = BitConverter.GetBytes( (ulong) manip.Eqp.Entry ); + var (numBytes, byteOffset) = Eqp.BytesAndOffset( manip.Eqp.Slot ); + for( var i = byteOffset; i < numBytes + byteOffset; ++i ) + b.Write( bytes[ i ] ); + } + + break; + case MetaManipulation.Type.Est: + foreach( var manip in manips ) + { + b.Write( ( ushort )Names.CombinedRace( manip.Est.Gender, manip.Est.Race ) ); + b.Write( manip.Est.SetId ); + b.Write( manip.Est.Entry ); + } + + break; + case MetaManipulation.Type.Gmp: + foreach( var manip in manips ) + { + b.Write( ( uint )manip.Gmp.Entry.Value ); + b.Write( manip.Gmp.Entry.UnknownTotal ); + } + + break; + } + + var size = b.BaseStream.Position - offset; + b.Seek( ( int )oldPos, SeekOrigin.Begin ); + return ( uint )size; + } + + private static string ManipToPath( MetaManipulation manip ) + => manip.ManipulationType switch + { + MetaManipulation.Type.Imc => ManipToPath( manip.Imc ), + MetaManipulation.Type.Eqdp => ManipToPath( manip.Eqdp ), + MetaManipulation.Type.Eqp => ManipToPath( manip.Eqp ), + MetaManipulation.Type.Est => ManipToPath( manip.Est ), + MetaManipulation.Type.Gmp => ManipToPath( manip.Gmp ), + MetaManipulation.Type.Rsp => ManipToPath( manip.Rsp ), + _ => string.Empty, + }; + + private static string ManipToPath( ImcManipulation manip ) + { + var path = manip.GamePath().ToString(); + var replacement = manip.ObjectType switch + { + ObjectType.Accessory => $"_{manip.EquipSlot.ToSuffix()}.meta", + ObjectType.Equipment => $"_{manip.EquipSlot.ToSuffix()}.meta", + ObjectType.Character => $"_{manip.BodySlot.ToSuffix()}.meta", + _ => ".meta", + }; + + return path.Replace( ".imc", replacement ); + } + + private static string ManipToPath( EqdpManipulation manip ) + => manip.Slot.IsAccessory() + ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" + : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; + + private static string ManipToPath( EqpManipulation manip ) + => manip.Slot.IsAccessory() + ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" + : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; + + private static string ManipToPath( EstManipulation manip ) + { + var raceCode = Names.CombinedRace( manip.Gender, manip.Race ).ToRaceCode(); + return manip.Slot switch + { + EstManipulation.EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", + EstManipulation.EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", + EstManipulation.EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", + EstManipulation.EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", + _ => throw new ArgumentOutOfRangeException(), + }; + } + + private static string ManipToPath( GmpManipulation manip ) + => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta"; + + + private static string ManipToPath( RspManipulation manip ) + => $"chara/xls/charamake/rgsp/{( int )manip.SubRace - 1}-{( int )manip.Attribute.ToGender() - 1}.rgsp"; +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 0058764c..2a2c98e5 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; @@ -18,7 +19,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { // An empty TexToolsMeta. - public static readonly TexToolsMeta Invalid = new( string.Empty, 0 ); + public static readonly TexToolsMeta Invalid = new(string.Empty, 0); // The info class determines the files or table locations the changes need to apply to from the filename. @@ -84,7 +85,7 @@ public partial class TexToolsMeta // Read a null terminated string from a binary reader. private static string ReadNullTerminated( BinaryReader reader ) { - var builder = new System.Text.StringBuilder(); + var builder = new StringBuilder(); for( var c = reader.ReadChar(); c != 0; c = reader.ReadChar() ) { builder.Append( c ); diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 8f36507e..5b769b7f 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,7 +1,6 @@ using System; using System.Numerics; using Newtonsoft.Json; -using OtterGui; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; @@ -14,26 +13,26 @@ public readonly struct ImcEntry : IEquatable< ImcEntry > { public byte MaterialId { get; init; } public byte DecalId { get; init; } - private readonly ushort _attributeAndSound; + public readonly ushort AttributeAndSound; public byte VfxId { get; init; } public byte MaterialAnimationId { get; init; } public ushort AttributeMask { - get => ( ushort )( _attributeAndSound & 0x3FF ); - init => _attributeAndSound = ( ushort )( ( _attributeAndSound & ~0x3FF ) | ( value & 0x3FF ) ); + get => ( ushort )( AttributeAndSound & 0x3FF ); + init => AttributeAndSound = ( ushort )( ( AttributeAndSound & ~0x3FF ) | ( value & 0x3FF ) ); } public byte SoundId { - get => ( byte )( _attributeAndSound >> 10 ); - init => _attributeAndSound = ( ushort )( AttributeMask | ( value << 10 ) ); + get => ( byte )( AttributeAndSound >> 10 ); + init => AttributeAndSound = ( ushort )( AttributeMask | ( value << 10 ) ); } public bool Equals( ImcEntry other ) => MaterialId == other.MaterialId && DecalId == other.DecalId - && _attributeAndSound == other._attributeAndSound + && AttributeAndSound == other.AttributeAndSound && VfxId == other.VfxId && MaterialAnimationId == other.MaterialAnimationId; @@ -41,14 +40,14 @@ public readonly struct ImcEntry : IEquatable< ImcEntry > => obj is ImcEntry other && Equals( other ); public override int GetHashCode() - => HashCode.Combine( MaterialId, DecalId, _attributeAndSound, VfxId, MaterialAnimationId ); + => HashCode.Combine( MaterialId, DecalId, AttributeAndSound, VfxId, MaterialAnimationId ); [JsonConstructor] public ImcEntry( byte materialId, byte decalId, ushort attributeMask, byte soundId, byte vfxId, byte materialAnimationId ) { MaterialId = materialId; DecalId = decalId; - _attributeAndSound = 0; + AttributeAndSound = 0; VfxId = vfxId; MaterialAnimationId = materialAnimationId; AttributeMask = attributeMask; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index f388ea8e..f6ff2d94 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -232,4 +232,16 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa Type.Imc => Imc.ToString(), _ => throw new ArgumentOutOfRangeException(), }; + + public string EntryToString() + => ManipulationType switch + { + Type.Imc => $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", + Type.Eqdp => $"{(ushort) Eqdp.Entry:X}", + Type.Eqp => $"{(ulong)Eqp.Entry:X}", + Type.Est => $"{Est.Entry}", + Type.Gmp => $"{Gmp.Entry.Value}", + Type.Rsp => $"{Rsp.Entry}", + _ => string.Empty, + }; } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 883f74e6..fe0de7d0 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using Newtonsoft.Json; @@ -59,6 +60,37 @@ public partial class Mod } } + public void WriteAllTexToolsMeta() + { + try + { + _default.WriteTexToolsMeta( ModPath ); + foreach( var group in Groups ) + { + var dir = NewOptionDirectory( ModPath, group.Name ); + if( !dir.Exists ) + { + dir.Create(); + } + + foreach( var option in group.OfType< SubMod >() ) + { + var optionDir = NewOptionDirectory( dir, option.Name ); + if( !optionDir.Exists ) + { + optionDir.Create(); + } + + option.WriteTexToolsMeta( optionDir ); + } + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Error writing TexToolsMeta:\n{e}" ); + } + } + // A sub mod is a collection of // - file replacements @@ -197,5 +229,70 @@ public partial class Mod } } } + + public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) + { + var files = TexToolsMeta.ConvertToTexTools( Manipulations ); + + foreach( var (file, data) in files ) + { + var path = Path.Combine( basePath.FullName, file ); + try + { + Directory.CreateDirectory( Path.GetDirectoryName( path )! ); + File.WriteAllBytes( path, data ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" ); + } + } + + if( test ) + { + TestMetaWriting( files ); + } + } + + [Conditional("DEBUG")] + private void TestMetaWriting( Dictionary< string, byte[] > files ) + { + var meta = new HashSet< MetaManipulation >( Manipulations.Count ); + foreach( var (file, data) in files ) + { + try + { + var x = file.EndsWith( "rgsp" ) ? TexToolsMeta.FromRgspFile( file, data ) : new TexToolsMeta( data ); + meta.UnionWith( x.MetaManipulations ); + } + catch + { + // ignored + } + } + + if( !Manipulations.SetEquals( meta ) ) + { + Penumbra.Log.Information( "Meta Sets do not equal." ); + foreach( var (m1, m2) in Manipulations.Zip( meta ) ) + { + Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" ); + } + + foreach( var m in Manipulations.Skip( meta.Count ) ) + { + Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); + } + + foreach( var m in meta.Skip( Manipulations.Count ) ) + { + Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); + } + } + else + { + Penumbra.Log.Information( "Meta Sets are equal." ); + } + } } } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 9a725685..8c2e8340 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -17,18 +17,22 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private const string ModelSetIdTooltip = "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string PrimaryIdTooltip = "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string ModelSetIdTooltipShort = "Model Set ID"; - private const string EquipSlotTooltip = "Equip Slot"; - private const string ModelRaceTooltip = "Model Race"; - private const string GenderTooltip = "Gender"; - private const string ObjectTypeTooltip = "Object Type"; - private const string SecondaryIdTooltip = "Secondary ID"; - private const string VariantIdTooltip = "Variant ID"; - private const string EstTypeTooltip = "EST Type"; - private const string RacialTribeTooltip = "Racial Tribe"; - private const string ScalingTypeTooltip = "Scaling Type"; + private const string ModelSetIdTooltip = + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; + + private const string PrimaryIdTooltip = + "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; + + private const string ModelSetIdTooltipShort = "Model Set ID"; + private const string EquipSlotTooltip = "Equip Slot"; + private const string ModelRaceTooltip = "Model Race"; + private const string GenderTooltip = "Gender"; + private const string ObjectTypeTooltip = "Object Type"; + private const string SecondaryIdTooltip = "Secondary ID"; + private const string VariantIdTooltip = "Variant ID"; + private const string EstTypeTooltip = "EST Type"; + private const string RacialTribeTooltip = "Racial Tribe"; + private const string ScalingTypeTooltip = "Scaling Type"; private void DrawMetaTab() { @@ -61,6 +65,11 @@ public partial class ModEditWindow SetFromClipboardButton(); ImGui.SameLine(); CopyToClipboardButton( "Copy all current manipulations to clipboard.", _iconSize, _editor.Meta.Recombine() ); + ImGui.SameLine(); + if( ImGui.Button( "Write as TexTools Files" ) ) + { + _mod!.WriteAllTexToolsMeta(); + } using var child = ImRaii.Child( "##meta", -Vector2.One, true ); if( !child )