From 5074cbe10d59648e94f854b921b788d9badfcb44 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:21:29 +1100 Subject: [PATCH 01/13] Only use decal for correct slot --- .../Models/Composer/ComposerCache.cs | 8 ++-- .../Models/Composer/MaterialComposer.cs | 2 - Meddle/Meddle.Plugin/Models/Enums.cs | 30 +++++++------- .../Models/Layout/ParsedInstance.cs | 2 + .../Meddle.Plugin/Services/ResolverService.cs | 39 +++++++++++++++---- 5 files changed, 53 insertions(+), 28 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Composer/ComposerCache.cs b/Meddle/Meddle.Plugin/Models/Composer/ComposerCache.cs index 0e533d2..7fd2595 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/ComposerCache.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/ComposerCache.cs @@ -365,16 +365,18 @@ string SaveColorTableTex(SkTexture tex) if (characterInfo != null) { - if (characterInfo.CustomizeData?.DecalPath != null) + if (characterInfo.CustomizeData?.DecalPath != null && materialInfo?.ApplyDecal == true) { var decalCachePath = CacheTexture(characterInfo.CustomizeData.DecalPath); material.SetProperty("Decal_PngCachePath", Path.GetRelativePath(cacheDir, decalCachePath)); + material.SetProperty("DecalPath", characterInfo.CustomizeData.DecalPath ?? ""); } - if (characterInfo.CustomizeData?.LegacyBodyDecalPath != null) + if (characterInfo.CustomizeData?.LegacyBodyDecalPath != null && materialInfo?.ApplyLegacyDecal == true) { var legacyDecalCachePath = CacheTexture(characterInfo.CustomizeData.LegacyBodyDecalPath); - material.SetProperty("LegacyBodyDecal_PngCachePath", Path.GetRelativePath(cacheDir, legacyDecalCachePath)); + material.SetProperty("LegacyBodyDecal_PngCachePath", Path.GetRelativePath(cacheDir, legacyDecalCachePath));; + material.SetProperty("LegacyBodyDecalPath", characterInfo.CustomizeData.LegacyBodyDecalPath ?? ""); } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/MaterialComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/MaterialComposer.cs index 1a7bb1e..e660cdf 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/MaterialComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/MaterialComposer.cs @@ -35,8 +35,6 @@ public void SetPropertiesFromCharacterInfo(ParsedCharacterInfo characterInfo) SetProperty("Highlights", customizeData.Highlights); SetProperty("LipStick", customizeData.LipStick); SetProperty("FacePaintReversed", customizeData.FacePaintReversed); - SetProperty("LegacyBodyDecalPath", customizeData.LegacyBodyDecalPath ?? ""); - SetProperty("DecalPath", customizeData.DecalPath ?? ""); SetProperty("CustomizeData", JsonNode.Parse(JsonSerializer.Serialize(customizeData, JsonOptions))!); } diff --git a/Meddle/Meddle.Plugin/Models/Enums.cs b/Meddle/Meddle.Plugin/Models/Enums.cs index 777e239..10894b2 100644 --- a/Meddle/Meddle.Plugin/Models/Enums.cs +++ b/Meddle/Meddle.Plugin/Models/Enums.cs @@ -42,21 +42,21 @@ public enum CacheFileType public enum HumanModelSlotIndex { - Head = 0, - Top = 1, - Arms = 2, - Legs = 3, - Feet = 4, - Ear = 5, - Neck = 6, - Wrist = 7, - RFinger = 8, - LFinger = 9, - Hair = 10, - Face = 11, - TailEars = 12, - Glasses = 16, - Extra = 17, + Head = 0, // 0x0 + Top = 1, // 0x1 + Arms = 2, // 0x2 + Legs = 3, // 0x3 + Feet = 4, // 0x4 + Ear = 5, // 0x5 + Neck = 6, // 0x6 + Wrist = 7, // 0x7 + RFinger = 8, // 0x8 + LFinger = 9, // 0x9 + Hair = 10, // 0xA all slots < 0xA (not including) *can* have Human->LegacyBodyDecal if shpk = skin.shpk + Face = 11, // 0xB enables Human->Decal + TailEars = 12, // 0xC + Glasses = 16, // 0x10 + Extra = 17, // 0x11 } public enum HumanEquipmentSlotIndex diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index dbcb297..bbc7720 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -453,6 +453,8 @@ public class ParsedMaterialInfo(string path, string pathFromModel, string shpk, public ParsedStain? Stain0 { get; init; } public ParsedStain? Stain1 { get; init; } public ParsedMaterialInfo? SkinSlotMaterial { get; init; } + public bool ApplyDecal { get; init; } + public bool ApplyLegacyDecal { get; init; } [JsonIgnore] public IColorTableSet? ColorTable { get; } = colorTable; diff --git a/Meddle/Meddle.Plugin/Services/ResolverService.cs b/Meddle/Meddle.Plugin/Services/ResolverService.cs index 04d525e..76d7e90 100644 --- a/Meddle/Meddle.Plugin/Services/ResolverService.cs +++ b/Meddle/Meddle.Plugin/Services/ResolverService.cs @@ -208,11 +208,16 @@ private void ResolveInstance(ParsedInstance instance) return modelInfo; } - private unsafe ParsedMaterialInfo? ParseMaterial(Pointer materialPtr, Pointer modelPtr, int mtrlIdx, - Dictionary colorTableSets, - Stain? stain0, Stain? stain1, - int? textureCountFromMaterial, - ParsedMaterialInfo? skinSlotMaterial) + private unsafe ParsedMaterialInfo? ParseMaterial( + Pointer materialPtr, + Pointer modelPtr, + int mtrlIdx, + Dictionary colorTableSets, + Stain? stain0, + Stain? stain1, + int? textureCountFromMaterial, + ParsedMaterialInfo? skinSlotMaterial, + CharacterBase.ModelType modelType) { if (materialPtr == null || materialPtr.Value == null) { @@ -277,11 +282,28 @@ private void ResolveInstance(ParsedInstance instance) } } + bool applyDecal = false; + bool applyLegacyDecal = false; + if (modelType == CharacterBase.ModelType.Human && modelPtr != null && shaderName == "skin.shpk") // only apply these decals to skin shader + { + var humanSlotIndex = (HumanModelSlotIndex)modelPtr.Value->SlotIndex; + if (humanSlotIndex == HumanModelSlotIndex.Face) // can have regular decal + { + applyDecal = true; + } + else if (humanSlotIndex < HumanModelSlotIndex.Hair) // can have legacy decal + { + applyLegacyDecal = true; + } + } + var materialInfo = new ParsedMaterialInfo(materialPath, materialPathFromModel, shaderName, colorTable, textures.ToArray()) { Stain0 = stain0, Stain1 = stain1, - SkinSlotMaterial = skinSlotMaterial + SkinSlotMaterial = skinSlotMaterial, + ApplyDecal = applyDecal, + ApplyLegacyDecal = applyLegacyDecal }; return materialInfo; } @@ -325,7 +347,8 @@ private void ResolveInstance(ParsedInstance instance) var materialInfo = ParseMaterial(mtrlPtr.Value->MaterialResourceHandle, modelPtr, mtrlIdx, colorTableSets, stain0, stain1, mtrlPtr.Value->TextureCount, - skinSlotMaterial); + skinSlotMaterial, + modelType); materials.Add(materialInfo); } @@ -463,7 +486,7 @@ public record struct ParsedHumanInfo } var materialInfo = ParseMaterial(material, null, slotIdx, new Dictionary(), - null, null, null, null); + null, null, null, null, CharacterBase.ModelType.Human); return materialInfo; } From 02260e976dc557a716fa7b98ab395b94ffa18876 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:21:37 +1100 Subject: [PATCH 02/13] Update SqPackUtil.cs --- .../Meddle.Utils/Files/SqPack/SqPackUtil.cs | 55 ++----------------- 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/Meddle/Meddle.Utils/Files/SqPack/SqPackUtil.cs b/Meddle/Meddle.Utils/Files/SqPack/SqPackUtil.cs index ae6bb3d..82c18f8 100644 --- a/Meddle/Meddle.Utils/Files/SqPack/SqPackUtil.cs +++ b/Meddle/Meddle.Utils/Files/SqPack/SqPackUtil.cs @@ -6,7 +6,7 @@ namespace Meddle.Utils.Files.SqPack; public static class SqPackUtil { - public static SqPackFile? ReadFile(long offset, string datFilePath, FileType type) + public static SqPackFile? ReadFile(long offset, string datFilePath, FileType? type = null) { using var fileStream = File.OpenRead(datFilePath); using var br = new BinaryReader(fileStream); @@ -15,7 +15,7 @@ public static class SqPackUtil fileStream.Seek(offset, SeekOrigin.Begin); var header = br.Read(); - if (header.Type != type) + if (type != null && header.Type != type) { return null; } @@ -38,51 +38,8 @@ public static class SqPackUtil fileStream.Close(); } } - - public static SqPackFile ReadFile(long offset, string datFilePath) - { - using var fileStream = File.OpenRead(datFilePath); - using var br = new BinaryReader(fileStream); - try - { - fileStream.Seek(offset, SeekOrigin.Begin); - - var header = br.Read(); - switch (header.Type) - { - case FileType.Empty: - { - var data = new byte[header.RawFileSize]; - return new SqPackFile(header, data); - } - case FileType.Texture: - { - var data = ParseTexFile(offset, header, br); - return new SqPackFile(header, data); - } - case FileType.Standard: - { - var buffer = ParseStandardFile(offset, header, br); - return new SqPackFile(header, buffer); - } - case FileType.Model: - { - var data2 = ParseModelFile(offset, header, br); - return new SqPackFile(header, data2); - } - default: - throw new InvalidDataException($"Unknown file type {header.Type}"); - } - } - finally - { - br.Close(); - fileStream.Close(); - } - } - - private static ReadOnlySpan ParseStandardFile(long offset, SqPackFileInfo header, BinaryReader br) + public static ReadOnlySpan ParseStandardFile(long offset, SqPackFileInfo header, BinaryReader br) { var buffer = new byte[(int)header.RawFileSize]; using var ms = new MemoryStream(buffer); @@ -108,8 +65,8 @@ private struct ChunkInfo public ushort BlockStart; public ushort NumBlocks; } - - private static unsafe ReadOnlySpan ParseModelFile(long offset, SqPackFileInfo header, BinaryReader br) + + public static unsafe ReadOnlySpan ParseModelFile(long offset, SqPackFileInfo header, BinaryReader br) { br.BaseStream.Position = offset; var modelBlock = br.Read(); @@ -303,7 +260,7 @@ private static ReadOnlySpan ReadFileBlock(long offset, BinaryReader br) } } - private static ReadOnlySpan ParseTexFile(long offset, SqPackFileInfo header, BinaryReader br) + public static ReadOnlySpan ParseTexFile(long offset, SqPackFileInfo header, BinaryReader br) { var buffer = new byte[(int)header.RawFileSize]; using var ms = new MemoryStream(buffer); From 4112d408b01bad57210e25eec105eb2d5fe24ff9 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sun, 9 Nov 2025 17:39:44 +1100 Subject: [PATCH 03/13] Complete decal resolve refactor Support fc decal and resolve decal for non-human draw objects --- .../Models/Composer/ComposerCache.cs | 103 +++-- Meddle/Meddle.Plugin/Models/Enums.cs | 6 +- .../Models/Layout/ParsedInstance.cs | 11 +- .../Meddle.Plugin/Services/LayoutService.cs | 25 +- Meddle/Meddle.Plugin/Services/ParseService.cs | 56 --- .../Meddle.Plugin/Services/ResolverService.cs | 352 +--------------- Meddle/Meddle.Plugin/Services/StainHooks.cs | 118 ------ .../Meddle.Plugin/Services/StainProvider.cs | 57 +++ Meddle/Meddle.Plugin/Services/UI/CommonUI.cs | 7 +- Meddle/Meddle.Plugin/UI/DebugTab.cs | 17 +- Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs | 44 +- .../Utils/OnRenderMaterialUtil.cs | 398 ++++++++++++++++++ .../Meddle.Plugin/Utils/ParseMaterialUtil.cs | 336 +++++++++++++++ .../{Services => Utils}/SigUtil.cs | 3 +- Meddle/Meddle.Plugin/Utils/UIUtil.cs | 2 - .../Meddle.Utils/Export/CustomizeParameter.cs | 2 - Meddle/Meddle.Utils/Export/ShaderPackage.cs | 7 +- Meddle/Meddle.Utils/Files/MtrlFile.cs | 2 + 18 files changed, 910 insertions(+), 636 deletions(-) delete mode 100644 Meddle/Meddle.Plugin/Services/StainHooks.cs create mode 100644 Meddle/Meddle.Plugin/Services/StainProvider.cs create mode 100644 Meddle/Meddle.Plugin/Utils/OnRenderMaterialUtil.cs create mode 100644 Meddle/Meddle.Plugin/Utils/ParseMaterialUtil.cs rename Meddle/Meddle.Plugin/{Services => Utils}/SigUtil.cs (98%) diff --git a/Meddle/Meddle.Plugin/Models/Composer/ComposerCache.cs b/Meddle/Meddle.Plugin/Models/Composer/ComposerCache.cs index 7fd2595..e4a19b6 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/ComposerCache.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/ComposerCache.cs @@ -227,11 +227,6 @@ public string CacheTexture(string fullPath) File.WriteAllBytes(pngCachePath, textureBytes); return pngCachePath; } - - private static readonly IReadOnlyList SkinSlotShaders = - [ - "characterstockings.shpk" - ]; public MaterialBuilder ComposeMaterial(string mtrlPath, ParsedMaterialInfo? materialInfo = null, @@ -259,36 +254,42 @@ public MaterialBuilder ComposeMaterial(string mtrlPath, if (materialInfo != null) { // kinda janky, but we set skin data first so that the keys are still available to be overridden by the main material info. - if (materialInfo.SkinSlotMaterial != null && SkinSlotShaders.Contains(materialInfo.Shpk)) + if (materialInfo.RenderMaterialOutput != null) { - var skinMtrlFile = GetMtrlFile(materialInfo.SkinSlotMaterial.Path.FullPath, out var skinMtrlCachePath); - if (skinMtrlCachePath != null) + var render = materialInfo.RenderMaterialOutput; + if (render.DecalTexturePath != null) { - material.SetProperty("SkinMtrlCachePath", Path.GetRelativePath(cacheDir, skinMtrlCachePath)); + var decalCachePath = CacheTexture(render.DecalTexturePath); + material.SetProperty("Decal_PngCachePath", Path.GetRelativePath(cacheDir, decalCachePath)); + material.SetProperty("DecalPath", render.DecalTexturePath); } - var skinShaderPackage = GetShaderPackage(skinMtrlFile.GetShaderPackageName()); - var skinMaterial = new MaterialComposer(skinMtrlFile, materialInfo.SkinSlotMaterial.Path.FullPath, skinShaderPackage); - var constants = Names.GetConstants(); - foreach (var (key, value) in skinMaterial.ShaderKeyDict) + else if (render.DecalTexture != null) { - var category = (uint)key; - var keyMatch = constants.GetValueOrDefault(category); - var valMatch = constants.GetValueOrDefault(value); - material.SetProperty(keyMatch != null ? keyMatch.Value : $"0x{category:X8}", valMatch != null ? valMatch.Value : $"0x{value:X8}"); + var tex = render.DecalTexture; + var decalCachePath = SaveInMemoryTex(tex, "decals"); + material.SetProperty("Decal_PngCachePath", Path.GetRelativePath(cacheDir, decalCachePath)); + material.SetProperty("DecalPath", $"InMemoryTexture_{tex.GetHashCode()}"); } - foreach (var texture in skinMaterial.TextureUsageDict) + + if (render.SkinMaterialTextures.Count > 0) { - var fullPath = texture.Value.FullPath; - var match = materialInfo.Textures.FirstOrDefault(x => x.Path.GamePath == texture.Value.GamePath); - if (match != null) + foreach (var texture in render.SkinMaterialTextures) { - fullPath = match.Path.FullPath; - } + var fullPath = texture.TexturePath; + var match = materialInfo.Textures.FirstOrDefault(x => x.Path.GamePath == texture.TexturePathFromMaterial); + if (match != null) + { + fullPath = match.Path.FullPath; + } - var cachePath = CacheTexture(fullPath); - var keyUsage = $"{texture.Key}".Replace("g_Sampler", "g_SamplerSkin"); - material.SetProperty($"{keyUsage}", texture.Value.GamePath); - material.SetProperty($"{keyUsage}_PngCachePath", Path.GetRelativePath(cacheDir, cachePath)); + var cachePath = CacheTexture(fullPath); + // var keyUsage = $"{key}".Replace("g_Sampler", "g_SamplerSkin"); + if (shaderPackage.Textures.TryGetValue(texture.TargetSamplerCrc, out var samplerName)) + { + material.SetProperty($"{samplerName}", texture.TexturePath); + material.SetProperty($"{samplerName}_PngCachePath", Path.GetRelativePath(cacheDir, cachePath)); + } + } } } @@ -296,26 +297,24 @@ public MaterialBuilder ComposeMaterial(string mtrlPath, if (materialInfo.ColorTable != null) { material.SetPropertiesFromColorTable(materialInfo.ColorTable); - // since colortables are purely in-memory, they dont have a path. - // going to store them in the 'ColorTables' directory in the cache with a unique name based on the hash. if (materialInfo.ColorTable is ColorTableSet colorTableSet) { var tex = colorTableSet.ColorTable.ToTexture(); - var colorTablePath = SaveColorTableTex(tex); + var colorTablePath = SaveInMemoryTex(tex, "color_tables"); material.SetProperty("ColorTable_PngCachePath", Path.GetRelativePath(cacheDir, colorTablePath)); } else if (materialInfo.ColorTable is LegacyColorTableSet legacyColorTableSet) { var tex = legacyColorTableSet.ColorTable.ToTexture(); - var colorTablePath = SaveColorTableTex(tex); + var colorTablePath = SaveInMemoryTex(tex, "color_tables"); material.SetProperty("LegacyColorTable_PngCachePath", Path.GetRelativePath(cacheDir, colorTablePath)); } } - string SaveColorTableTex(SkTexture tex) + string SaveInMemoryTex(SkTexture tex, string type) { - var colorTableCacheDir = Path.Combine(cacheDir, "color_tables"); - Directory.CreateDirectory(colorTableCacheDir); + var texCacheDir = Path.Combine(cacheDir, type); + Directory.CreateDirectory(texCacheDir); var buf = tex.Bitmap.Bytes; var hash = System.Security.Cryptography.SHA256.HashData(buf); var hashStr = Convert.ToHexStringLower(hash); @@ -325,7 +324,7 @@ string SaveColorTableTex(SkTexture tex) hashStr = hashStr[..8]; } var mtrlPathWithoutExtension = Path.GetFileNameWithoutExtension(mtrlPath); - var colorTablePath = Path.Combine(colorTableCacheDir, $"{mtrlPathWithoutExtension}_{materialInfo.Shpk}_{hashStr}.png"); + var colorTablePath = Path.Combine(texCacheDir, $"{mtrlPathWithoutExtension}_{materialInfo.Shpk}_{hashStr}.png"); if (!File.Exists(colorTablePath)) { using var fileStream = new FileStream(colorTablePath, FileMode.Create, FileAccess.Write); @@ -362,23 +361,23 @@ string SaveColorTableTex(SkTexture tex) // remove full path prefix, get only dir below cache dir. material.SetProperty($"{texture.Key}_PngCachePath", Path.GetRelativePath(cacheDir, cachePath)); } - - if (characterInfo != null) - { - if (characterInfo.CustomizeData?.DecalPath != null && materialInfo?.ApplyDecal == true) - { - var decalCachePath = CacheTexture(characterInfo.CustomizeData.DecalPath); - material.SetProperty("Decal_PngCachePath", Path.GetRelativePath(cacheDir, decalCachePath)); - material.SetProperty("DecalPath", characterInfo.CustomizeData.DecalPath ?? ""); - } - - if (characterInfo.CustomizeData?.LegacyBodyDecalPath != null && materialInfo?.ApplyLegacyDecal == true) - { - var legacyDecalCachePath = CacheTexture(characterInfo.CustomizeData.LegacyBodyDecalPath); - material.SetProperty("LegacyBodyDecal_PngCachePath", Path.GetRelativePath(cacheDir, legacyDecalCachePath));; - material.SetProperty("LegacyBodyDecalPath", characterInfo.CustomizeData.LegacyBodyDecalPath ?? ""); - } - } + // + // if (characterInfo != null) + // { + // if (characterInfo.CustomizeData?.DecalPath != null && materialInfo?.ApplyDecal == true) + // { + // var decalCachePath = CacheTexture(characterInfo.CustomizeData.DecalPath); + // material.SetProperty("Decal_PngCachePath", Path.GetRelativePath(cacheDir, decalCachePath)); + // material.SetProperty("DecalPath", characterInfo.CustomizeData.DecalPath ?? ""); + // } + // + // if (characterInfo.CustomizeData?.LegacyBodyDecalPath != null && materialInfo?.ApplyLegacyDecal == true) + // { + // var legacyDecalCachePath = CacheTexture(characterInfo.CustomizeData.LegacyBodyDecalPath); + // material.SetProperty("LegacyBodyDecal_PngCachePath", Path.GetRelativePath(cacheDir, legacyDecalCachePath));; + // material.SetProperty("LegacyBodyDecalPath", characterInfo.CustomizeData.LegacyBodyDecalPath ?? ""); + // } + // } materialBuilder.Extras = material.ExtrasNode; diff --git a/Meddle/Meddle.Plugin/Models/Enums.cs b/Meddle/Meddle.Plugin/Models/Enums.cs index 10894b2..fff0b0a 100644 --- a/Meddle/Meddle.Plugin/Models/Enums.cs +++ b/Meddle/Meddle.Plugin/Models/Enums.cs @@ -46,13 +46,13 @@ public enum HumanModelSlotIndex Top = 1, // 0x1 Arms = 2, // 0x2 Legs = 3, // 0x3 - Feet = 4, // 0x4 + Feet = 4, // 0x4 all slots <= 0x4 *can* have skin material assigned if shpk = skin.shpk Ear = 5, // 0x5 Neck = 6, // 0x6 Wrist = 7, // 0x7 RFinger = 8, // 0x8 - LFinger = 9, // 0x9 - Hair = 10, // 0xA all slots < 0xA (not including) *can* have Human->LegacyBodyDecal if shpk = skin.shpk + LFinger = 9, // 0x9 all slots <= 0x9 *can* have Human->LegacyBodyDecal if shpk = skin.shpk + Hair = 10, // 0xA Face = 11, // 0xB enables Human->Decal TailEars = 12, // 0xC Glasses = 16, // 0x10 diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index bbc7720..70fa6a7 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -446,15 +446,13 @@ public class ParsedTextureInfo(string path, string pathFromMaterial, TextureReso public TextureResource Resource { get; } = resource; } -public class ParsedMaterialInfo(string path, string pathFromModel, string shpk, IColorTableSet? colorTable, ParsedTextureInfo[] textures) +public class ParsedMaterialInfo(string path, string pathFromModel, string shpk, OnRenderMaterialOutput? renderMaterialOutput, IColorTableSet? colorTable, ParsedTextureInfo[] textures) { public HandleString Path { get; } = new() { FullPath = path, GamePath = pathFromModel }; public string Shpk { get; } = shpk; + public OnRenderMaterialOutput? RenderMaterialOutput { get; } = renderMaterialOutput; public ParsedStain? Stain0 { get; init; } public ParsedStain? Stain1 { get; init; } - public ParsedMaterialInfo? SkinSlotMaterial { get; init; } - public bool ApplyDecal { get; init; } - public bool ApplyLegacyDecal { get; init; } [JsonIgnore] public IColorTableSet? ColorTable { get; } = colorTable; @@ -519,11 +517,10 @@ public record ParsedCharacterInfo public IReadOnlyList Models; public readonly ParsedSkeleton Skeleton; public readonly ParsedAttach Attach; - private readonly ResolverService.ParsedHumanInfo humanInfo; + private readonly ParsedHumanInfo humanInfo; public IReadOnlyList Attaches = []; public CustomizeData? CustomizeData => humanInfo.CustomizeData; public CustomizeParameter? CustomizeParameter => humanInfo.CustomizeParameter; - public IReadOnlyList SkinSlotMaterials => humanInfo.SkinSlotMaterials; public IReadOnlyList EquipmentModelIds => humanInfo.EquipmentModelIds; public GenderRace GenderRace => humanInfo.GenderRace; @@ -531,7 +528,7 @@ public ParsedCharacterInfo( ParsedModelInfo[] models, ParsedSkeleton skeleton, ParsedAttach attach, - ResolverService.ParsedHumanInfo humanInfo) + ParsedHumanInfo humanInfo) { Models = models; Skeleton = skeleton; diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index d8a0dbb..e831716 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -29,7 +29,7 @@ public class LayoutService : IService, IDisposable { private readonly ILogger logger; private readonly IFramework framework; - private readonly StainHooks stainHooks; + private readonly StainProvider stainProvider; private readonly Configuration config; private readonly SigUtil sigUtil; @@ -37,13 +37,13 @@ public LayoutService( SigUtil sigUtil, ILogger logger, IFramework framework, - StainHooks stainHooks, + StainProvider stainProvider, Configuration config) { this.sigUtil = sigUtil; this.logger = logger; this.framework = framework; - this.stainHooks = stainHooks; + this.stainProvider = stainProvider; this.config = config; this.framework.Update += Update; } @@ -636,16 +636,17 @@ private unsafe HousingTerritoryData ParseTerritoryFurniture(LayoutManager* activ if (territory == null || territory->IsLoaded() == false) return HousingTerritoryData.Empty; var type = territory->GetTerritoryType(); + var furniture = type switch { - HousingTerritoryType.Indoor => ((IndoorTerritory*)territory)->Furniture, - HousingTerritoryType.Outdoor => ((OutdoorTerritory*)territory)->Furniture, + HousingTerritoryType.Indoor => ((IndoorTerritory*)territory)->FurnitureManager.FurnitureMemory, + HousingTerritoryType.Outdoor => ((OutdoorTerritory*)territory)->FurnitureStruct.FurnitureMemory, _ => [] }; var objectManager = type switch { - HousingTerritoryType.Indoor => &((IndoorTerritory*)territory)->HousingObjectManager, - HousingTerritoryType.Outdoor => &((OutdoorTerritory*)territory)->HousingObjectManager, + HousingTerritoryType.Indoor => &((IndoorTerritory*)territory)->FurnitureManager.ObjectManager, + HousingTerritoryType.Outdoor => &((OutdoorTerritory*)territory)->FurnitureStruct.ObjectManager, _ => null }; @@ -687,7 +688,7 @@ bool IsSgbHandleValid(SharedGroupResourceHandle* handle) continue; } - var defaultStain = stainHooks.GetStain(housingSettings.Value->DefaultColorId); + var defaultStain = StainProvider.GetStain(housingSettings.Value->DefaultColorId); if (defaultStain == null) { logger.LogWarning("Default stain is null for fixture {FixtureId}", meddleFixture.FixtureId); @@ -695,7 +696,7 @@ bool IsSgbHandleValid(SharedGroupResourceHandle* handle) } string? fixtureName = null; - if (meddleFixture.FixtureId != 0 && stainHooks.HousingDict.TryGetValue(meddleFixture.FixtureId, out var itemId) && stainHooks.ItemDict.TryGetValue(itemId, out var item)) + if (meddleFixture.FixtureId != 0 && StainProvider.HousingDict.TryGetValue(meddleFixture.FixtureId, out var itemId) && StainProvider.ItemDict.TryGetValue(itemId, out var item)) { fixtureName = item.Name.ToString(); } @@ -704,7 +705,7 @@ bool IsSgbHandleValid(SharedGroupResourceHandle* handle) { FixtureName = fixtureName, FixtureId = meddleFixture.FixtureId, - Stain = stainHooks.GetStain(meddleFixture.StainId), + Stain = StainProvider.GetStain(meddleFixture.StainId), DefaultStain = defaultStain.Value, LayoutInstance = meddleFixture.UnkGroup->FixtureLayoutInstance }); @@ -766,8 +767,8 @@ bool IsSgbHandleValid(SharedGroupResourceHandle* handle) GameObject = housingObjectPtr, LayoutInstance = layoutInstance, HousingFurniture = item, - Stain = stainHooks.GetStain(item.Stain), - DefaultStain = stainHooks.GetStain(housingSettings.Value->DefaultColorId)!.Value, + Stain = StainProvider.GetStain(item.Stain), + DefaultStain = StainProvider.GetStain(housingSettings.Value->DefaultColorId)!.Value, }); } diff --git a/Meddle/Meddle.Plugin/Services/ParseService.cs b/Meddle/Meddle.Plugin/Services/ParseService.cs index 1b9c4ac..2760e40 100644 --- a/Meddle/Meddle.Plugin/Services/ParseService.cs +++ b/Meddle/Meddle.Plugin/Services/ParseService.cs @@ -35,60 +35,4 @@ public void Dispose() { logger.LogDebug("Disposing ParseUtil"); } - public unsafe Dictionary ParseColorTableTextures(CharacterBase* characterBase) - { - var colorTableTextures = new Dictionary(); - for (var i = 0; i < characterBase->ColorTableTexturesSpan.Length; i++) - { - var colorTableTex = characterBase->ColorTableTexturesSpan[i]; - if (colorTableTex == null) continue; - - var colorTableTexture = colorTableTex.Value; - if (colorTableTexture != null) - { - var colorTableSet = ParseColorTableTexture(colorTableTexture); - colorTableTextures[i] = colorTableSet; - } - } - - return colorTableTextures; - } - - // Only call from main thread or you will probably crash - public unsafe IColorTableSet ParseColorTableTexture(Texture* colorTableTexture) - { - var (colorTableRes, stride) = DxHelper.ExportTextureResource(colorTableTexture); - if ((TexFile.TextureFormat)colorTableTexture->TextureFormat != TexFile.TextureFormat.R16G16B16A16F) - { - throw new ArgumentException( - $"Color table is not R16G16B16A16F ({(TexFile.TextureFormat)colorTableTexture->TextureFormat})"); - } - - if (colorTableTexture->ActualWidth == 4 && colorTableTexture->ActualHeight == 16) - { - // legacy table - var stridedData = ImageUtils.AdjustStride((int)stride, (int)colorTableTexture->ActualWidth * 8, - (int)colorTableTexture->ActualHeight, colorTableRes.Data); - var reader = new SpanBinaryReader(stridedData); - return new LegacyColorTableSet - { - ColorTable = new LegacyColorTable(ref reader) - }; - } - - if (colorTableTexture->ActualWidth == 8 && colorTableTexture->ActualHeight == 32) - { - // new table - var stridedData = ImageUtils.AdjustStride((int)stride, (int)colorTableTexture->ActualWidth * 8, - (int)colorTableTexture->ActualHeight, colorTableRes.Data); - var reader = new SpanBinaryReader(stridedData); - return new ColorTableSet - { - ColorTable = new ColorTable(ref reader) - }; - } - - throw new ArgumentException( - $"Color table is not 4x16 or 8x32 ({colorTableTexture->ActualWidth}x{colorTableTexture->ActualHeight})"); - } } diff --git a/Meddle/Meddle.Plugin/Services/ResolverService.cs b/Meddle/Meddle.Plugin/Services/ResolverService.cs index 76d7e90..adeef23 100644 --- a/Meddle/Meddle.Plugin/Services/ResolverService.cs +++ b/Meddle/Meddle.Plugin/Services/ResolverService.cs @@ -1,25 +1,14 @@ -using System.Numerics; -using System.Runtime.InteropServices; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; -using Lumina.Excel.Sheets; -using Meddle.Plugin.Models; using Meddle.Plugin.Models.Layout; using Meddle.Plugin.Utils; -using Meddle.Utils; -using Meddle.Utils.Constants; using Meddle.Utils.Files; using Meddle.Utils.Files.SqPack; -using Meddle.Utils.Files.Structs.Material; using Meddle.Utils.Helpers; using Microsoft.Extensions.Logging; -using CustomizeData = Meddle.Utils.Export.CustomizeData; -using CustomizeParameter = Meddle.Plugin.Models.Structs.CustomizeParameter; -using Model = FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model; namespace Meddle.Plugin.Services; @@ -28,27 +17,20 @@ public class ResolverService : IService private readonly ILogger logger; private readonly LayoutService layoutService; private readonly SqPack pack; - private readonly ParseService parseService; private readonly IFramework framework; - private readonly StainHooks stainHooks; private readonly PbdHooks pbdHooks; public ResolverService( ILogger logger, LayoutService layoutService, SqPack pack, - ParseService parseService, IFramework framework, - StainHooks stainHooks, - IDataManager dataManager, PbdHooks pbdHooks) { this.logger = logger; this.layoutService = layoutService; this.pack = pack; - this.parseService = parseService; this.framework = framework; - this.stainHooks = stainHooks; this.pbdHooks = pbdHooks; } @@ -88,7 +70,7 @@ private unsafe void ResolveParsedCharacterInstance(ParsedCharacterInstance chara if (characterInstance.IdType == ParsedCharacterInstance.ParsedCharacterInstanceIdType.CharacterBase) { var cBase = (CharacterBase*)characterInstance.Id; - var characterInfo = ParseDrawObject(&cBase->DrawObject); + var characterInfo = ParseMaterialUtil.ParseDrawObject(&cBase->DrawObject, pbdHooks); characterInstance.CharacterInfo = characterInfo; } else @@ -101,7 +83,7 @@ private unsafe void ResolveParsedCharacterInstance(ParsedCharacterInstance chara } else { - var characterInfo = ParseDrawObject(gameObject->DrawObject); + var characterInfo = ParseMaterialUtil.ParseDrawObject(gameObject->DrawObject, pbdHooks); characterInstance.CharacterInfo = characterInfo; } } @@ -149,6 +131,9 @@ private void ResolveInstance(ParsedInstance instance) } } + /// + /// Used for terrain + /// public ParsedModelInfo? ParseModelFromPath(string path) { var modelResource = pack.GetFile(path); @@ -199,7 +184,7 @@ private void ResolveInstance(ParsedInstance instance) textures.Add(texInfo); } - var materialInfo = new ParsedMaterialInfo(mtrlName, mtrlName, shaderName, colorTable, textures.ToArray()); + var materialInfo = new ParsedMaterialInfo(mtrlName, mtrlName, shaderName, null, colorTable, textures.ToArray()); materials.Add(materialInfo); } @@ -208,157 +193,9 @@ private void ResolveInstance(ParsedInstance instance) return modelInfo; } - private unsafe ParsedMaterialInfo? ParseMaterial( - Pointer materialPtr, - Pointer modelPtr, - int mtrlIdx, - Dictionary colorTableSets, - Stain? stain0, - Stain? stain1, - int? textureCountFromMaterial, - ParsedMaterialInfo? skinSlotMaterial, - CharacterBase.ModelType modelType) + public ParsedCharacterInfo? ParseDrawObject(Pointer drawObject) { - if (materialPtr == null || materialPtr.Value == null) - { - return null; - } - - var material = materialPtr.Value; - var materialPath = material->FileName.ParseString(); - - var model = modelPtr.Value; - string? materialPathFromModel; - if (model != null) - { - materialPathFromModel = modelPtr.Value->ModelResourceHandle->GetMaterialFileNameBySlot((uint)mtrlIdx); - } - else - { - materialPathFromModel = materialPath; - } - - var shaderName = material->ShpkName; - - IColorTableSet? colorTable = null; - if (model != null && colorTableSets.TryGetValue((int)(modelPtr.Value->SlotIndex * CharacterBase.MaterialsPerSlot) + mtrlIdx, out var gpuColorTable)) - { - colorTable = gpuColorTable; - } - else if (material->HasColorTable) - { - var colorTableSpan = material->ColorTableSpan; - if (colorTableSpan.Length == ColorTable.Size) - { - var reader = new SpanBinaryReader(MemoryMarshal.AsBytes(colorTableSpan)); - colorTable = new ColorTableSet - { - ColorTable = new ColorTable(ref reader) - }; - } - else if (colorTableSpan.Length == LegacyColorTable.Size) - { - var reader = new SpanBinaryReader(MemoryMarshal.AsBytes(colorTableSpan)); - colorTable = new LegacyColorTableSet - { - ColorTable = new LegacyColorTable(ref reader) - }; - } - } - - var textures = new List(); - for (var texIdx = 0; texIdx < material->TexturesSpan.Length; texIdx++) - { - var texturePtr = material->TexturesSpan[texIdx]; - if (texturePtr.TextureResourceHandle == null) continue; - - var texturePath = texturePtr.TextureResourceHandle->FileName.ParseString(); - if (texIdx < textureCountFromMaterial || textureCountFromMaterial == null) - { - var texturePathFromMaterial = material->TexturePath(texIdx); - var (resource, _) = DxHelper.ExportTextureResource(texturePtr.TextureResourceHandle->Texture); - var textureInfo = new ParsedTextureInfo(texturePath, texturePathFromMaterial, resource); - textures.Add(textureInfo); - } - } - - bool applyDecal = false; - bool applyLegacyDecal = false; - if (modelType == CharacterBase.ModelType.Human && modelPtr != null && shaderName == "skin.shpk") // only apply these decals to skin shader - { - var humanSlotIndex = (HumanModelSlotIndex)modelPtr.Value->SlotIndex; - if (humanSlotIndex == HumanModelSlotIndex.Face) // can have regular decal - { - applyDecal = true; - } - else if (humanSlotIndex < HumanModelSlotIndex.Hair) // can have legacy decal - { - applyLegacyDecal = true; - } - } - - var materialInfo = new ParsedMaterialInfo(materialPath, materialPathFromModel, shaderName, colorTable, textures.ToArray()) - { - Stain0 = stain0, - Stain1 = stain1, - SkinSlotMaterial = skinSlotMaterial, - ApplyDecal = applyDecal, - ApplyLegacyDecal = applyLegacyDecal - }; - return materialInfo; - } - - public unsafe ParsedModelInfo? ParseModel(Pointer characterBasePtr, Pointer modelPtr, Dictionary colorTableSets) - { - if (modelPtr == null) return null; - var model = modelPtr.Value; - if (model == null) return null; - var modelPath = model->ModelResourceHandle->ResourceHandle.FileName.ParseString(); - if (characterBasePtr == null) return null; - if (characterBasePtr.Value == null) return null; - var characterBase = characterBasePtr.Value; - var modelPathFromCharacter = characterBase->ResolveMdlPath(model->SlotIndex); - var shapeAttributeGroup = StructExtensions.ParseModelShapeAttributes(model); - - // var stain0 = stainHooks.GetStainFromCache((nint)characterBasePtr.Value, model->SlotIndex, 0); - // var stain1 = stainHooks.GetStainFromCache((nint)characterBasePtr.Value, model->SlotIndex, 1); - var modelType = characterBase->GetModelType(); - Stain? stain0 = null; - Stain? stain1 = null; - ParsedMaterialInfo? skinSlotMaterial = null; - if (modelType == CharacterBase.ModelType.Human) - { - var equipId = GetEquipmentModelId(characterBasePtr.Value, (HumanEquipmentSlotIndex)model->SlotIndex); - stain0 = equipId != null ? stainHooks.GetStain(equipId.Value.Stain0) : null; - stain1 = equipId != null ? stainHooks.GetStain(equipId.Value.Stain1) : null; - var human = (Human*)characterBasePtr.Value; - skinSlotMaterial = GetSkinSlotMaterial(human, (int)model->SlotIndex); - } - - var materials = new List(); - for (var mtrlIdx = 0; mtrlIdx < model->MaterialsSpan.Length; mtrlIdx++) - { - var mtrlPtr = model->MaterialsSpan[mtrlIdx]; - if (mtrlPtr == null || mtrlPtr.Value == null) - { - materials.Add(null); - continue; - } - var materialInfo = ParseMaterial(mtrlPtr.Value->MaterialResourceHandle, modelPtr, mtrlIdx, colorTableSets, - stain0, stain1, - mtrlPtr.Value->TextureCount, - skinSlotMaterial, - modelType); - materials.Add(materialInfo); - } - - var deform = modelType == CharacterBase.ModelType.Human ? pbdHooks.TryGetDeformer((nint)characterBasePtr.Value, model->SlotIndex) : null; - var modelInfo = new ParsedModelInfo(modelPath, modelPathFromCharacter, deform, shapeAttributeGroup, materials.ToArray(), stain0, stain1) - { - ModelAddress = (nint)modelPtr.Value - }; - - return modelInfo; + return ParseMaterialUtil.ParseDrawObject(drawObject, pbdHooks); } public unsafe ParsedCharacterInfo? ParseCharacter(Character* character) @@ -374,7 +211,7 @@ private void ResolveInstance(ParsedInstance instance) return null; } - var characterInfo = ParseDrawObject(drawObject); + var characterInfo = ParseMaterialUtil.ParseDrawObject(drawObject, pbdHooks); if (characterInfo == null) { return null; @@ -395,7 +232,7 @@ private void ResolveInstance(ParsedInstance instance) foreach (var weapon in character->DrawData.WeaponData) { - var weaponInfo = ParseDrawObject(weapon.DrawObject); + var weaponInfo = ParseMaterialUtil.ParseDrawObject(weapon.DrawObject, pbdHooks); if (weaponInfo != null) { attaches.Add(weaponInfo); @@ -406,171 +243,4 @@ private void ResolveInstance(ParsedInstance instance) return characterInfo; } - - public unsafe ParsedCharacterInfo? ParseDrawObject(DrawObject* drawObject) - { - if (drawObject == null) - { - return null; - } - - var objectType = drawObject->Object.GetObjectType(); - if (objectType != ObjectType.CharacterBase) - { - return null; - } - - var characterBase = (CharacterBase*)drawObject; - var colorTableTextures = parseService.ParseColorTableTextures(characterBase); - var models = new List(); - var modelType = characterBase->GetModelType(); - - foreach (var modelPtr in characterBase->ModelsSpan) - { - var modelInfo = ParseModel(characterBase, modelPtr, colorTableTextures); - if (modelInfo != null) - models.Add(modelInfo); - } - - var skeleton = StructExtensions.GetParsedSkeleton(characterBase); - var parsedHumanInfo = ParseHuman(characterBase); - return new ParsedCharacterInfo(models.ToArray(), skeleton, StructExtensions.GetParsedAttach(characterBase), parsedHumanInfo); - } - - public record struct ParsedHumanInfo - { - public Meddle.Utils.Export.CustomizeParameter? CustomizeParameter; - public CustomizeData? CustomizeData; - public GenderRace GenderRace; - public IReadOnlyList SkinSlotMaterials; - public IReadOnlyList EquipmentModelIds; - } - - public static unsafe EquipmentModelId? GetEquipmentModelId(CharacterBase* characterBase, HumanEquipmentSlotIndex slotIdx) - { - if (characterBase == null) - { - return null; - } - - if (!Enum.IsDefined(slotIdx)) - { - return null; - } - - if (characterBase->GetModelType() != CharacterBase.ModelType.Human) - { - return null; - } - var human = (Human*)characterBase; - var equipId = (&human->Head)[(int)slotIdx]; - return equipId; - } - - private unsafe ParsedMaterialInfo? GetSkinSlotMaterial(Human* human, int slotIdx) - { - if (human == null) - { - return null; - } - - if (slotIdx < 0 || slotIdx >= human->SlotSkinMaterials.Length) - { - return null; - } - - var material = human->SlotSkinMaterials[slotIdx]; - if (material == null || material.Value == null) - { - return null; - } - - var materialInfo = ParseMaterial(material, null, slotIdx, new Dictionary(), - null, null, null, null, CharacterBase.ModelType.Human); - return materialInfo; - } - - private unsafe IReadOnlyList GetSkinSlotMaterials(Human* human) - { - var skinSlotMaterials = new List(); - for (var mtrlIdx = 0; mtrlIdx < human->SlotSkinMaterials.Length; mtrlIdx++) - { - var materialInfo = GetSkinSlotMaterial(human, mtrlIdx); - skinSlotMaterials.Add(materialInfo); - } - - return skinSlotMaterials; - } - - public unsafe ParsedHumanInfo ParseHuman(CharacterBase* characterBase) - { - var modelType = characterBase->GetModelType(); - if (modelType != CharacterBase.ModelType.Human) - { - return new ParsedHumanInfo - { - CustomizeParameter = null, - CustomizeData = null, - GenderRace = GenderRace.Unknown, - SkinSlotMaterials = [], - EquipmentModelIds = [] - }; - } - - var human = (Human*)characterBase; - var customizeCBuf = human->CustomizeParameterCBuffer->TryGetBuffer()[0]; - var decalCol = human->DecalColorCBuffer->TryGetBuffer()[0]; - var customizeParams = new Meddle.Utils.Export.CustomizeParameter - { - SkinColor = customizeCBuf.SkinColor, - MuscleTone = customizeCBuf.MuscleTone, - SkinFresnelValue0 = customizeCBuf.SkinFresnelValue0, - LipColor = customizeCBuf.LipColor, - MainColor = customizeCBuf.MainColor, - FacePaintUvMultiplier = customizeCBuf.FacePaintUVMultiplier, - HairFresnelValue0 = customizeCBuf.HairFresnelValue0, - MeshColor = customizeCBuf.MeshColor, - FacePaintUvOffset = customizeCBuf.FacePaintUVOffset, - LeftColor = customizeCBuf.LeftColor, - RightColor = customizeCBuf.RightColor, - OptionColor = customizeCBuf.OptionColor, - DecalColor = decalCol - }; - var customizeData = new CustomizeData - { - LipStick = human->Customize.Lipstick, - Highlights = human->Customize.Highlights, - DecalPath = GetTexturePath(human->Decal), - LegacyBodyDecalPath = GetTexturePath(human->LegacyBodyDecal), - FacePaintReversed = human->Customize.FacePaintReversed, - }; - var genderRace = (GenderRace)human->RaceSexId; - var skinSlotMaterials = GetSkinSlotMaterials(human); - var equipData = new List(); - for (var slotIdx = 0; slotIdx <= (int)HumanEquipmentSlotIndex.Extra; slotIdx++) - { - equipData.Add(GetEquipmentModelId(characterBase, (HumanEquipmentSlotIndex)slotIdx)!.Value); - } - - return new ParsedHumanInfo - { - CustomizeParameter = customizeParams, - CustomizeData = customizeData, - GenderRace = genderRace, - SkinSlotMaterials = skinSlotMaterials, - EquipmentModelIds = equipData.ToArray() - }; - } - - private unsafe string? GetTexturePath(Pointer ptr) - { - if (ptr == null || ptr.Value == null) - { - return null; - } - - var textureResourceHandle = ptr.Value; - var texturePath = textureResourceHandle->FileName.ParseString(); - return texturePath; - } } diff --git a/Meddle/Meddle.Plugin/Services/StainHooks.cs b/Meddle/Meddle.Plugin/Services/StainHooks.cs deleted file mode 100644 index 5966260..0000000 --- a/Meddle/Meddle.Plugin/Services/StainHooks.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Numerics; -using Dalamud.Bindings.ImGui; -using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using Lumina.Excel.Sheets; -using Meddle.Plugin.Utils; -using Microsoft.Extensions.Logging; - -namespace Meddle.Plugin.Services; - -public unsafe class StainHooks : IDisposable, IService -{ - private readonly ILogger logger; - // private const string HumanGetDyeForSlotSignature = "83 FA 09 77 16 8B D2"; - // private readonly Hook? humanGetDyeForSlotHook; - // private delegate uint HumanGetDyeForSlotDelegate(Human* a1, uint a2, uint a3); - // private const string DemiHumanGetDyeForSlotSignature = "44 8B CA 49 C1 E1 05 4C 03 89 ?? ?? ?? ??"; - // private readonly Hook? demiHumanGetDyeForSlotHook; - // private delegate uint DemiHumanGetDyeForSlotDelegate(CharacterBase* a1, uint a2, uint a3); - // private const string WeaponGetDyeForSlotSignature = "48 8B 81 ?? ?? ?? ?? 41 8B D0 0F B6 44 10 ??"; - // private readonly Hook? weaponGetDyeForSlotHook; - // private delegate uint WeaponGetDyeForSlotDelegate(Weapon* a1, uint a2, uint a3); - // private readonly Dictionary<(nint, uint, uint), uint> dyeCache = new(); - private readonly Dictionary stainDict; - private readonly Dictionary housingDict; - private readonly Dictionary itemDict; - private readonly Dictionary housingExterior; - public IReadOnlyDictionary StainDict => stainDict; - public IReadOnlyDictionary HousingDict => housingDict; - public IReadOnlyDictionary ItemDict => itemDict; - public IReadOnlyDictionary HousingExterior => housingExterior; - - // public uint? GetDyeFromCache(nint obj, uint slotIdx, uint dyeChannel) - // { - // uint? result = dyeCache.TryGetValue((obj, slotIdx, dyeChannel), out var dye) ? dye : null; - // return result; - // } - // - // public (Stain Stain, Vector4 Color)? GetStainFromCache(nint obj, uint slotIdx, uint dyeChannel) - // { - // uint? result = dyeCache.TryGetValue((obj, slotIdx, dyeChannel), out var dye) ? dye : null; - // if (result == null) return null; - // var stain = GetStain(result.Value); - // if (stain == null) return null; - // var color = GetStainColor(stain.Value); - // return (stain.Value, color); - // } - - public Stain? GetStain(uint rowId) - { - return stainDict.TryGetValue(rowId, out var stain) ? stain : null; - } - - public static Vector4 GetStainColor(Stain stain) - { - var mapped = UiUtil.SeColorToRgba(stain.Color); - return ImGui.ColorConvertU32ToFloat4(mapped); - } - - public StainHooks(ILogger logger, IDataManager dataManager, HookManager hookManager) - { - this.logger = logger; - stainDict = dataManager.GetExcelSheet().ToDictionary(row => row.RowId, row => row); - var housingData = dataManager.GetExcelSheet(); - housingExterior = dataManager.GetExcelSheet().ToDictionary(row => row.RowId, row => row); - housingDict = new Dictionary(); - foreach (var housingItem in housingData) - { - housingDict[housingItem.Roof.RowId] = housingItem.RowId; - housingDict[housingItem.Walls.RowId] = housingItem.RowId; - housingDict[housingItem.Windows.RowId] = housingItem.RowId; - housingDict[housingItem.Door.RowId] = housingItem.RowId; - housingDict[housingItem.Fence.RowId] = housingItem.RowId; - housingDict[housingItem.OptionalRoof.RowId] = housingItem.RowId; - housingDict[housingItem.OptionalWall.RowId] = housingItem.RowId; - housingDict[housingItem.OptionalSignboard.RowId] = housingItem.RowId; - } - itemDict = dataManager.GetExcelSheet() - .Where(item => item.AdditionalData.RowId != 0 && item.ItemSearchCategory.RowId is 65 or 66) - .ToDictionary(row => row.AdditionalData.RowId, row => row); - - // humanGetDyeForSlotHook = hookManager.CreateHook(HumanGetDyeForSlotSignature, Human_GetDyeForSlotDetour); - // humanGetDyeForSlotHook?.Enable(); - // - // demiHumanGetDyeForSlotHook = hookManager.CreateHook(DemiHumanGetDyeForSlotSignature, DemiHuman_GetDyeForSlotDetour); - // demiHumanGetDyeForSlotHook?.Enable(); - // - // weaponGetDyeForSlotHook = hookManager.CreateHook(WeaponGetDyeForSlotSignature, Weapon_GetDyeForSlotDetour); - // weaponGetDyeForSlotHook?.Enable(); - } - - // private uint Weapon_GetDyeForSlotDetour(Weapon* a1, uint slotIdx, uint dyeChannel) - // { - // var result = weaponGetDyeForSlotHook!.Original(a1, slotIdx, dyeChannel); - // dyeCache[((nint)a1, slotIdx, dyeChannel)] = result; - // return result; - // } - // - // private uint Human_GetDyeForSlotDetour(Human* a1, uint slotIdx, uint dyeChannel) - // { - // var result = humanGetDyeForSlotHook!.Original(a1, slotIdx, dyeChannel); - // dyeCache[((nint)a1, slotIdx, dyeChannel)] = result; - // return result; - // } - // - // private uint DemiHuman_GetDyeForSlotDetour(CharacterBase* a1, uint slotIdx, uint dyeChannel) - // { - // var result = demiHumanGetDyeForSlotHook!.Original(a1, slotIdx, dyeChannel); - // dyeCache[((nint)a1, slotIdx, dyeChannel)] = result; - // return result; - // } - - public void Dispose() - { - logger.LogDebug("Disposing StainHooks"); - } -} diff --git a/Meddle/Meddle.Plugin/Services/StainProvider.cs b/Meddle/Meddle.Plugin/Services/StainProvider.cs new file mode 100644 index 0000000..ad2dc32 --- /dev/null +++ b/Meddle/Meddle.Plugin/Services/StainProvider.cs @@ -0,0 +1,57 @@ +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.Services; +using Lumina.Excel.Sheets; +using Meddle.Plugin.Utils; +using Microsoft.Extensions.Logging; + +namespace Meddle.Plugin.Services; + +public class StainProvider : IDisposable, IService +{ + private readonly ILogger logger; + public static IReadOnlyDictionary StainDict { get; private set; } = null!; + private static Dictionary HousingDictPriv = null!; + public static IReadOnlyDictionary HousingDict => HousingDictPriv; + public static IReadOnlyDictionary ItemDict { get; private set; } = null!; + public static IReadOnlyDictionary HousingExterior { get; private set; } = null!; + + public static Stain? GetStain(uint rowId) + { + return StainDict.TryGetValue(rowId, out var stain) ? stain : null; + } + + public static Vector4 GetStainColor(Stain stain) + { + var mapped = UiUtil.SeColorToRgba(stain.Color); + return ImGui.ColorConvertU32ToFloat4(mapped); + } + + public StainProvider(ILogger logger, IDataManager dataManager, HookManager hookManager) + { + this.logger = logger; + StainDict = dataManager.GetExcelSheet().ToDictionary(row => row.RowId, row => row); + var housingData = dataManager.GetExcelSheet(); + HousingExterior = dataManager.GetExcelSheet().ToDictionary(row => row.RowId, row => row); + HousingDictPriv = new Dictionary(); + foreach (var housingItem in housingData) + { + HousingDictPriv[housingItem.Roof.RowId] = housingItem.RowId; + HousingDictPriv[housingItem.Walls.RowId] = housingItem.RowId; + HousingDictPriv[housingItem.Windows.RowId] = housingItem.RowId; + HousingDictPriv[housingItem.Door.RowId] = housingItem.RowId; + HousingDictPriv[housingItem.Fence.RowId] = housingItem.RowId; + HousingDictPriv[housingItem.OptionalRoof.RowId] = housingItem.RowId; + HousingDictPriv[housingItem.OptionalWall.RowId] = housingItem.RowId; + HousingDictPriv[housingItem.OptionalSignboard.RowId] = housingItem.RowId; + } + ItemDict = dataManager.GetExcelSheet() + .Where(item => item.AdditionalData.RowId != 0 && item.ItemSearchCategory.RowId is 65 or 66) + .ToDictionary(row => row.AdditionalData.RowId, row => row); + } + + public void Dispose() + { + logger.LogDebug("Disposing StainHooks"); + } +} diff --git a/Meddle/Meddle.Plugin/Services/UI/CommonUI.cs b/Meddle/Meddle.Plugin/Services/UI/CommonUI.cs index 41d6f29..ecb5dac 100644 --- a/Meddle/Meddle.Plugin/Services/UI/CommonUI.cs +++ b/Meddle/Meddle.Plugin/Services/UI/CommonUI.cs @@ -192,8 +192,13 @@ public unsafe void DrawCharacterSelect(ref ICharacter? selectedCharacter, Object public unsafe string GetCharacterDisplayText(IGameObject obj, bool includeDistance, bool includeId) { + if (!obj.IsValid()) + { + return $"Invalid Object"; + } + string suffix = includeId - ? $"##{obj.GameObjectId}" + ? $"##{obj.Address}" : string.Empty; var drawObject = ((GameObject*)obj.Address)->DrawObject; diff --git a/Meddle/Meddle.Plugin/UI/DebugTab.cs b/Meddle/Meddle.Plugin/UI/DebugTab.cs index 32d9b1c..e173aaa 100644 --- a/Meddle/Meddle.Plugin/UI/DebugTab.cs +++ b/Meddle/Meddle.Plugin/UI/DebugTab.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; -using System.Numerics; +using System.Numerics; using System.Text.Json; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.Textures; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; @@ -42,7 +40,7 @@ public class DebugTab : ITab private readonly PbdHooks pbdHooks; private readonly INotificationManager notificationManager; private readonly SqPack sqPack; - private readonly StainHooks stainHooks; + private readonly StainProvider stainProvider; private readonly IDataManager dataManager; private readonly ComposerFactory composerFactory; private string boneSearch = ""; @@ -70,10 +68,9 @@ public DebugTab(Configuration config, SigUtil sigUtil, CommonUi commonUi, LayoutService layoutService, ParseService parseService, PbdHooks pbdHooks, INotificationManager notificationManager, - TextureCache textureCache, ITextureProvider textureProvider, SqPack sqPack, - StainHooks stainHooks, + StainProvider stainProvider, IDataManager dataManager, ComposerFactory composerFactory) { @@ -86,10 +83,9 @@ public DebugTab(Configuration config, SigUtil sigUtil, CommonUi commonUi, this.parseService = parseService; this.pbdHooks = pbdHooks; this.notificationManager = notificationManager; - this.textureCache = textureCache; this.textureProvider = textureProvider; this.sqPack = sqPack; - this.stainHooks = stainHooks; + this.stainProvider = stainProvider; this.dataManager = dataManager; this.composerFactory = composerFactory; } @@ -430,16 +426,15 @@ public void DrawFileExportUi() ImGui.Image(wrap.Handle, new Vector2(availableWidth, availableWidth * wrap.Height / wrap.Width)); } } - private readonly TextureCache textureCache; private readonly ITextureProvider textureProvider; private void DrawStainInfo() { - foreach (var (key, stain) in stainHooks.StainDict) + foreach (var (key, stain) in StainProvider.StainDict) { using var id = ImRaii.PushId(key.ToString()); ImGui.Text($"Stain: {key}, {stain.Name}"); - var color = StainHooks.GetStainColor(stain); + var color = StainProvider.GetStainColor(stain); ImGui.SameLine(); ImGui.ColorButton("Color", color); } diff --git a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs index eceeee9..0cc5d15 100644 --- a/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs +++ b/Meddle/Meddle.Plugin/UI/LiveCharacterTab.cs @@ -56,7 +56,7 @@ public unsafe class LiveCharacterTab : ITab private readonly Dictionary selectedModels = new(); private readonly TextureCache textureCache; private readonly ResolverService resolverService; - private readonly StainHooks stainHooks; + private readonly StainProvider stainProvider; private readonly ITextureProvider textureProvider; private Task exportTask = Task.CompletedTask; private CancellationTokenSource cancelToken = new(); @@ -72,7 +72,7 @@ public LiveCharacterTab( ParseService parseService, TextureCache textureCache, ResolverService resolverService, - StainHooks stainHooks, + StainProvider stainProvider, SqPack pack, PbdHooks pbd, CommonUi commonUi, @@ -86,7 +86,7 @@ public LiveCharacterTab( this.parseService = parseService; this.textureCache = textureCache; this.resolverService = resolverService; - this.stainHooks = stainHooks; + this.stainProvider = stainProvider; this.pack = pack; this.pbd = pbd; this.commonUi = commonUi; @@ -168,11 +168,10 @@ private void DrawCharacter(CSCharacter* character, string name, int depth = 0) var cBase = (CSCharacterBase*)drawObject; var modelType = cBase->GetModelType(); - Lazy? humanData = null; if (modelType == CSCharacterBase.ModelType.Human) { //humanData = resolverService.ParseHuman(cBase); - humanData = new Lazy(() => resolverService.ParseHuman(cBase)); + var humanData = ParseMaterialUtil.ParseHuman(cBase); DrawHumanCharacter(humanData); ExportButton("Export All Models With Attaches", () => { @@ -186,7 +185,7 @@ private void DrawCharacter(CSCharacter* character, string name, int depth = 0) }, character->NameString.GetCharacterName(config, character->ObjectKind)); } - DrawDrawObject(drawObject, character->NameString.GetCharacterName(config, character->ObjectKind), humanData); + DrawDrawObject(drawObject, character->NameString.GetCharacterName(config, character->ObjectKind)); try { @@ -197,7 +196,7 @@ private void DrawCharacter(CSCharacter* character, string name, int depth = 0) { ImGui.Separator(); ImGui.Text($"Weapon {weaponIdx}"); - DrawDrawObject(weaponData.DrawObject, $"{character->NameString.GetCharacterName(config, character->ObjectKind)}_Weapon", null); + DrawDrawObject(weaponData.DrawObject, $"{character->NameString.GetCharacterName(config, character->ObjectKind)}_Weapon"); } } @@ -325,7 +324,7 @@ private void DrawExportSettings(ParsedCharacterInfo characterInfo, string name, } } - private void DrawDrawObject(DrawObject* drawObject, string name, Lazy? humanData) + private void DrawDrawObject(DrawObject* drawObject, string name) { if (drawObject == null) { @@ -392,11 +391,11 @@ private void DrawDrawObject(DrawObject* drawObject, string name, Lazy cPtr, Pointer mPtr, Lazy? lazyHuman) + private void DrawModel(Pointer cPtr, Pointer mPtr) { if (cPtr == null || cPtr.Value == null) { @@ -502,36 +501,26 @@ private void DrawModel(Pointer cPtr, Pointer mPtr, Lazy< ImGui.Text($"Slot Index: {model->SlotIndex}"); if (modelType == CharacterBase.ModelType.Human) { - var equipmentModelId = ResolverService.GetEquipmentModelId(cBase, (HumanEquipmentSlotIndex)model->SlotIndex); + var equipmentModelId = ParseMaterialUtil.GetEquipmentModelId(cBase, (HumanEquipmentSlotIndex)model->SlotIndex); if (equipmentModelId != null) { ImGui.Text($"Equipment Model Id: {equipmentModelId.Value.Id}"); ImGui.Text($"Equipment Model Variant: {equipmentModelId.Value.Variant}"); } - var stain0 = equipmentModelId != null ? stainHooks.GetStain(equipmentModelId.Value.Stain0) : null; - var stain1 = equipmentModelId != null ? stainHooks.GetStain(equipmentModelId.Value.Stain1) : null; + var stain0 = equipmentModelId != null ? StainProvider.GetStain(equipmentModelId.Value.Stain0) : null; + var stain1 = equipmentModelId != null ? StainProvider.GetStain(equipmentModelId.Value.Stain1) : null; if (stain0 != null) { ImGui.Text($"Stain 0: {stain0.Value.Name.ExtractText()} ({stain0.Value.RowId})"); ImGui.SameLine(); - ImGui.ColorButton("##Stain0", StainHooks.GetStainColor(stain0.Value)); + ImGui.ColorButton("##Stain0", StainProvider.GetStainColor(stain0.Value)); } if (stain1 != null) { ImGui.Text($"Stain 1: {stain1.Value.Name.ExtractText()} ({stain1.Value.RowId})"); ImGui.SameLine(); - ImGui.ColorButton("##Stain1", StainHooks.GetStainColor(stain1.Value)); - } - - if (config.DisplayDebugInfo) - { - var humanData = lazyHuman?.Value; - var skinSlotMaterial = humanData?.SkinSlotMaterials.ElementAtOrDefault((int)model->SlotIndex); - if (skinSlotMaterial != null) - { - ImGui.Text($"Skin Slot Material: {skinSlotMaterial.Path.GamePath}"); - } + ImGui.ColorButton("##Stain1", StainProvider.GetStainColor(stain1.Value)); } } @@ -829,7 +818,7 @@ private void DrawMaterial(Pointer mtPtr, int materialIdx, Func lazyHuman) + private void DrawHumanCharacter(ParsedHumanInfo humanData) { if (ImGui.CollapsingHeader("Customize Options")) { - var humanData = lazyHuman.Value; var width = ImGui.GetContentRegionAvail().X; using var table = ImRaii.Table("##CustomizeTable", 2, ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable); ImGui.TableSetupColumn("Params", ImGuiTableColumnFlags.WidthFixed, width * 0.75f); diff --git a/Meddle/Meddle.Plugin/Utils/OnRenderMaterialUtil.cs b/Meddle/Meddle.Plugin/Utils/OnRenderMaterialUtil.cs new file mode 100644 index 0000000..b4789d1 --- /dev/null +++ b/Meddle/Meddle.Plugin/Utils/OnRenderMaterialUtil.cs @@ -0,0 +1,398 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text.Json.Serialization; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; +using Meddle.Plugin.Models; +using Meddle.Utils; +using Meddle.Utils.Helpers; + +namespace Meddle.Plugin.Utils; + +public class OnRenderMaterialOutput +{ + public string? DecalTexturePath { get; set; } + + [JsonIgnore] + public SkTexture? DecalTexture { get; set; } + public Vector4? DecalTextureColor { get; set; } + public List SkinMaterialTextures { get; set; } = new(); +} +public record SkinMaterialTextureInfo(string TexturePath, string TexturePathFromMaterial, uint SamplerFlags, uint TargetSamplerCrc); + +public static unsafe class OnRenderMaterialUtil +{ + const int MaxSlotCount = (int)HumanModelSlotIndex.Hair; + const int MaxSkinSlotCount = (int)HumanModelSlotIndex.Feet; + const int FaceSlot = (int)HumanModelSlotIndex.Face; + const int HairSlotIndex = (int)HumanModelSlotIndex.Hair; + + public static OnRenderMaterialOutput ResolveMonsterOnRenderMaterial(Pointer monster, Pointer model, uint materialIndex) + { + var output = new OnRenderMaterialOutput(); + if (monster.Value->Decal != null) + { + var decalTexturePtr = monster.Value->Decal; + var decalColorCBuffer = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + ApplyTextureAndConstantBuffer(ref output, decalTexturePtr, decalColorCBuffer); + } + else + { + var transparentTexture = GetTransparentTexture(); + var decalColorCBuffer = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + ApplyTextureAndConstantBuffer(ref output, transparentTexture.Value, decalColorCBuffer); + } + + return output; + } + + public static OnRenderMaterialOutput ResolveWeaponOnRenderMaterial(Pointer weapon, Pointer model, uint materialIndex) + { + var output = new OnRenderMaterialOutput(); + var slotIndex = model.Value->SlotIndex; + if (ShouldApplyFreeCompanyCrest(weapon, slotIndex)) + { + ApplyFreeCompanyCrest(ref output, weapon.Value->FreeCompanyCrest); + return output; + } + + if (weapon.Value->Decal != null) + { + var decalTexturePtr = weapon.Value->Decal; + var decalColorCBuffer = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + ApplyTextureAndConstantBuffer(ref output, decalTexturePtr, decalColorCBuffer); + } + else + { + var transparentTexture = GetTransparentTexture(); + var decalColorCBuffer = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + ApplyTextureAndConstantBuffer(ref output, transparentTexture.Value, decalColorCBuffer); + } + + return output; + } + + public static OnRenderMaterialOutput ResolveDemihumanOnRenderMaterial(Pointer demihuman, Pointer model, uint materialIndex) + { + var output = new OnRenderMaterialOutput(); + var slotIndex = model.Value->SlotIndex; + if (ShouldApplyFreeCompanyCrest(demihuman, slotIndex)) + { + ApplyFreeCompanyCrest(ref output, demihuman.Value->FreeCompanyCrest); + return output; + } + + var slotDecal = demihuman.Value->SlotDecals.Length > slotIndex + ? demihuman.Value->SlotDecals[(int)slotIndex] + : null; + if (slotDecal != null) + { + var decalTexturePtr = slotDecal.Value; + var decalColorCBuffer = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + ApplyTextureAndConstantBuffer(ref output, decalTexturePtr, decalColorCBuffer); + } + else + { + var transparentTexture = GetTransparentTexture(); + var decalColorCBuffer = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + ApplyTextureAndConstantBuffer(ref output, transparentTexture.Value, decalColorCBuffer); + } + + return output; + } + + public static OnRenderMaterialOutput ResolveHumanOnRenderMaterial(Pointer human, Pointer model, uint materialIndex) + { + var slotIndex = model.Value->SlotIndex; + var materialAtIndex = model.Value->MaterialsSpan[(int)materialIndex]; + var output = new OnRenderMaterialOutput(); + if (slotIndex < MaxSlotCount) + { + if (IsLegacyBodyDecalMaterial(materialAtIndex)) + { + ApplyLegacyBodyDecal(ref output, human); + } + else if (ShouldApplyFreeCompanyCrest(human, slotIndex)) + { + ApplyFreeCompanyCrest(ref output, human.Value->FreeCompanyCrest); + } + else + { + ApplySlotDecal(ref output, human, slotIndex); + } + + if (slotIndex <= MaxSkinSlotCount) + { + ApplySkinMaterialTextures(ref output, human, materialAtIndex, slotIndex); + } + + return output; + } + + if (slotIndex == FaceSlot) + { + var decal = human.Value->Decal; + if (decal->LoadState <= 7) + { + // decal loaded + var decalColorCBuffer = human.Value->DecalColorTypedCBuffer.TryGetIndex(0); + ApplyTextureAndConstantBuffer(ref output, decal, decalColorCBuffer); + } + else + { + // fallback to transparent + ApplyTextureAndConstantBuffer(ref output, null, null); + } + + return output; + } + + if (slotIndex == HairSlotIndex) + { + // return HandleHairSlot(parameters); + // hair sets some flags on the output but no texture or color is applied + return output; + } + + return output; + } + + private static void ApplySkinMaterialTextures(ref OnRenderMaterialOutput renderMaterialOutput, Pointer human, Pointer materialAtIndex, uint slotIndex) + { + var skinMaterialHandle = human.Value->SlotSkinMaterials.Length > slotIndex + ? human.Value->SlotSkinMaterials[(int)slotIndex] + : null; + + if (skinMaterialHandle == null || skinMaterialHandle.Value == null) + { + return; + } + + var skinMaterial = skinMaterialHandle.Value->Material; + if (skinMaterial == null) + { + return; + } + + /* + unk* skinCBuffer = *(&human->field_B98 + slotIndex); + if (!skinCBuffer) { + return; + } + */ + + var slotShpk = materialAtIndex.Value->MaterialResourceHandle->ShpkName; + if (slotShpk != "characterstockings.shpk") + { + return; + } + + CopySkinMaterialTextures(ref renderMaterialOutput, skinMaterial, materialAtIndex); + ApplyLegacyBodyDecal(ref renderMaterialOutput, human); + } + + private static void CopySkinMaterialTextures(ref OnRenderMaterialOutput renderMaterialOutput, Pointer skinMaterial, Pointer materialAtIndex) + { + var cu = (CharacterUtilityExtension*)CharacterUtility.Instance(); + + var skinMaterialHandle = skinMaterial.Value->MaterialResourceHandle; + if (skinMaterialHandle == null) + { + return; + } + + var displayMaterialTextures = materialAtIndex.Value->MaterialResourceHandle->ShaderPackageResourceHandle->ShaderPackage->TexturesSpan.ToArray(); + for (var i = 0; i < skinMaterial.Value->TexturesSpan.Length; i++) + { + var entry = skinMaterial.Value->TexturesSpan[i]; + var textureId = entry.Id; + uint targetSlot; + if (cu->Field540_Low == textureId) + { + targetSlot = cu->Field548_High; + } + else if (cu->Field540_High == textureId) + { + targetSlot = cu->Field550_Low; + } + else if (cu->Field548_Low == textureId) + { + targetSlot = cu->Field550_High; + } + else + { + continue; // Not a texture we care about + } + + string? path = null; + string? pathFromMaterial = skinMaterialHandle->TextureCount > i ? skinMaterialHandle->TexturePath(i) : null; + if (entry.Texture != null && entry.Texture->Texture != null) + { + path = entry.Texture->FileName.ToString(); + } + + uint samplerFlags = entry.SamplerFlags; // if tex was out of index; dword_14271F638; + path ??= GetTransparentTexture().Value->FileName.ToString(); + pathFromMaterial ??= GetTransparentTexture().Value->FileName.ToString(); + + if (displayMaterialTextures.Any(x => x.Id == targetSlot)) + { + var targetSlotMatch = displayMaterialTextures.First(x => x.Id == targetSlot); + var targetSlotCrc = targetSlotMatch.CRC; + renderMaterialOutput.SkinMaterialTextures.Add(new SkinMaterialTextureInfo(path, pathFromMaterial, samplerFlags, targetSlotCrc)); + } + } + } + + + [StructLayout(LayoutKind.Explicit)] + public struct CharacterUtilityExtension + { + [FieldOffset(0x540)] + public uint Field540_Low; + + [FieldOffset(0x544)] + public uint Field540_High; + + [FieldOffset(0x548)] + public uint Field548_Low; + + [FieldOffset(0x54C)] + public uint Field548_High; + + [FieldOffset(0x550)] + public uint Field550_Low; + + [FieldOffset(0x554)] + public uint Field550_High; + } + + private static void ApplyFreeCompanyCrest(ref OnRenderMaterialOutput renderMaterialOutput, Texture* crestPtr) + { + string? texturePath = null; + if (crestPtr == null) + { + crestPtr = GetTransparentTexture().Value->Texture; + texturePath = GetTransparentTexture().Value->FileName.ToString(); + } + + // need to resolve the actual crest here since it's computed at runtime + var crestResource = DxHelper.ExportTextureResource(crestPtr); + + var crestColorCBuffer = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + renderMaterialOutput.DecalTexture = crestResource.Resource.ToTexture(); + renderMaterialOutput.DecalTextureColor = crestColorCBuffer; + renderMaterialOutput.DecalTexturePath = texturePath; // crest has no path, keeping it null. + } + + private static Pointer GetTransparentTexture() + { + var characterUtility = CharacterUtility.Instance(); + var handle = characterUtility->ResourceHandles[79]; + return (TextureResourceHandle*)handle.Value; + } + + private static void ApplyLegacyBodyDecal(ref OnRenderMaterialOutput renderMaterialOutput, Pointer human) + { + var decalTexturePtr = human.Value->LegacyBodyDecal; + var decalColorCBuffer = CharacterUtility.Instance()->LegacyBodyDecalColorTypedCBuffer.TryGetIndex(0); + ApplyTextureAndConstantBuffer(ref renderMaterialOutput, decalTexturePtr, decalColorCBuffer); + } + + private static Vector4? TryGetIndex(this ConstantBufferPointer buffer, uint index) + { + var buf = buffer.TryGetBuffer(); + if (buf.Length > (int)index) + { + return buf[(int)index]; + } + + return null; + } + + private static void ApplySlotDecal(ref OnRenderMaterialOutput renderMaterialOutput, Pointer human, uint slotIndex) + { + var slotDecal = human.Value->SlotDecals.Length > slotIndex + ? human.Value->SlotDecals[(int)slotIndex] + : null; + + TextureResourceHandle* decalTexture; + Vector4? decalColor = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + if (slotDecal != null) + { + decalTexture = slotDecal.Value; + } + else + { + decalTexture = GetTransparentTexture().Value; + } + + ApplyTextureAndConstantBuffer(ref renderMaterialOutput, decalTexture, decalColor); + } + + private static void ApplyTextureAndConstantBuffer(ref OnRenderMaterialOutput renderMaterialOutput, TextureResourceHandle* textureResourceHandle, Vector4? buffer) + { + if (textureResourceHandle == null) + { + renderMaterialOutput.DecalTexturePath = GetTransparentTexture().Value->FileName.ToString(); + } + else + { + renderMaterialOutput.DecalTexturePath = textureResourceHandle->FileName.ToString(); + } + + if (buffer == null) + { + renderMaterialOutput.DecalTextureColor = CharacterUtility.Instance()->FreeCompanyCrestColorTypedCBuffer.TryGetIndex(0); + } + else + { + renderMaterialOutput.DecalTextureColor = buffer.Value; + } + } + + private static bool IsLegacyBodyDecalMaterial(Pointer materialPtr) + { + if (materialPtr == null || materialPtr.Value == null) + { + return false; + } + + var material = materialPtr.Value; + var materialPath = material->MaterialResourceHandle->ShpkName.ToString(); + if (materialPath == "skin.shpk") + { + return true; + } + + return false; + } + + private static bool ShouldApplyFreeCompanyCrest(Pointer ptr, uint slotIdx) + { + return ShouldApplyFreeCompanyCrest(ptr.Value->FreeCompanyCrest, slotIdx, ptr.Value->SlotFreeCompanyCrestBitfield); + } + + private static bool ShouldApplyFreeCompanyCrest(Pointer ptr, uint slotIdx) + { + return ShouldApplyFreeCompanyCrest(ptr.Value->FreeCompanyCrest, slotIdx, ptr.Value->SlotFreeCompanyCrestBitfield); + } + + private static bool ShouldApplyFreeCompanyCrest(Pointer ptr, uint slotIdx) + { + return ShouldApplyFreeCompanyCrest(ptr.Value->FreeCompanyCrest, slotIdx, ptr.Value->SlotFreeCompanyCrestBitfield); + } + + private static bool ShouldApplyFreeCompanyCrest(Texture* crestPtr, uint slotIdx, uint slotBitfield) + { + if (crestPtr == null) + { + return false; + } + + return (slotBitfield >> (int)slotIdx & 1) != 0; + } +} diff --git a/Meddle/Meddle.Plugin/Utils/ParseMaterialUtil.cs b/Meddle/Meddle.Plugin/Utils/ParseMaterialUtil.cs new file mode 100644 index 0000000..478ef1d --- /dev/null +++ b/Meddle/Meddle.Plugin/Utils/ParseMaterialUtil.cs @@ -0,0 +1,336 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Interop; +using Lumina.Excel.Sheets; +using Meddle.Plugin.Models; +using Meddle.Plugin.Models.Layout; +using Meddle.Plugin.Models.Structs; +using Meddle.Plugin.Services; +using Meddle.Utils; +using Meddle.Utils.Constants; +using Meddle.Utils.Files; +using Meddle.Utils.Files.Structs.Material; +using Meddle.Utils.Helpers; +using CustomizeData = Meddle.Utils.Export.CustomizeData; + +namespace Meddle.Plugin.Utils; + + +public record struct ParsedHumanInfo +{ + public Meddle.Utils.Export.CustomizeParameter? CustomizeParameter; + public CustomizeData? CustomizeData; + public GenderRace GenderRace; + public IReadOnlyList EquipmentModelIds; +} + +public static class ParseMaterialUtil +{ + public static unsafe ParsedCharacterInfo? ParseDrawObject(Pointer drawObjectPtr, PbdHooks pbdHooks) + { + if (drawObjectPtr == null || drawObjectPtr.Value == null) + { + return null; + } + + var drawObject = drawObjectPtr.Value; + var objectType = drawObject->Object.GetObjectType(); + if (objectType != ObjectType.CharacterBase) + { + return null; + } + + var characterBase = (CharacterBase*)drawObject; + var modelType = characterBase->GetModelType(); + var colorTableSets = ParseColorTableTextures(characterBase); + + var models = new List(); + foreach (var modelPtr in characterBase->ModelsSpan) + { + if (modelPtr == null || modelPtr.Value == null) + { + continue; + } + + var model = modelPtr.Value; + var materials = new List(); + + Stain? stain0 = null; + Stain? stain1 = null; + if (modelType == CharacterBase.ModelType.Human) + { + var equipId = GetEquipmentModelId(characterBase, (HumanEquipmentSlotIndex)model->SlotIndex); + stain0 = equipId != null ? StainProvider.GetStain(equipId.Value.Stain0) : null; + stain1 = equipId != null ? StainProvider.GetStain(equipId.Value.Stain1) : null; + } + + for (int materialIndex = 0; materialIndex < model->MaterialsSpan.Length; materialIndex++) + { + var materialPtr = model->MaterialsSpan[materialIndex]; + if (materialPtr == null || materialPtr.Value == null) + { + continue; + } + + var material = materialPtr.Value; + var onRenderMaterialOutput = modelType switch + { + CharacterBase.ModelType.Human => OnRenderMaterialUtil.ResolveHumanOnRenderMaterial((Human*)characterBase, model, (uint)materialIndex), + CharacterBase.ModelType.DemiHuman => OnRenderMaterialUtil.ResolveDemihumanOnRenderMaterial((Demihuman*)characterBase, model, (uint)materialIndex), + CharacterBase.ModelType.Monster => OnRenderMaterialUtil.ResolveMonsterOnRenderMaterial((Monster*)characterBase, model, (uint)materialIndex), + CharacterBase.ModelType.Weapon => OnRenderMaterialUtil.ResolveWeaponOnRenderMaterial((Weapon*)characterBase, model, (uint)materialIndex), + _ => throw new NotImplementedException($"OnRenderMaterialUtil not implemented for model type {modelType}") + }; + + var materialPath = material->MaterialResourceHandle->FileName.ParseString(); + var shaderName = material->MaterialResourceHandle->ShpkName.ToString(); + string? materialPathFromModel; + if (model != null) + { + materialPathFromModel = modelPtr.Value->ModelResourceHandle->GetMaterialFileNameBySlot((uint)materialIndex); + } + else + { + materialPathFromModel = materialPath; + } + + var colorTableSet = GetColorTableSet(modelPtr, materialPtr, (uint)materialIndex, colorTableSets); + var textures = new List(); + for (var texIdx = 0; texIdx < material->MaterialResourceHandle->TexturesSpan.Length; texIdx++) + { + var texturePtr = material->MaterialResourceHandle->TexturesSpan[texIdx]; + if (texturePtr.TextureResourceHandle == null) continue; + + var texturePath = texturePtr.TextureResourceHandle->FileName.ParseString(); + if (texIdx < material->TextureCount) + { + var texturePathFromMaterial = material->MaterialResourceHandle->TexturePath(texIdx); + var (resource, _) = DxHelper.ExportTextureResource(texturePtr.TextureResourceHandle->Texture); + var textureInfo = new ParsedTextureInfo(texturePath, texturePathFromMaterial, resource); + textures.Add(textureInfo); + } + } + + var materialInfo = new ParsedMaterialInfo( + materialPath, + materialPathFromModel, + shaderName, + onRenderMaterialOutput, + colorTableSet, + textures.ToArray()) + { + Stain0 = stain0, + Stain1 = stain1 + }; + materials.Add(materialInfo); + } + + var deform = modelType == CharacterBase.ModelType.Human ? pbdHooks.TryGetDeformer((nint)characterBase, model->SlotIndex) : null; + var modelInfo = new ParsedModelInfo( + model->ModelResourceHandle->FileName.ParseString(), + characterBase->ResolveMdlPath(model->SlotIndex), + deform, + StructExtensions.ParseModelShapeAttributes(model), + materials.ToArray(), + stain0, stain1) + { + ModelAddress = (nint)modelPtr.Value + }; + models.Add(modelInfo); + } + + var skeleton = StructExtensions.GetParsedSkeleton(characterBase); + var parsedHumanInfo = ParseHuman(characterBase); + return new ParsedCharacterInfo(models.ToArray(), skeleton, StructExtensions.GetParsedAttach(characterBase), parsedHumanInfo); + } + + public static unsafe ParsedHumanInfo ParseHuman(Pointer characterBasePtr) + { + var characterBase = characterBasePtr.Value; + var modelType = characterBase->GetModelType(); + if (modelType != CharacterBase.ModelType.Human) + { + return new ParsedHumanInfo + { + CustomizeParameter = null, + CustomizeData = null, + GenderRace = GenderRace.Unknown, + EquipmentModelIds = [] + }; + } + + var human = (Human*)characterBase; + var customizeCBuf = human->CustomizeParameterCBuffer->TryGetBuffer()[0]; + var decalCol = human->DecalColorCBuffer->TryGetBuffer()[0]; + var customizeParams = new Meddle.Utils.Export.CustomizeParameter + { + SkinColor = customizeCBuf.SkinColor, + MuscleTone = customizeCBuf.MuscleTone, + SkinFresnelValue0 = customizeCBuf.SkinFresnelValue0, + LipColor = customizeCBuf.LipColor, + MainColor = customizeCBuf.MainColor, + FacePaintUvMultiplier = customizeCBuf.FacePaintUVMultiplier, + HairFresnelValue0 = customizeCBuf.HairFresnelValue0, + MeshColor = customizeCBuf.MeshColor, + FacePaintUvOffset = customizeCBuf.FacePaintUVOffset, + LeftColor = customizeCBuf.LeftColor, + RightColor = customizeCBuf.RightColor, + OptionColor = customizeCBuf.OptionColor, + DecalColor = decalCol + }; + var customizeData = new CustomizeData + { + LipStick = human->Customize.Lipstick, + Highlights = human->Customize.Highlights, + FacePaintReversed = human->Customize.FacePaintReversed, + }; + var genderRace = (GenderRace)human->RaceSexId; + var equipData = new List(); + for (var slotIdx = 0; slotIdx <= (int)HumanEquipmentSlotIndex.Extra; slotIdx++) + { + equipData.Add(GetEquipmentModelId(characterBase, (HumanEquipmentSlotIndex)slotIdx)!.Value); + } + + return new ParsedHumanInfo + { + CustomizeParameter = customizeParams, + CustomizeData = customizeData, + GenderRace = genderRace, + EquipmentModelIds = equipData.ToArray() + }; + } + + private static unsafe IColorTableSet? GetColorTableSet(Pointer modelPtr, Pointer materialPtr, uint materialIndex, Dictionary colorTableSets) + { + IColorTableSet? colorTable = null; + if (materialPtr == null || materialPtr.Value == null) + { + return null; + } + + if (modelPtr == null || modelPtr.Value == null) + { + return null; + } + + var model = modelPtr.Value; + var material = materialPtr.Value; + if (colorTableSets.TryGetValue((int)(modelPtr.Value->SlotIndex * CharacterBase.MaterialsPerSlot) + (int)materialIndex, out var gpuColorTable)) + { + colorTable = gpuColorTable; + } + else if (material->MaterialResourceHandle->HasColorTable) + { + var colorTableSpan = material->MaterialResourceHandle->ColorTableSpan; + if (colorTableSpan.Length == ColorTable.Size) + { + var reader = new SpanBinaryReader(MemoryMarshal.AsBytes(colorTableSpan)); + colorTable = new ColorTableSet + { + ColorTable = new ColorTable(ref reader) + }; + } + else if (colorTableSpan.Length == LegacyColorTable.Size) + { + var reader = new SpanBinaryReader(MemoryMarshal.AsBytes(colorTableSpan)); + colorTable = new LegacyColorTableSet + { + ColorTable = new LegacyColorTable(ref reader) + }; + } + } + + return colorTable; + } + + /// + /// Parses the color table textures from the character base. + /// Must be called from the main thread. + /// + public static unsafe Dictionary ParseColorTableTextures(Pointer characterBasePtr) + { + if (characterBasePtr == null || characterBasePtr.Value == null) + { + return new Dictionary(); + } + var characterBase = characterBasePtr.Value; + var colorTableTextures = new Dictionary(); + for (var i = 0; i < characterBase->ColorTableTexturesSpan.Length; i++) + { + var colorTableTex = characterBase->ColorTableTexturesSpan[i]; + if (colorTableTex == null) continue; + + var colorTableTexture = colorTableTex.Value; + if (colorTableTexture != null) + { + var colorTableSet = ParseColorTableTexture(colorTableTexture); + colorTableTextures[i] = colorTableSet; + } + } + + return colorTableTextures; + } + + public static unsafe IColorTableSet ParseColorTableTexture(Texture* colorTableTexture) + { + var (colorTableRes, stride) = DxHelper.ExportTextureResource(colorTableTexture); + if ((TexFile.TextureFormat)colorTableTexture->TextureFormat != TexFile.TextureFormat.R16G16B16A16F) + { + throw new ArgumentException( + $"Color table is not R16G16B16A16F ({(TexFile.TextureFormat)colorTableTexture->TextureFormat})"); + } + + if (colorTableTexture->ActualWidth == 4 && colorTableTexture->ActualHeight == 16) + { + // legacy table + var stridedData = ImageUtils.AdjustStride((int)stride, (int)colorTableTexture->ActualWidth * 8, + (int)colorTableTexture->ActualHeight, colorTableRes.Data); + var reader = new SpanBinaryReader(stridedData); + return new LegacyColorTableSet + { + ColorTable = new LegacyColorTable(ref reader) + }; + } + + if (colorTableTexture->ActualWidth == 8 && colorTableTexture->ActualHeight == 32) + { + // new table + var stridedData = ImageUtils.AdjustStride((int)stride, (int)colorTableTexture->ActualWidth * 8, + (int)colorTableTexture->ActualHeight, colorTableRes.Data); + var reader = new SpanBinaryReader(stridedData); + return new ColorTableSet + { + ColorTable = new ColorTable(ref reader) + }; + } + + throw new ArgumentException( + $"Color table is not 4x16 or 8x32 ({colorTableTexture->ActualWidth}x{colorTableTexture->ActualHeight})"); + } + + public static unsafe EquipmentModelId? GetEquipmentModelId(Pointer characterBasePtr, HumanEquipmentSlotIndex slotIdx) + { + if (characterBasePtr == null || characterBasePtr.Value == null) + { + return null; + } + + var characterBase = characterBasePtr.Value; + if (!Enum.IsDefined(slotIdx)) + { + return null; + } + + if (characterBase->GetModelType() != CharacterBase.ModelType.Human) + { + return null; + } + var human = (Human*)characterBase; + var equipId = (&human->Head)[(int)slotIdx]; + return equipId; + } +} diff --git a/Meddle/Meddle.Plugin/Services/SigUtil.cs b/Meddle/Meddle.Plugin/Utils/SigUtil.cs similarity index 98% rename from Meddle/Meddle.Plugin/Services/SigUtil.cs rename to Meddle/Meddle.Plugin/Utils/SigUtil.cs index e56b956..09e16cb 100644 --- a/Meddle/Meddle.Plugin/Services/SigUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/SigUtil.cs @@ -7,12 +7,13 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.LayoutEngine; +using Meddle.Plugin.Services; using Microsoft.Extensions.Logging; using Camera = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Camera; using CameraManager = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CameraManager; using World = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.World; -namespace Meddle.Plugin.Services; +namespace Meddle.Plugin.Utils; public unsafe class SigUtil : IService, IDisposable { diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index 207bc56..34ef344 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -266,8 +266,6 @@ public static void DrawCustomizeData(CustomizeData? customize) ImGui.Text($"Lipstick: {customize.LipStick}"); ImGui.Text($"Highlights: {customize.Highlights}"); ImGui.Text($"FacePaintReversed: {customize.FacePaintReversed}"); - UiUtil.Text($"Decal Path: {customize.DecalPath ?? "None"}", customize.DecalPath); - UiUtil.Text($"Legacy Body Decal Path: {customize.LegacyBodyDecalPath ?? "None"}", customize.LegacyBodyDecalPath); } public static void DrawColorTable(IColorTableSet table) diff --git a/Meddle/Meddle.Utils/Export/CustomizeParameter.cs b/Meddle/Meddle.Utils/Export/CustomizeParameter.cs index 5c604c5..2887fa6 100644 --- a/Meddle/Meddle.Utils/Export/CustomizeParameter.cs +++ b/Meddle/Meddle.Utils/Export/CustomizeParameter.cs @@ -6,8 +6,6 @@ public class CustomizeData { public bool LipStick; public bool Highlights; - public string? DecalPath; - public string? LegacyBodyDecalPath; public bool FacePaintReversed; } diff --git a/Meddle/Meddle.Utils/Export/ShaderPackage.cs b/Meddle/Meddle.Utils/Export/ShaderPackage.cs index 281ed74..535a92c 100644 --- a/Meddle/Meddle.Utils/Export/ShaderPackage.cs +++ b/Meddle/Meddle.Utils/Export/ShaderPackage.cs @@ -1,5 +1,4 @@ using System.Text; -using Meddle.Utils.Constants; using Meddle.Utils.Files; namespace Meddle.Utils.Export; @@ -9,6 +8,7 @@ public class ShaderPackage public string Name { get; } public Dictionary MaterialConstants { get; } public Dictionary ResourceKeys { get; } + public Dictionary Textures { get; } public Dictionary DefaultKeyValues { get; } public ShaderPackage(ShpkFile file, string name) @@ -17,6 +17,7 @@ public ShaderPackage(ShpkFile file, string name) var resourceKeys = new Dictionary(); var defaultKeyValues = new Dictionary(); + var textures = new Dictionary(); var stringReader = new SpanBinaryReader(file.RawData[(int)file.FileHeader.StringsOffset..]); foreach (var sampler in file.Samplers) { @@ -38,9 +39,10 @@ public ShaderPackage(ShpkFile file, string name) foreach (var texture in file.Textures) { + var resName = stringReader.ReadString((int)texture.StringOffset); + textures[texture.Id] = resName; if (texture.Slot != 2) continue; - var resName = stringReader.ReadString((int)texture.StringOffset); resourceKeys[texture.Id] = resName; } @@ -88,6 +90,7 @@ public ShaderPackage(ShpkFile file, string name) DefaultKeyValues = defaultKeyValues; MaterialConstants = materialConstantDict; ResourceKeys = resourceKeys; + Textures = textures; } } diff --git a/Meddle/Meddle.Utils/Files/MtrlFile.cs b/Meddle/Meddle.Utils/Files/MtrlFile.cs index 448c3b0..4ddd11b 100644 --- a/Meddle/Meddle.Utils/Files/MtrlFile.cs +++ b/Meddle/Meddle.Utils/Files/MtrlFile.cs @@ -30,6 +30,8 @@ public MtrlFile(ReadOnlySpan data) this.data = data.ToArray(); var reader = new SpanBinaryReader(data); FileHeader = reader.Read(); + if (FileHeader.Version != MtrlMagic) + throw new InvalidDataException($"Invalid MTRL magic: {FileHeader.Version:X}"); TextureOffsets = new TextureOffset[FileHeader.TextureCount]; var offsets = reader.Read(FileHeader.TextureCount); From b3a122bc9787956bb90f8dfa4fed8d1fa570f29e Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:42:07 +1100 Subject: [PATCH 04/13] Use new field --- Meddle/Meddle.Plugin/Services/LayoutService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meddle/Meddle.Plugin/Services/LayoutService.cs b/Meddle/Meddle.Plugin/Services/LayoutService.cs index e831716..a34e3cd 100644 --- a/Meddle/Meddle.Plugin/Services/LayoutService.cs +++ b/Meddle/Meddle.Plugin/Services/LayoutService.cs @@ -735,7 +735,7 @@ bool IsSgbHandleValid(SharedGroupResourceHandle* handle) { var index = item.Index; if (item.Index == -1) continue; - var objectPtr = objectManager->Objects[index]; + var objectPtr = objectManager->ObjectArray.Objects[index]; if (objectPtr == null || objectPtr.Value == null || objectPtr.Value->SharedGroupLayoutInstance == null) { continue; From 6fffd3885721341c736b56a47b115b7b4e18f3ee Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:42:26 +1100 Subject: [PATCH 05/13] Include ParsedAt field on char blob --- .../Meddle.Plugin/Models/Layout/ParsedInstance.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs index 70fa6a7..d1815ab 100644 --- a/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs +++ b/Meddle/Meddle.Plugin/Models/Layout/ParsedInstance.cs @@ -517,12 +517,13 @@ public record ParsedCharacterInfo public IReadOnlyList Models; public readonly ParsedSkeleton Skeleton; public readonly ParsedAttach Attach; - private readonly ParsedHumanInfo humanInfo; + public readonly ParsedHumanInfo HumanInfo; public IReadOnlyList Attaches = []; - public CustomizeData? CustomizeData => humanInfo.CustomizeData; - public CustomizeParameter? CustomizeParameter => humanInfo.CustomizeParameter; - public IReadOnlyList EquipmentModelIds => humanInfo.EquipmentModelIds; - public GenderRace GenderRace => humanInfo.GenderRace; + public readonly DateTime ParsedAt = DateTime.UtcNow; + public CustomizeData? CustomizeData => HumanInfo.CustomizeData; + public CustomizeParameter? CustomizeParameter => HumanInfo.CustomizeParameter; + public IReadOnlyList EquipmentModelIds => HumanInfo.EquipmentModelIds; + public GenderRace GenderRace => HumanInfo.GenderRace; public ParsedCharacterInfo( ParsedModelInfo[] models, @@ -533,7 +534,7 @@ public ParsedCharacterInfo( Models = models; Skeleton = skeleton; Attach = attach; - this.humanInfo = humanInfo; + this.HumanInfo = humanInfo; } } From 46f85577cd377bb2db8e47a96712a09be8f7af71 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 12 Nov 2025 18:00:22 +1100 Subject: [PATCH 06/13] Update ModelUtils.cs --- Meddle/Meddle.Utils/Helpers/ModelUtils.cs | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Meddle/Meddle.Utils/Helpers/ModelUtils.cs b/Meddle/Meddle.Utils/Helpers/ModelUtils.cs index b1f31a6..8bfe159 100644 --- a/Meddle/Meddle.Utils/Helpers/ModelUtils.cs +++ b/Meddle/Meddle.Utils/Helpers/ModelUtils.cs @@ -51,6 +51,34 @@ public static Dictionary GetMaterialNames(this MdlFile file) return names; } + public static Dictionary GetAttributeNames(this MdlFile file) + { + var strings = file.GetStrings(); + var names = new Dictionary(); + for (var i = 0; i < file.AttributeNameOffsets.Length; i++) + { + var offset = (int)file.AttributeNameOffsets[i]; + var name = strings[offset]; + names[offset] = name; + } + + return names; + } + + public static Dictionary GetShapeNames(this MdlFile file) + { + var strings = file.GetStrings(); + var names = new Dictionary(); + for (var i = 0; i < file.Shapes.Length; i++) + { + var shape = file.Shapes[i]; + var name = strings[(int)shape.StringOffset]; + names[(int)shape.StringOffset] = name; + } + + return names; + } + public static Dictionary GetStrings(this MdlFile file) { var strings = new Dictionary(); From 8706903d5b8d82c0fbff4c047adf751f8878d5f0 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 12 Nov 2025 19:05:32 +1100 Subject: [PATCH 07/13] Fix memory safety on tex composition --- Meddle/Meddle.Utils/Helpers/ImageUtils.cs | 70 ++++++++++------------- Meddle/Meddle.Utils/SKTexture.cs | 48 +--------------- 2 files changed, 32 insertions(+), 86 deletions(-) diff --git a/Meddle/Meddle.Utils/Helpers/ImageUtils.cs b/Meddle/Meddle.Utils/Helpers/ImageUtils.cs index 8b54b4f..a2d024b 100644 --- a/Meddle/Meddle.Utils/Helpers/ImageUtils.cs +++ b/Meddle/Meddle.Utils/Helpers/ImageUtils.cs @@ -20,7 +20,7 @@ public static class ImageUtils // _ => width * 4, // }; // } - + public static TextureResource ToResource(this TexFile file) { var h = file.Header; @@ -28,16 +28,16 @@ public static TextureResource ToResource(this TexFile file) if (h.Type.HasFlag(TexFile.Attribute.TextureTypeCube)) flags |= D3DResourceMiscFlags.TextureCube; return new TextureResource( - TexFile.GetDxgiFormatFromTextureFormat(h.Format), - h.Width, - h.Height, - h.CalculatedMips, - h.CalculatedArraySize, - TexFile.GetTexDimensionFromAttribute(h.Type), - flags, + TexFile.GetDxgiFormatFromTextureFormat(h.Format), + h.Width, + h.Height, + h.CalculatedMips, + h.CalculatedArraySize, + TexFile.GetTexDimensionFromAttribute(h.Type), + flags, file.TextureBuffer); } - + public static ReadOnlySpan ImageAsPng(this Image image) { unsafe @@ -57,7 +57,7 @@ public static ReadOnlySpan ImageAsPng(this Image image) } } } - + // public static TexMeta GetTexMeta(TextureResource resource) // { // var meta = new TexMeta @@ -164,17 +164,17 @@ public static Image GetTexData(TexFile tex, int arrayLevel, int mipLevel, int sl return img; } - + public static byte[] GetRawRgbaData(TexFile tex, int arrayLevel, int mipLevel, int slice) { var img = GetTexData(tex, arrayLevel, mipLevel, slice); if (img.Format != DXGIFormat.R8G8B8A8UNorm) throw new ArgumentException("Image must be in RGBA format.", nameof(tex)); - + // assume RGBA return img.Span.ToArray(); } - + // public static unsafe SkTexture ToTexture(this Image img, Vector2? resize = null) // { // if (img.Format != DXGIFormat.R8G8B8A8UNorm) @@ -208,25 +208,24 @@ public static byte[] GetRawRgbaData(TexFile tex, int arrayLevel, int mipLevel, i // new SKSamplingOptions(SKCubicResampler.Mitchell)); // return new SkTexture(bitmap); // } - + public static SkTexture ToTexture(this TextureResource resource, (int width, int height)? resize = null) { var bitmap = resource.ToBitmap(); - + if (resize != null) { - bitmap = bitmap.Resize(new SKImageInfo(resize.Value.width, resize.Value.height, SKColorType.Rgba8888, SKAlphaType.Unpremul), + bitmap = bitmap.Resize(new SKImageInfo(resize.Value.width, resize.Value.height, SKColorType.Rgba8888, SKAlphaType.Unpremul), new SKSamplingOptions(SKCubicResampler.Mitchell)); } - + return new SkTexture(bitmap); } - + public static unsafe SKBitmap ToBitmap(this TextureResource resource) { var meta = FromResource(resource); var image = ScratchImage.Initialize(meta); - // copy data - ensure destination not too short fixed (byte* data = image.Pixels) { @@ -238,7 +237,7 @@ public static unsafe SKBitmap ToBitmap(this TextureResource resource) "{Length} > {Length2} " + "{Width}x{Height} {Format}\n" + "Trimmed to fit.", - resource.Data.Length, span.Length, resource.Width, + resource.Data.Length, span.Length, resource.Width, resource.Height, resource.Format); } else @@ -246,27 +245,18 @@ public static unsafe SKBitmap ToBitmap(this TextureResource resource) resource.Data.CopyTo(span); } } - - image.GetRGBA(out var rgba); - var bitmap = new SKBitmap(rgba.Meta.Width, rgba.Meta.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul); - bitmap.Erase(new SKColor(0)); - fixed (byte* data = rgba.Pixels) - { - bitmap.InstallPixels(bitmap.Info, (nint)data, rgba.Meta.Width * 4); - } - // bitmap.SetImmutable(); - // - // return bitmap.Copy() ?? throw new InvalidOperationException("Failed to copy bitmap."); - var copy = new SKBitmap(rgba.Meta.Width, rgba.Meta.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul); - var pixelBuf = copy.GetPixelSpan().ToArray(); - bitmap.GetPixelSpan().CopyTo(pixelBuf); - fixed (byte* data = pixelBuf) - { - copy.InstallPixels(copy.Info, (nint)data, copy.Info.RowBytes); - } - - return copy; + image.GetRGBA(out var rgba); + var img = rgba.GetImage(0, 0, 0); + if (img.Format != DXGIFormat.R8G8B8A8UNorm) + throw new ArgumentException("Image must be in RGBA format.", nameof(resource)); + + var bitmap = new SKBitmap(img.Width, img.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul); + var pixels = bitmap.GetPixels(); + var destinationSpan = new Span((void*)pixels, img.Width * img.Height * 4); + img.Span.CopyTo(destinationSpan); + + return bitmap; } public static TexMeta FromResource(TextureResource resource) diff --git a/Meddle/Meddle.Utils/SKTexture.cs b/Meddle/Meddle.Utils/SKTexture.cs index bc5facd..d04d618 100644 --- a/Meddle/Meddle.Utils/SKTexture.cs +++ b/Meddle/Meddle.Utils/SKTexture.cs @@ -43,58 +43,14 @@ public SkTexture(SKBitmap bitmap) : this(bitmap.Width, bitmap.Height) using (var canvas = new SKCanvas(newBitmap)) canvas.DrawBitmap(bitmap, 0, 0); - if (newBitmap.ByteCount != Data.Length) - throw new ArgumentException("Invalid byte count"); - newBitmap.Bytes.CopyTo(Data, 0); - - if (!newBitmap.Bytes.SequenceEqual(Data)) - throw new InvalidOperationException("Invalid cloned data"); + newBitmap.GetPixelSpan().CopyTo(Data); } else { - if (bitmap.ByteCount != Data.Length) - throw new ArgumentException("Invalid byte count"); - bitmap.Bytes.CopyTo(Data, 0); + bitmap.GetPixelSpan().CopyTo(Data); } } - public SkTexture Copy() - { - var ret = new SkTexture(Width, Height); - Data.CopyTo(ret.Data, 0); - return ret; - } - - public SkTexture Resize(int width, int height) - { - var info = new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Unpremul); - var bitmapCopy = Bitmap.Copy(); - var resize = bitmapCopy.Resize(info, new SKSamplingOptions(SKCubicResampler.Mitchell)); - - return new SkTexture(resize); - } - - - public SKColor SampleWrap(Vector2 uv) => SampleWrap(uv.X, uv.Y); - public SKColor SampleWrap(float u, float v) - { - u %= 1; - v %= 1; - if (u < 0) - u += 1; - if (v < 0) - v += 1; - return Sample(u, v); - } - - public SKColor Sample(Vector2 uv) => Sample(uv.X, uv.Y); - public SKColor Sample(float u, float v) - { - var x = (int)(u * Width); - var y = (int)(v * Height); - return this[x, y]; - } - private Span GetPixelData(int x, int y) => Data.AsSpan().Slice((Width * y + x) * 4, 4); From c3ec8d54e44defb454b46d78b858cbb83528e627 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:47:46 +1100 Subject: [PATCH 08/13] Fix triangle winding to match vertex normals Added logic to check and flip triangle winding order if the calculated face normal opposes the vertex normal. --- Meddle/Meddle.Utils/MeshBuilder.cs | 58 +++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index 917ee97..4483d57 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -87,7 +87,16 @@ public IMeshBuilder BuildSubMesh(SubMesh subMesh) var triA = Vertices[indA]; var triB = Vertices[indB]; var triC = Vertices[indC]; - primitive.AddTriangle(triA, triB, triC); + + // Check if winding order matches vertex normals, flip if needed + if (ShouldFlipWinding(Mesh.Vertices[indA], Mesh.Vertices[indB], Mesh.Vertices[indC])) + { + primitive.AddTriangle(triA, triC, triB); + } + else + { + primitive.AddTriangle(triA, triB, triC); + } } return ret; @@ -101,14 +110,53 @@ public IMeshBuilder BuildMesh() for (var triIdx = 0; triIdx < Mesh.Indices.Count; triIdx += 3) { - var triA = Vertices[Mesh.Indices[triIdx + 0]]; - var triB = Vertices[Mesh.Indices[triIdx + 1]]; - var triC = Vertices[Mesh.Indices[triIdx + 2]]; - primitive.AddTriangle(triA, triB, triC); + var indA = Mesh.Indices[triIdx + 0]; + var indB = Mesh.Indices[triIdx + 1]; + var indC = Mesh.Indices[triIdx + 2]; + var triA = Vertices[indA]; + var triB = Vertices[indB]; + var triC = Vertices[indC]; + + // Check if winding order matches vertex normals, flip if needed + if (ShouldFlipWinding(Mesh.Vertices[indA], Mesh.Vertices[indB], Mesh.Vertices[indC])) + { + primitive.AddTriangle(triA, triC, triB); + } + else + { + primitive.AddTriangle(triA, triB, triC); + } } return ret; } + + /// + /// Determines if triangle winding should be flipped based on vertex normals. + /// Returns true if the calculated face normal points opposite to the vertex normals. + /// + private static bool ShouldFlipWinding(Vertex vA, Vertex vB, Vertex vC) + { + // Skip check if we don't have normals or positions + if (vA.Normals == null || vA.Position == null || vB.Position == null || vC.Position == null) + return false; + + var posA = vA.Position.Value; + var posB = vB.Position.Value; + var posC = vC.Position.Value; + + // Calculate face normal from triangle geometry + var edge1 = posB - posA; + var edge2 = posC - posA; + var faceNormal = Vector3.Normalize(Vector3.Cross(edge1, edge2)); + + // Compare with vertex normal + var vertexNormal = Vector3.Normalize(new Vector3(vA.Normals[0].X, vA.Normals[0].Y, vA.Normals[0].Z)); + + // If dot product is negative, normals point in opposite directions + var dot = Vector3.Dot(faceNormal, vertexNormal); + return dot < -0.01f; + } /// Builds shape keys (known as morph targets in glTF). public IReadOnlyList BuildShapes(IReadOnlyList shapes, IMeshBuilder builder, int subMeshStart, int subMeshEnd) From 7326898dae099eef191077133c0eb8caad7cb24c Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:13:55 +1100 Subject: [PATCH 09/13] Test out different vector parsing --- Meddle/Meddle.Utils/Export/Vertex.cs | 76 ++++++++++++++++++------- Meddle/Meddle.Utils/SpanBinaryReader.cs | 8 +++ 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/Meddle/Meddle.Utils/Export/Vertex.cs b/Meddle/Meddle.Utils/Export/Vertex.cs index 0154893..8bf34a1 100644 --- a/Meddle/Meddle.Utils/Export/Vertex.cs +++ b/Meddle/Meddle.Utils/Export/Vertex.cs @@ -8,16 +8,31 @@ public unsafe struct Vertex { public enum VertexType : byte { - Single1 = 0, - Single2 = 1, - Single3 = 2, - Single4 = 3, - UInt = 5, - ByteFloat4 = 8, - Half2 = 13, - Half4 = 14, - UByte8 = 17 + Single1 = 0, // 0x00 + Single2 = 1, // 0x01 + Single3 = 2, // 0x02 + Single4 = 3, // 0x03 + UInt = 5, // 0x05 + ByteFloat4 = 8, // 0x08 + Half2 = 13, // 0x0D + Half4 = 14, // 0x0E + UByte8 = 17 // 0x11 } + + public static int GetVertexTypeSize(VertexType type) => + type switch + { + VertexType.Single1 => 4, + VertexType.Single2 => 8, + VertexType.Single3 => 12, + VertexType.Single4 => 16, + VertexType.UInt => 4, // ubyte4 + VertexType.ByteFloat4 => 4, // ubyte4n + VertexType.Half2 => 4, + VertexType.Half4 => 8, + VertexType.UByte8 => 8, + _ => throw new ArgumentException($"Unsupported vertex type {type}") + }; public enum VertexUsage : byte { @@ -52,19 +67,38 @@ public enum VertexUsage : byte public static Vector3 ReadVector3(ReadOnlySpan buffer, VertexType type) { - fixed (byte* b = buffer) + // fixed (byte* b = buffer) + // { + // var h = (Half*)b; + // var f = (float*)b; + // + // return type switch + // { + // VertexType.Single3 => new Vector3(f[0], f[1], f[2]), + // VertexType.Single4 => new Vector3(f[0], f[1], f[2]), // skip W + // VertexType.Half4 => new Vector3((float)h[0], (float)h[1], (float)h[2]), // skip W + // VertexType.Single1 => new Vector3(f[0], f[0], f[0]), + // _ => throw new ArgumentException($"Unsupported vector3 type {type}") + // }; + // } + + var size = GetVertexTypeSize(type); + Span temp = stackalloc byte[size]; + buffer[..size].CopyTo(temp); + var reader = new SpanBinaryReader(temp); + return type switch { - var h = (Half*)b; - var f = (float*)b; - - return type switch - { - VertexType.Single3 => new Vector3(f[0], f[1], f[2]), - VertexType.Single4 => new Vector3(f[0], f[1], f[2]), // skip W - VertexType.Half4 => new Vector3((float)h[0], (float)h[1], (float)h[2]), // skip W - VertexType.Single1 => new Vector3(f[0], f[0], f[0]), - _ => throw new ArgumentException($"Unsupported vector3 type {type}") - }; + VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + VertexType.Single4 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), // skip W + VertexType.Half4 => new Vector3((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), // skip W + VertexType.Single1 => ReadSingle1(reader), + _ => throw new ArgumentException($"Unsupported vector3 type {type}") + }; + + Vector3 ReadSingle1(SpanBinaryReader reader) + { + var v = reader.ReadSingle(); + return new Vector3(v, v, v); } } diff --git a/Meddle/Meddle.Utils/SpanBinaryReader.cs b/Meddle/Meddle.Utils/SpanBinaryReader.cs index 0778a92..18a7004 100644 --- a/Meddle/Meddle.Utils/SpanBinaryReader.cs +++ b/Meddle/Meddle.Utils/SpanBinaryReader.cs @@ -126,6 +126,14 @@ public ulong ReadUInt64() [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public nint ReadIntPtr() => Read(); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public Single ReadSingle() + => Read(); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public Half ReadHalf() + => Read(); /// /// Create a slice of the reader from to From bb65f14ac69c21488e25802484e01ece3b389a55 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:14:18 +1100 Subject: [PATCH 10/13] Add options for MeshBuilder --- Meddle/Meddle.Utils/MeshBuilder.cs | 33 ++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/Meddle/Meddle.Utils/MeshBuilder.cs b/Meddle/Meddle.Utils/MeshBuilder.cs index 4483d57..acb60d9 100644 --- a/Meddle/Meddle.Utils/MeshBuilder.cs +++ b/Meddle/Meddle.Utils/MeshBuilder.cs @@ -13,6 +13,11 @@ namespace Meddle.Utils; +public record MeshBuilderOptions +{ + public bool EnableWindingFlip { get; init; } +} + public class MeshBuilder { private Mesh Mesh { get; } @@ -24,6 +29,7 @@ public class MeshBuilder public Type SkinningT { get; } public Type VertexBuilderT { get; } public Type MeshBuilderT { get; } + private MeshBuilderOptions Options { get; } private IReadOnlyList Deformers { get; } @@ -33,12 +39,14 @@ public MeshBuilder( Mesh mesh, IReadOnlyList? boneMap, MaterialBuilder materialBuilder, - (GenderRace fromDeform, GenderRace toDeform, RaceDeformer deformer)? raceDeformer + (GenderRace fromDeform, GenderRace toDeform, RaceDeformer deformer)? raceDeformer, + MeshBuilderOptions? options = null ) { BoneMap = boneMap; Mesh = mesh; MaterialBuilder = materialBuilder; + Options = options ?? new MeshBuilderOptions(); GeometryT = GetVertexGeometryType(Mesh.Vertices); MaterialT = GetVertexMaterialType(Mesh); @@ -55,7 +63,7 @@ public MeshBuilder( } else { - Deformers = Array.Empty(); + Deformers = []; } Vertices = BuildVertices(); @@ -135,10 +143,14 @@ public IMeshBuilder BuildMesh() /// Determines if triangle winding should be flipped based on vertex normals. /// Returns true if the calculated face normal points opposite to the vertex normals. /// - private static bool ShouldFlipWinding(Vertex vA, Vertex vB, Vertex vC) + private bool ShouldFlipWinding(Vertex vA, Vertex vB, Vertex vC) { + if (!Options.EnableWindingFlip) + return false; // Skip check if we don't have normals or positions - if (vA.Normals == null || vA.Position == null || vB.Position == null || vC.Position == null) + if (vA.Normals == null || vA.Position == null || + vB.Normals == null || vB.Position == null || + vC.Normals == null || vC.Position == null) return false; var posA = vA.Position.Value; @@ -151,11 +163,16 @@ private static bool ShouldFlipWinding(Vertex vA, Vertex vB, Vertex vC) var faceNormal = Vector3.Normalize(Vector3.Cross(edge1, edge2)); // Compare with vertex normal - var vertexNormal = Vector3.Normalize(new Vector3(vA.Normals[0].X, vA.Normals[0].Y, vA.Normals[0].Z)); + var vnA = Vector3.Normalize(new Vector3(vA.Normals[0].X, vA.Normals[0].Y, vA.Normals[0].Z)); + var vnB = Vector3.Normalize(new Vector3(vB.Normals[0].X, vB.Normals[0].Y, vB.Normals[0].Z)); + var vnC = Vector3.Normalize(new Vector3(vC.Normals[0].X, vC.Normals[0].Y, vC.Normals[0].Z)); - // If dot product is negative, normals point in opposite directions - var dot = Vector3.Dot(faceNormal, vertexNormal); - return dot < -0.01f; + var dotA = Vector3.Dot(faceNormal, vnA); + var dotB = Vector3.Dot(faceNormal, vnB); + var dotC = Vector3.Dot(faceNormal, vnC); + + // If the dot product is negative, the normals point in opposite directions + return dotA < 0 && dotB < 0 && dotC < 0; } /// Builds shape keys (known as morph targets in glTF). From f0081b92a41e7070a58521f29b505fbdb7def6bb Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:21:18 +1100 Subject: [PATCH 11/13] Add winding option to config --- Meddle/Meddle.Plugin/Configuration.cs | 14 ++++++++++- .../Models/Composer/CharacterComposer.cs | 2 +- .../Models/Composer/InstanceComposer.cs | 4 ++-- Meddle/Meddle.Plugin/UI/OptionsTab.cs | 24 +++++++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Meddle/Meddle.Plugin/Configuration.cs b/Meddle/Meddle.Plugin/Configuration.cs index aa67ac3..593e7a4 100644 --- a/Meddle/Meddle.Plugin/Configuration.cs +++ b/Meddle/Meddle.Plugin/Configuration.cs @@ -6,6 +6,7 @@ using Meddle.Plugin.UI.Layout; using Meddle.Plugin.UI.Windows; using Meddle.Plugin.Utils; +using Meddle.Utils; using Microsoft.Extensions.Logging; namespace Meddle.Plugin; @@ -58,6 +59,7 @@ public class ExportConfiguration public bool LimitTerrainExportRange { get; set; } public float TerrainExportDistance { get; set; } = 500f; + public bool EnableWindingFlip { get; set; } // public enum ExportRootAttachHandling // { @@ -81,7 +83,8 @@ public ExportConfiguration Clone() // RootAttachHandling = RootAttachHandling UseDeformer = UseDeformer, LimitTerrainExportRange = LimitTerrainExportRange, - TerrainExportDistance = TerrainExportDistance + TerrainExportDistance = TerrainExportDistance, + EnableWindingFlip = EnableWindingFlip }; } @@ -105,6 +108,15 @@ public void Apply(ExportConfiguration other) UseDeformer = other.UseDeformer; LimitTerrainExportRange = other.LimitTerrainExportRange; TerrainExportDistance = other.TerrainExportDistance; + EnableWindingFlip = other.EnableWindingFlip; + } + + public MeshBuilderOptions CreateMeshBuilderOptions() + { + return new MeshBuilderOptions + { + EnableWindingFlip = EnableWindingFlip + }; } } diff --git a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs index ca26b71..b8093a4 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/CharacterComposer.cs @@ -121,7 +121,7 @@ private void HandleModel(ParsedCharacterInfo characterInfo, ParsedModelInfo m, S } var enabledAttributes = Model.GetEnabledValues(model.EnabledAttributeMask, model.AttributeMasks).ToArray(); - var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, skinningContext.Bones, deform); + var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, skinningContext.Bones, deform, exportConfig.CreateMeshBuilderOptions()); foreach (var mesh in meshes) { var extrasDict = new Dictionary diff --git a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs index 108b790..af20132 100644 --- a/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs +++ b/Meddle/Meddle.Plugin/Models/Composer/InstanceComposer.cs @@ -542,7 +542,7 @@ private NodeBuilder ComposeCameraInstance(ParsedCameraInstance parsedCameraInsta } var model = new Model(bgPartsInstance.Path.GamePath, mdlFile, null); - var meshExports = ModelBuilder.BuildMeshes(model, materialBuilders, [], null); + var meshExports = ModelBuilder.BuildMeshes(model, materialBuilders, [], null, exportConfig.CreateMeshBuilderOptions()); meshes = meshExports.Select(x => x.Mesh).ToArray(); } @@ -652,7 +652,7 @@ public NodeBuilder ComposeTerrain(ParsedTerrainInstance terrainInstance, SceneBu } var model = new Model(mdlPath, mdlFile, null); - var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, [], null); + var meshes = ModelBuilder.BuildMeshes(model, materialBuilders, [], null, exportConfig.CreateMeshBuilderOptions()); var plateRoot = new NodeBuilder(mdlPath); for (var meshIdx = 0; meshIdx < meshes.Count; meshIdx++) diff --git a/Meddle/Meddle.Plugin/UI/OptionsTab.cs b/Meddle/Meddle.Plugin/UI/OptionsTab.cs index 79eefc3..9dce2a4 100644 --- a/Meddle/Meddle.Plugin/UI/OptionsTab.cs +++ b/Meddle/Meddle.Plugin/UI/OptionsTab.cs @@ -174,6 +174,30 @@ public void Draw() config.OpenFolderOnExport = openFolderOnExport; config.Save(); } + + ImGui.Separator(); + ImGui.Text("Export Settings"); + + var enableWindingFlip = config.ExportConfig.EnableWindingFlip; + if (ImGui.Checkbox("Enable Winding Order Flip", ref enableWindingFlip)) + { + config.ExportConfig.EnableWindingFlip = enableWindingFlip; + config.Save(); + } + + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.Text(FontAwesomeIcon.QuestionCircle.ToIconString()); + } + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.Text("Automatically flips triangle winding order when face normals don't match vertex normals."); + ImGui.Text("Enable this if exported models have inverted or incorrect face normals."); + ImGui.EndTooltip(); + } } // private void DrawCharacterTextureMode() From da9de0595e98acb23ca208ceff3f731e96d01f0f Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:21:32 +1100 Subject: [PATCH 12/13] Update ModelBuilder.cs --- Meddle/Meddle.Utils/ModelBuilder.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Meddle/Meddle.Utils/ModelBuilder.cs b/Meddle/Meddle.Utils/ModelBuilder.cs index cc36ef8..061afe7 100644 --- a/Meddle/Meddle.Utils/ModelBuilder.cs +++ b/Meddle/Meddle.Utils/ModelBuilder.cs @@ -20,8 +20,9 @@ public static class ModelBuilder public static IReadOnlyList BuildMeshes( Model model, IReadOnlyList materials, - IReadOnlyList boneMap, - (GenderRace fromDeform, GenderRace toDeform, RaceDeformer deformer)? raceDeformer) + IReadOnlyList? boneMap, + (GenderRace fromDeform, GenderRace toDeform, RaceDeformer deformer)? raceDeformer, + MeshBuilderOptions? options = null) { var meshes = new List(); @@ -60,13 +61,20 @@ public static IReadOnlyList BuildMeshes( material = materials[mesh.MaterialIdx]; } - if (mesh.BoneTable != null) + if (mesh.BoneTable != null && boneMap != null) { - meshBuilder = new MeshBuilder(mesh, boneMap, material, raceDeformer); + meshBuilder = new MeshBuilder(mesh, boneMap, material, raceDeformer, options); + } + else if (mesh.BoneTable != null && boneMap == null) + { + // Global.Logger.LogWarning("[{Path}] Mesh {MeshIdx} has bone table but no bone map was provided", + // model.HandlePath, + // mesh.MeshIdx); + meshBuilder = new MeshBuilder(mesh, null, material, raceDeformer, options); } else { - meshBuilder = new MeshBuilder(mesh, null, material, raceDeformer); + meshBuilder = new MeshBuilder(mesh, null, material, raceDeformer, options); } Global.Logger.LogDebug("[{Path}] Building mesh {MeshIdx}\n{Mesh}", From 9cd4bb91b0a8f84903561210481e9d26e5be567f Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Sat, 15 Nov 2025 23:05:18 +1100 Subject: [PATCH 13/13] Move winding option to export options rather than options tab --- Meddle/Meddle.Plugin/UI/OptionsTab.cs | 24 ------------------------ Meddle/Meddle.Plugin/Utils/UIUtil.cs | 11 +++++++++++ 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/Meddle/Meddle.Plugin/UI/OptionsTab.cs b/Meddle/Meddle.Plugin/UI/OptionsTab.cs index 9dce2a4..79eefc3 100644 --- a/Meddle/Meddle.Plugin/UI/OptionsTab.cs +++ b/Meddle/Meddle.Plugin/UI/OptionsTab.cs @@ -174,30 +174,6 @@ public void Draw() config.OpenFolderOnExport = openFolderOnExport; config.Save(); } - - ImGui.Separator(); - ImGui.Text("Export Settings"); - - var enableWindingFlip = config.ExportConfig.EnableWindingFlip; - if (ImGui.Checkbox("Enable Winding Order Flip", ref enableWindingFlip)) - { - config.ExportConfig.EnableWindingFlip = enableWindingFlip; - config.Save(); - } - - ImGui.SameLine(); - using (ImRaii.PushFont(UiBuilder.IconFont)) - { - ImGui.Text(FontAwesomeIcon.QuestionCircle.ToIconString()); - } - - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.Text("Automatically flips triangle winding order when face normals don't match vertex normals."); - ImGui.Text("Enable this if exported models have inverted or incorrect face normals."); - ImGui.EndTooltip(); - } } // private void DrawCharacterTextureMode() diff --git a/Meddle/Meddle.Plugin/Utils/UIUtil.cs b/Meddle/Meddle.Plugin/Utils/UIUtil.cs index 34ef344..5138d23 100644 --- a/Meddle/Meddle.Plugin/Utils/UIUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/UIUtil.cs @@ -219,6 +219,17 @@ public static bool DrawExportConfig(Configuration.ExportConfiguration exportConf // HintCircle("PlayerAsAttachChild: If a 'Character' has a root attach (typically a mount), export the player as a child of the root attach\n" + // "Exclude: Export the root attach separately from the player"); + var enableWindingFlip = exportConfiguration.EnableWindingFlip; + if (ImGui.Checkbox("Enable Winding Order Flip", ref enableWindingFlip)) + { + exportConfiguration.EnableWindingFlip = enableWindingFlip; + changed = true; + } + + ImGui.SameLine(); + HintCircle("Automatically flips triangle winding order when face normals don't match vertex normals.\n" + + "Enable this if exported models have inverted or incorrect face normals."); + return changed; }