From 1d935def5865ee2c8460d1a5600b0b4bd36adf57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Jun 2022 14:37:05 +0200 Subject: [PATCH 01/22] Fix object reloading in GPose, also add index-based redraw to API/IPC. --- Penumbra/Api/IPenumbraApi.cs | 3 + Penumbra/Api/PenumbraApi.cs | 6 ++ Penumbra/Api/PenumbraIpc.cs | 13 +++ Penumbra/Api/RedrawController.cs | 14 ++- Penumbra/Interop/ObjectReloader.cs | 139 +++++++++++++++++++++++------ 5 files changed, 145 insertions(+), 30 deletions(-) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 14c3b8ea..9714b497 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -34,6 +34,9 @@ public interface IPenumbraApi : IPenumbraApiBase // Queue redrawing of all actors of the given name with the given RedrawType. public void RedrawObject( string name, RedrawType setting ); + // Queue redrawing of the actor with the given object table index, if it exists, with the given RedrawType. + public void RedrawObject( int tableIndex, RedrawType setting ); + // Queue redrawing of the specific actor with the given RedrawType. Should only be used when the actor is sure to be valid. public void RedrawObject( GameObject gameObject, RedrawType setting ); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 9238607c..082e9642 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -71,6 +71,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } + public void RedrawObject( int tableIndex, RedrawType setting ) + { + CheckInitialized(); + _penumbra!.ObjectReloader.RedrawObject( tableIndex, setting ); + } + public void RedrawObject( string name, RedrawType setting ) { CheckInitialized(); diff --git a/Penumbra/Api/PenumbraIpc.cs b/Penumbra/Api/PenumbraIpc.cs index d19d13b8..d8dc6db0 100644 --- a/Penumbra/Api/PenumbraIpc.cs +++ b/Penumbra/Api/PenumbraIpc.cs @@ -112,10 +112,12 @@ public partial class PenumbraIpc public partial class PenumbraIpc { public const string LabelProviderRedrawName = "Penumbra.RedrawObjectByName"; + public const string LabelProviderRedrawIndex = "Penumbra.RedrawObjectByIndex"; public const string LabelProviderRedrawObject = "Penumbra.RedrawObject"; public const string LabelProviderRedrawAll = "Penumbra.RedrawAll"; internal ICallGateProvider< string, int, object >? ProviderRedrawName; + internal ICallGateProvider< int, int, object >? ProviderRedrawIndex; internal ICallGateProvider< GameObject, int, object >? ProviderRedrawObject; internal ICallGateProvider< int, object >? ProviderRedrawAll; @@ -142,6 +144,16 @@ public partial class PenumbraIpc PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); } + try + { + ProviderRedrawIndex = pi.GetIpcProvider( LabelProviderRedrawIndex ); + ProviderRedrawIndex.RegisterAction( ( idx, i ) => Api.RedrawObject( idx, CheckRedrawType( i ) ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error registering IPC provider for {LabelProviderRedrawName}:\n{e}" ); + } + try { ProviderRedrawObject = pi.GetIpcProvider< GameObject, int, object >( LabelProviderRedrawObject ); @@ -166,6 +178,7 @@ public partial class PenumbraIpc private void DisposeRedrawProviders() { ProviderRedrawName?.UnregisterAction(); + ProviderRedrawIndex?.UnregisterAction(); ProviderRedrawObject?.UnregisterAction(); ProviderRedrawAll?.UnregisterAction(); } diff --git a/Penumbra/Api/RedrawController.cs b/Penumbra/Api/RedrawController.cs index f00f1871..df470a1a 100644 --- a/Penumbra/Api/RedrawController.cs +++ b/Penumbra/Api/RedrawController.cs @@ -17,7 +17,18 @@ public class RedrawController : WebApiController public async Task Redraw() { var data = await HttpContext.GetRequestDataAsync< RedrawData >(); - _penumbra.Api.RedrawObject( data.Name, data.Type ); + if( data.ObjectTableIndex >= 0 ) + { + _penumbra.Api.RedrawObject( data.ObjectTableIndex, data.Type ); + } + else if( data.Name.Length > 0 ) + { + _penumbra.Api.RedrawObject( data.Name, data.Type ); + } + else + { + _penumbra.Api.RedrawAll( data.Type ); + } } [Route( HttpVerbs.Post, "/redrawAll" )] @@ -30,5 +41,6 @@ public class RedrawController : WebApiController { public string Name { get; set; } = string.Empty; public RedrawType Type { get; set; } = RedrawType.Redraw; + public int ObjectTableIndex { get; set; } = -1; } } \ No newline at end of file diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index bf89f061..45c64e03 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -8,12 +8,100 @@ using Penumbra.Interop.Structs; namespace Penumbra.Interop; -public sealed unsafe class ObjectReloader : IDisposable +public unsafe partial class ObjectReloader { public const int GPosePlayerIdx = 201; public const int GPoseSlots = 42; public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots; + private readonly string?[] _gPoseNames = new string?[GPoseSlots]; + private int _gPoseNameCounter = 0; + private bool _inGPose = false; + + // VFuncs that disable and enable draw, used only for GPose actors. + private static void DisableDraw( GameObject actor ) + => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]( actor.Address ); + + private static void EnableDraw( GameObject actor ) + => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); + + + // Check whether we currently are in GPose. + // Also clear the name list. + private void SetGPose() + { + _inGPose = Dalamud.Objects[ GPosePlayerIdx ] != null; + _gPoseNameCounter = 0; + } + + private static bool IsGPoseActor( int idx ) + => idx is >= GPosePlayerIdx and < GPoseEndIdx; + + // Return whether an object has to be replaced by a GPose object. + // If the object does not exist, is already a GPose actor + // or no actor of the same name is found in the GPose actor list, + // obj will be the object itself (or null) and false will be returned. + // If we are in GPose and a game object with the same name as the original actor is found, + // this will be in obj and true will be returned. + private bool FindCorrectActor( int idx, out GameObject? obj ) + { + obj = Dalamud.Objects[ idx ]; + if( !_inGPose || obj == null || IsGPoseActor( idx ) ) + { + return false; + } + + var name = obj.Name.ToString(); + for( var i = 0; i < _gPoseNameCounter; ++i ) + { + var gPoseName = _gPoseNames[ i ]; + if( gPoseName == null ) + { + break; + } + + if( name == gPoseName ) + { + obj = Dalamud.Objects[ GPosePlayerIdx + i ]; + return true; + } + } + + for( ; _gPoseNameCounter < GPoseSlots; ++_gPoseNameCounter ) + { + var gPoseName = Dalamud.Objects[ GPosePlayerIdx + _gPoseNameCounter ]?.Name.ToString(); + _gPoseNames[ _gPoseNameCounter ] = gPoseName; + if( gPoseName == null ) + { + break; + } + + if( name == gPoseName ) + { + obj = Dalamud.Objects[ GPosePlayerIdx + _gPoseNameCounter ]; + return true; + } + } + + return obj; + } + + // Do not ever redraw any of the five UI Window actors. + private static bool BadRedrawIndices( GameObject? actor, out int tableIndex ) + { + if( actor == null ) + { + tableIndex = -1; + return true; + } + + tableIndex = ObjectTableIndex( actor ); + return tableIndex is >= 240 and < 245; + } +} + +public sealed unsafe partial class ObjectReloader : IDisposable +{ private readonly List< int > _queue = new(100); private readonly List< int > _afterGPoseQueue = new(GPoseSlots); private int _target = -1; @@ -27,27 +115,9 @@ public sealed unsafe class ObjectReloader : IDisposable public static DrawState* ActorDrawState( GameObject actor ) => ( DrawState* )( actor.Address + 0x0104 ); - private static void DisableDraw( GameObject actor ) - => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 17 ]( actor.Address ); - - private static void EnableDraw( GameObject actor ) - => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ 16 ]( actor.Address ); - private static int ObjectTableIndex( GameObject actor ) => ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->ObjectIndex; - private static bool BadRedrawIndices( GameObject? actor, out int tableIndex ) - { - if( actor == null ) - { - tableIndex = -1; - return true; - } - - tableIndex = ObjectTableIndex( actor ); - return tableIndex is >= 240 and < 245; - } - private static void WriteInvisible( GameObject? actor ) { if( BadRedrawIndices( actor, out var tableIndex ) ) @@ -57,7 +127,7 @@ public sealed unsafe class ObjectReloader : IDisposable *ActorDrawState( actor! ) |= DrawState.Invisibility; - if( tableIndex is >= GPosePlayerIdx and < GPoseEndIdx ) + if( IsGPoseActor( tableIndex ) ) { DisableDraw( actor! ); } @@ -72,7 +142,7 @@ public sealed unsafe class ObjectReloader : IDisposable *ActorDrawState( actor! ) &= ~DrawState.Invisibility; - if( tableIndex is >= GPosePlayerIdx and < GPoseEndIdx ) + if( IsGPoseActor( tableIndex ) ) { EnableDraw( actor! ); } @@ -136,15 +206,22 @@ public sealed unsafe class ObjectReloader : IDisposable for( var i = 0; i < _queue.Count; ++i ) { var idx = _queue[ i ]; - if( idx < 0 ) + if( FindCorrectActor( idx < 0 ? ~idx : idx, out var obj ) ) { - var newIdx = ~idx; - WriteInvisible( Dalamud.Objects[ newIdx ] ); - _queue[ numKept++ ] = newIdx; + _afterGPoseQueue.Add( idx < 0 ? idx : ~idx ); } - else + + if( obj != null ) { - WriteVisible( Dalamud.Objects[ idx ] ); + if( idx < 0 ) + { + WriteInvisible( obj ); + _queue[ numKept++ ] = ObjectTableIndex( obj ); + } + else + { + WriteVisible( obj ); + } } } @@ -153,7 +230,7 @@ public sealed unsafe class ObjectReloader : IDisposable private void HandleAfterGPose() { - if( _afterGPoseQueue.Count == 0 || Dalamud.Objects[ GPosePlayerIdx ] != null ) + if( _afterGPoseQueue.Count == 0 || _inGPose ) { return; } @@ -174,7 +251,7 @@ public sealed unsafe class ObjectReloader : IDisposable } } - _afterGPoseQueue.RemoveRange( numKept, _queue.Count - numKept ); + _afterGPoseQueue.RemoveRange( numKept, _afterGPoseQueue.Count - numKept ); } private void OnUpdateEvent( object framework ) @@ -186,6 +263,7 @@ public sealed unsafe class ObjectReloader : IDisposable return; } + SetGPose(); HandleRedraw(); HandleAfterGPose(); HandleTarget(); @@ -229,6 +307,9 @@ public sealed unsafe class ObjectReloader : IDisposable return ret; } + public void RedrawObject( int tableIndex, RedrawType settings ) + => RedrawObject( Dalamud.Objects[tableIndex], settings ); + public void RedrawObject( string name, RedrawType settings ) { var lowerName = name.ToLowerInvariant(); From c3a3a2cd350648baa86d5746c2b3bdbc9bb6c8f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Jun 2022 15:32:25 +0200 Subject: [PATCH 02/22] Add range check to index redrawing. --- Penumbra/Interop/ObjectReloader.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs index 45c64e03..058fc87c 100644 --- a/Penumbra/Interop/ObjectReloader.cs +++ b/Penumbra/Interop/ObjectReloader.cs @@ -308,7 +308,12 @@ public sealed unsafe partial class ObjectReloader : IDisposable } public void RedrawObject( int tableIndex, RedrawType settings ) - => RedrawObject( Dalamud.Objects[tableIndex], settings ); + { + if( tableIndex >= 0 && tableIndex < Dalamud.Objects.Length ) + { + RedrawObject( Dalamud.Objects[ tableIndex ], settings ); + } + } public void RedrawObject( string name, RedrawType settings ) { From bf58c6b098f2413fac1110800ee8ec0106403b7b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jun 2022 22:11:38 +0200 Subject: [PATCH 03/22] Remove some further Unknown Unknown identifications. --- Penumbra.GameData/GamePathParser.cs | 6 ++++++ Penumbra.GameData/ObjectIdentification.cs | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData/GamePathParser.cs b/Penumbra.GameData/GamePathParser.cs index a054ab39..3a255bdd 100644 --- a/Penumbra.GameData/GamePathParser.cs +++ b/Penumbra.GameData/GamePathParser.cs @@ -44,6 +44,7 @@ internal class GamePathParser : IGamePathParser , { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture/(?'minus'(--)?)(v(?'variant'\d{2})_)?c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?(_[a-z])?_[a-z]\.tex") , new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture") , new(@"chara/common/texture/skin(?'skin'.*)\.tex") + , new(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex") , new(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex") } } } } , { FileType.Model, new Dictionary< ObjectType, Regex[] >() { { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl") } } @@ -223,6 +224,11 @@ internal class GamePathParser : IGamePathParser private static GameObjectInfo HandleCustomization( FileType fileType, GroupCollection groups ) { + if( groups[ "catchlight" ].Success ) + { + return GameObjectInfo.Customization( fileType, CustomizationType.Iris ); + } + if( groups[ "skin" ].Success ) { return GameObjectInfo.Customization( fileType, CustomizationType.Skin ); diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index 16362e37..93e0a52c 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -290,10 +290,16 @@ internal class ObjectIdentification : IObjectIdentifier case CustomizationType.DecalFace: set[ $"Customization: Face Decal {info.PrimaryId}" ] = null; break; + case CustomizationType.Iris when race == ModelRace.Unknown: + set[ $"Customization: All Eyes (Catchlight)" ] = null; + break; default: { - var customizationString = - $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; + var customizationString = race == ModelRace.Unknown + || info.BodySlot == BodySlot.Unknown + || info.CustomizationType == CustomizationType.Unknown + ? "Customization: Unknown" + : $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; set[ customizationString ] = null; break; } From d2eae541491d8c5d30408bd9665d241058f9a55e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jun 2022 22:11:59 +0200 Subject: [PATCH 04/22] Fix disabling a inheritance not removing the mod correctly. --- Penumbra/Collections/ModCollection.Cache.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 925a37d4..9a9e31bd 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -87,13 +87,13 @@ public partial class ModCollection ReloadMod( Penumbra.ModManager[ modIdx ], true ); break; case ModSettingChange.EnableState: - if( oldValue != 1 ) + if( _collection.Settings[ modIdx ]!.Enabled ) { - AddMod( Penumbra.ModManager[ modIdx ], true ); + AddMod( Penumbra.ModManager[modIdx], true ); } else { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); + RemoveMod( Penumbra.ModManager[modIdx], true ); } break; @@ -257,6 +257,7 @@ public partial class ModCollection { AddMetaFiles(); } + if( _collection == Penumbra.CollectionManager.Default ) { Penumbra.ResidentResources.Reload(); From 02f1a4ceddb0fce3c547c811575c6e290f2d554a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jun 2022 22:12:54 +0200 Subject: [PATCH 05/22] Add option to auto-deduplicate on import. --- Penumbra/Configuration.cs | 1 + Penumbra/Import/ImporterState.cs | 1 + Penumbra/Import/TexToolsImport.cs | 8 +- Penumbra/Import/TexToolsImporter.Gui.cs | 21 +++++- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 75 +++++++++++++++++-- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 3 + 6 files changed, 98 insertions(+), 11 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8d120b67..8cb3e746 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -48,6 +48,7 @@ public partial class Configuration : IPluginConfiguration public bool FixMainWindow { get; set; } = false; public bool ShowAdvanced { get; set; } + public bool AutoDeduplicateOnImport { get; set; } = false; public bool DisableSoundStreaming { get; set; } = true; public bool EnableHttpApi { get; set; } diff --git a/Penumbra/Import/ImporterState.cs b/Penumbra/Import/ImporterState.cs index 5a9476e6..8d576f97 100644 --- a/Penumbra/Import/ImporterState.cs +++ b/Penumbra/Import/ImporterState.cs @@ -5,5 +5,6 @@ public enum ImporterState None, WritingPackToDisk, ExtractingModFiles, + DeduplicatingFiles, Done, } \ No newline at end of file diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index f239ff3c..73cb9029 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Dalamud.Logging; using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; -using Penumbra.Util; +using Penumbra.Mods; using FileMode = System.IO.FileMode; namespace Penumbra.Import; @@ -95,6 +94,11 @@ public partial class TexToolsImporter : IDisposable { var directory = VerifyVersionAndImport( file ); ExtractedMods.Add( ( file, directory, null ) ); + if( Penumbra.Config.AutoDeduplicateOnImport ) + { + State = ImporterState.DeduplicatingFiles; + Mod.Editor.DeduplicateMod( directory ); + } } catch( Exception e ) { diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 5ada0f46..6e365e1b 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -38,7 +38,14 @@ public partial class TexToolsImporter var percentage = _modPackCount / ( float )_currentModPackIdx; ImGui.ProgressBar( percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}" ); ImGui.NewLine(); - ImGui.TextUnformatted( $"Extracting {_currentModName}..." ); + if( State == ImporterState.DeduplicatingFiles ) + { + ImGui.TextUnformatted( $"Deduplicating {_currentModName}..." ); + } + else + { + ImGui.TextUnformatted( $"Extracting {_currentModName}..." ); + } if( _currentNumOptions > 1 ) { @@ -47,8 +54,11 @@ public partial class TexToolsImporter percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / ( float )_currentNumOptions; ImGui.ProgressBar( percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}" ); ImGui.NewLine(); - ImGui.TextUnformatted( - $"Extracting option {( _currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - " )}{_currentOptionName}..." ); + if( State != ImporterState.DeduplicatingFiles ) + { + ImGui.TextUnformatted( + $"Extracting option {( _currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - " )}{_currentOptionName}..." ); + } } ImGui.NewLine(); @@ -56,7 +66,10 @@ public partial class TexToolsImporter percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / ( float )_currentNumFiles; ImGui.ProgressBar( percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}" ); ImGui.NewLine(); - ImGui.TextUnformatted( $"Extracting file {_currentFileName}..." ); + if( State != ImporterState.DeduplicatingFiles ) + { + ImGui.TextUnformatted( $"Extracting file {_currentFileName}..." ); + } } } diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index 4722c353..8e5b1e95 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -24,7 +24,7 @@ public partial class Mod public bool DuplicatesFinished { get; private set; } = true; - public void DeleteDuplicates() + public void DeleteDuplicates( bool useModManager = true ) { if( !DuplicatesFinished || _duplicates.Count == 0 ) { @@ -41,15 +41,16 @@ public partial class Mod var remaining = set[ 0 ]; foreach( var duplicate in set.Skip( 1 ) ) { - HandleDuplicate( duplicate, remaining ); + HandleDuplicate( duplicate, remaining, useModManager ); } } _availableFiles.RemoveAll( p => !p.File.Exists ); _duplicates.Clear(); + DeleteEmptyDirectories( _mod.ModPath ); } - private void HandleDuplicate( FullPath duplicate, FullPath remaining ) + private void HandleDuplicate( FullPath duplicate, FullPath remaining, bool useModManager ) { void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx ) { @@ -58,7 +59,23 @@ public partial class Mod kvp => ChangeDuplicatePath( kvp.Value, duplicate, remaining, kvp.Key, ref changes ) ); if( changes ) { - Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict ); + if( useModManager ) + { + Penumbra.ModManager.OptionSetFiles( _mod, groupIdx, optionIdx, dict ); + } + else + { + var sub = ( SubMod )subMod; + sub.FileData = dict; + if( groupIdx == -1 ) + { + _mod.SaveDefaultMod(); + } + else + { + IModGroup.Save( _mod.Groups[ groupIdx ], _mod.ModPath, groupIdx ); + } + } } } @@ -94,7 +111,7 @@ public partial class Mod { DuplicatesFinished = false; UpdateFiles(); - var files = _availableFiles.OrderByDescending(f => f.FileSize).ToArray(); + var files = _availableFiles.OrderByDescending( f => f.FileSize ).ToArray(); Task.Run( () => CheckDuplicates( files ) ); } } @@ -215,5 +232,53 @@ public partial class Mod using var stream = File.OpenRead( f.FullName ); return _hasher.ComputeHash( stream ); } + + // Recursively delete all empty directories starting from the given directory. + // Deletes inner directories first, so that a tree of empty directories is actually deleted. + private void DeleteEmptyDirectories( DirectoryInfo baseDir ) + { + try + { + if( !baseDir.Exists ) + { + return; + } + + foreach( var dir in baseDir.EnumerateDirectories( "*", SearchOption.TopDirectoryOnly ) ) + { + DeleteEmptyDirectories( dir ); + } + + baseDir.Refresh(); + if( !baseDir.EnumerateFileSystemInfos().Any() ) + { + Directory.Delete( baseDir.FullName, false ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete empty directories in {baseDir.FullName}:\n{e}" ); + } + } + + + + // Deduplicate a mod simply by its directory without any confirmation or waiting time. + internal static void DeduplicateMod( DirectoryInfo modDirectory ) + { + try + { + var mod = new Mod( modDirectory ); + mod.Reload( out _ ); + var editor = new Editor( mod, 0, 0 ); + editor.DuplicatesFinished = false; + editor.CheckDuplicates( editor.AvailableFiles.OrderByDescending( f => f.FileSize ).ToArray() ); + editor.DeleteDuplicates( false ); + } + catch( Exception e ) + { + PluginLog.Warning( $"Could not deduplicate mod {modDirectory.Name}:\n{e}" ); + } + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs index d992f8ff..13fcce3e 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs @@ -20,6 +20,9 @@ public partial class ConfigWindow return; } + Checkbox( "Auto Deduplicate on Import", + "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", + Penumbra.Config.AutoDeduplicateOnImport, v => Penumbra.Config.AutoDeduplicateOnImport = v ); DrawRequestedResourceLogging(); DrawDisableSoundStreamingBox(); DrawEnableHttpApiBox(); From 1d3a31db6ffb42d4190d5e371df0c38a2c593623 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jun 2022 22:13:29 +0200 Subject: [PATCH 06/22] Fix changing file redirections manually not counting as applied changes. --- Penumbra/Mods/Editor/Mod.Editor.Files.cs | 4 +-- Penumbra/UI/Classes/ModEditWindow.Files.cs | 34 +++++++++++----------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 5d56c871..029c93fa 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -163,7 +163,7 @@ public partial class Mod // If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. public bool SetGamePath( int fileIdx, int pathIdx, Utf8GamePath path ) { - if( _usedPaths.Contains( path ) || fileIdx < 0 || fileIdx > _availableFiles.Count || pathIdx < 0 ) + if( _usedPaths.Contains( path ) || fileIdx < 0 || fileIdx > _availableFiles.Count ) { return false; } @@ -174,7 +174,7 @@ public partial class Mod return false; } - if( pathIdx == registry.SubModUsage.Count ) + if( (pathIdx == - 1 || pathIdx == registry.SubModUsage.Count) && !path.IsEmpty ) { registry.SubModUsage.Add( ( CurrentOption, path ) ); ++registry.CurrentUsage; diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 47a2178c..78f27e3c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -154,17 +154,12 @@ public partial class ModEditWindow if( ImGui.IsItemDeactivatedAfterEdit() ) { + if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) ) + { + _editor!.SetGamePath( _fileIdx, _pathIdx, path ); + } _fileIdx = -1; _pathIdx = -1; - if( _gamePathEdit.Length == 0 ) - { - registry.SubModUsage.RemoveAt( j-- ); - --registry.CurrentUsage; - } - else if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) ) - { - registry.SubModUsage[ j ] = ( subMod, path ); - } } } @@ -181,13 +176,12 @@ public partial class ModEditWindow if( ImGui.IsItemDeactivatedAfterEdit() ) { - _fileIdx = -1; - _pathIdx = -1; if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) && !path.IsEmpty ) { - registry.SubModUsage.Add( ( subMod, path ) ); - ++registry.CurrentUsage; + _editor!.SetGamePath( _fileIdx, _pathIdx, path ); } + _fileIdx = -1; + _pathIdx = -1; } } @@ -200,19 +194,22 @@ public partial class ModEditWindow ImGui.DragInt( "##skippedFolders", ref _folderSkip, 0.01f, 0, 10 ); ImGuiUtil.HoverTooltip( "Skip the first N folders when automatically constructing the game path from the file path." ); ImGui.SameLine(); - spacing.Pop( ); + spacing.Pop(); if( ImGui.Button( "Add Paths" ) ) { _editor!.AddPathsToSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ), _folderSkip ); } - ImGuiUtil.HoverTooltip( "Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders." ); - + + ImGuiUtil.HoverTooltip( + "Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders." ); + ImGui.SameLine(); if( ImGui.Button( "Remove Paths" ) ) { _editor!.RemovePathsFromSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) ); } + ImGuiUtil.HoverTooltip( "Remove all game paths associated with the selected files in the current option." ); @@ -221,7 +218,9 @@ public partial class ModEditWindow { _editor!.DeleteFiles( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) ); } - ImGuiUtil.HoverTooltip( "Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!" ); + + ImGuiUtil.HoverTooltip( + "Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!" ); ImGui.SameLine(); var changes = _editor!.FileChanges; var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; @@ -239,6 +238,7 @@ public partial class ModEditWindow { _editor!.RevertFiles(); } + ImGuiUtil.HoverTooltip( "Revert all revertible changes since the last file or option reload or data refresh." ); ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale ); From 10f06e2715e5715b4d17ff2e156b82fa3a2dfc68 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jun 2022 22:15:23 +0200 Subject: [PATCH 07/22] Start texture import stuff. --- OtterGui | 2 +- Penumbra/Import/Textures/TextureImporter.cs | 446 ++++++++++++++++++ Penumbra/Penumbra.csproj | 1 + Penumbra/UI/Classes/ModEditWindow.Textures.cs | 348 ++++++++++++++ Penumbra/UI/Classes/ModEditWindow.cs | 1 + 5 files changed, 797 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Import/Textures/TextureImporter.cs create mode 100644 Penumbra/UI/Classes/ModEditWindow.Textures.cs diff --git a/OtterGui b/OtterGui index a9a5b2a4..0bd85ed7 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a9a5b2a4bbf061d9cfed5234ca731bd2d94bcb96 +Subproject commit 0bd85ed72057b1941579d20a6f622cc2cd9c58ac diff --git a/Penumbra/Import/Textures/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs new file mode 100644 index 00000000..864bcb49 --- /dev/null +++ b/Penumbra/Import/Textures/TextureImporter.cs @@ -0,0 +1,446 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; +using Dalamud.Logging; +using Lumina.Data.Files; +using Lumina.Extensions; +using System.Drawing; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Textures; + +[StructLayout( LayoutKind.Sequential )] +public struct PixelFormat +{ + [Flags] + public enum FormatFlags : uint + { + AlphaPixels = 0x000001, + Alpha = 0x000002, + FourCC = 0x000004, + RGB = 0x000040, + YUV = 0x000200, + Luminance = 0x020000, + } + + public enum FourCCType : uint + { + DXT1 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '1' << 24 ), + DXT3 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '3' << 24 ), + DXT5 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '5' << 24 ), + DX10 = 'D' | ( 'X' << 8 ) | ( '1' << 16 ) | ( '0' << 24 ), + } + + public int Size; + public FormatFlags Flags; + public FourCCType FourCC; + public int RgbBitCount; + public int RBitMask; + public int GBitMask; + public int BBitMask; + public int ABitMask; +} + +[StructLayout( LayoutKind.Sequential )] +public struct DdsHeader +{ + [Flags] + public enum DdsFlags : uint + { + Caps = 0x00000001, + Height = 0x00000002, + Width = 0x00000004, + Pitch = 0x00000008, + PixelFormat = 0x00001000, + MipMapCount = 0x00020000, + LinearSize = 0x00080000, + Depth = 0x00800000, + + Required = Caps | Height | Width | PixelFormat, + } + + [Flags] + public enum DdsCaps1 : uint + { + Complex = 0x08, + MipMap = 0x400000, + Texture = 0x1000, + } + + [Flags] + public enum DdsCaps2 : uint + { + CubeMap = 0x200, + CubeMapPositiveEX = 0x400, + CubeMapNegativeEX = 0x800, + CubeMapPositiveEY = 0x1000, + CubeMapNegativeEY = 0x2000, + CubeMapPositiveEZ = 0x4000, + CubeMapNegativeEZ = 0x8000, + Volume = 0x200000, + } + + public int Size; + public DdsFlags Flags; + public int Height; + public int Width; + public int PitchOrLinearSize; + public int Depth; + public int MipMapCount; + public int Reserved1; + public int Reserved2; + public int Reserved3; + public int Reserved4; + public int Reserved5; + public int Reserved6; + public int Reserved7; + public int Reserved8; + public int Reserved9; + public int ReservedA; + public int ReservedB; + public PixelFormat PixelFormat; + public DdsCaps1 Caps1; + public DdsCaps2 Caps2; + public uint Caps3; + public uint Caps4; + public int ReservedC; +} + +[StructLayout( LayoutKind.Sequential )] +public struct DXT10Header +{ + public enum DXGIFormat : uint + { + Unknown = 0, + R32G32B32A32Typeless = 1, + R32G32B32A32Float = 2, + R32G32B32A32UInt = 3, + R32G32B32A32SInt = 4, + R32G32B32Typeless = 5, + R32G32B32Float = 6, + R32G32B32UInt = 7, + R32G32B32SInt = 8, + R16G16B16A16Typeless = 9, + R16G16B16A16Float = 10, + R16G16B16A16UNorm = 11, + R16G16B16A16UInt = 12, + R16G16B16A16SNorm = 13, + R16G16B16A16SInt = 14, + R32G32Typeless = 15, + R32G32Float = 16, + R32G32UInt = 17, + R32G32SInt = 18, + R32G8X24Typeless = 19, + D32FloatS8X24UInt = 20, + R32FloatX8X24Typeless = 21, + X32TypelessG8X24UInt = 22, + R10G10B10A2Typeless = 23, + R10G10B10A2UNorm = 24, + R10G10B10A2UInt = 25, + R11G11B10Float = 26, + R8G8B8A8Typeless = 27, + R8G8B8A8UNorm = 28, + R8G8B8A8UNormSRGB = 29, + R8G8B8A8UInt = 30, + R8G8B8A8SNorm = 31, + R8G8B8A8SInt = 32, + R16G16Typeless = 33, + R16G16Float = 34, + R16G16UNorm = 35, + R16G16UInt = 36, + R16G16SNorm = 37, + R16G16SInt = 38, + R32Typeless = 39, + D32Float = 40, + R32Float = 41, + R32UInt = 42, + R32SInt = 43, + R24G8Typeless = 44, + D24UNormS8UInt = 45, + R24UNormX8Typeless = 46, + X24TypelessG8UInt = 47, + R8G8Typeless = 48, + R8G8UNorm = 49, + R8G8UInt = 50, + R8G8SNorm = 51, + R8G8SInt = 52, + R16Typeless = 53, + R16Float = 54, + D16UNorm = 55, + R16UNorm = 56, + R16UInt = 57, + R16SNorm = 58, + R16SInt = 59, + R8Typeless = 60, + R8UNorm = 61, + R8UInt = 62, + R8SNorm = 63, + R8SInt = 64, + A8UNorm = 65, + R1UNorm = 66, + R9G9B9E5SharedEXP = 67, + R8G8B8G8UNorm = 68, + G8R8G8B8UNorm = 69, + BC1Typeless = 70, + BC1UNorm = 71, + BC1UNormSRGB = 72, + BC2Typeless = 73, + BC2UNorm = 74, + BC2UNormSRGB = 75, + BC3Typeless = 76, + BC3UNorm = 77, + BC3UNormSRGB = 78, + BC4Typeless = 79, + BC4UNorm = 80, + BC4SNorm = 81, + BC5Typeless = 82, + BC5UNorm = 83, + BC5SNorm = 84, + B5G6R5UNorm = 85, + B5G5R5A1UNorm = 86, + B8G8R8A8UNorm = 87, + B8G8R8X8UNorm = 88, + R10G10B10XRBiasA2UNorm = 89, + B8G8R8A8Typeless = 90, + B8G8R8A8UNormSRGB = 91, + B8G8R8X8Typeless = 92, + B8G8R8X8UNormSRGB = 93, + BC6HTypeless = 94, + BC6HUF16 = 95, + BC6HSF16 = 96, + BC7Typeless = 97, + BC7UNorm = 98, + BC7UNormSRGB = 99, + AYUV = 100, + Y410 = 101, + Y416 = 102, + NV12 = 103, + P010 = 104, + P016 = 105, + F420Opaque = 106, + YUY2 = 107, + Y210 = 108, + Y216 = 109, + NV11 = 110, + AI44 = 111, + IA44 = 112, + P8 = 113, + A8P8 = 114, + B4G4R4A4UNorm = 115, + P208 = 130, + V208 = 131, + V408 = 132, + SamplerFeedbackMinMipOpaque, + SamplerFeedbackMipRegionUsedOpaque, + ForceUInt = 0xffffffff, + } + + public enum D3DResourceDimension : int + { + Unknown = 0, + Buffer = 1, + Texture1D = 2, + Texture2D = 3, + Texture3D = 4, + } + + [Flags] + public enum D3DResourceMiscFlags : uint + { + GenerateMips = 0x000001, + Shared = 0x000002, + TextureCube = 0x000004, + DrawIndirectArgs = 0x000010, + BufferAllowRawViews = 0x000020, + BufferStructured = 0x000040, + ResourceClamp = 0x000080, + SharedKeyedMutex = 0x000100, + GDICompatible = 0x000200, + SharedNTHandle = 0x000800, + RestrictedContent = 0x001000, + RestrictSharedResource = 0x002000, + RestrictSharedResourceDriver = 0x004000, + Guarded = 0x008000, + TilePool = 0x020000, + Tiled = 0x040000, + HWProtected = 0x080000, + SharedDisplayable, + SharedExclusiveWriter, + }; + + public enum D3DAlphaMode : int + { + Unknown = 0, + Straight = 1, + Premultiplied = 2, + Opaque = 3, + Custom = 4, + }; + + public DXGIFormat Format; + public D3DResourceDimension ResourceDimension; + public D3DResourceMiscFlags MiscFlags; + public uint ArraySize; + public D3DAlphaMode AlphaMode; +} + +public class DdsFile +{ + public const int DdsIdentifier = 0x20534444; + + public DdsHeader Header; + public DXT10Header? DXT10Header; + public byte[] MainSurfaceData; + public byte[] RemainingSurfaceData; + + private DdsFile( DdsHeader header, byte[] mainSurfaceData, byte[] remainingSurfaceData, DXT10Header? dXT10Header = null ) + { + Header = header; + DXT10Header = dXT10Header; + MainSurfaceData = mainSurfaceData; + RemainingSurfaceData = remainingSurfaceData; + } + + public static bool Load( Stream data, [NotNullWhen( true )] out DdsFile? file ) + { + file = null; + try + { + using var br = new BinaryReader( data ); + if( br.ReadUInt32() != DdsIdentifier ) + { + return false; + } + + var header = br.ReadStructure< DdsHeader >(); + var dxt10 = header.PixelFormat.FourCC == PixelFormat.FourCCType.DX10 ? ( DXT10Header? )br.ReadStructure< DXT10Header >() : null; + + file = new DdsFile( header, br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ), Array.Empty< byte >(), + dxt10 ); + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load DDS file:\n{e}" ); + return false; + } + } + + public bool ConvertToTex( out byte[] texBytes ) + { + using var mem = new MemoryStream( MainSurfaceData.Length * 2 ); + using( var bw = new BinaryWriter( mem ) ) + { + var format = WriteTexHeader( bw ); + bw.Write( ConvertBytes( MainSurfaceData, format ) ); + } + + texBytes = mem.ToArray(); + return true; + } + + private TexFile.TextureFormat WriteTexHeader( BinaryWriter bw ) + { + var (format, mipLength) = ConvertFormat( Header.PixelFormat, Header.Height, Header.Width, DXT10Header ); + + bw.Write( ( uint )TexFile.Attribute.TextureType2D ); + bw.Write( ( uint )format ); + bw.Write( ( ushort )Header.Width ); + bw.Write( ( ushort )Header.Height ); + bw.Write( ( ushort )Header.Depth ); + bw.Write( ( ushort )Header.MipMapCount ); + bw.Write( 0 ); + bw.Write( 1 ); + bw.Write( 2 ); + + var offset = 80; + for( var i = 0; i < Header.MipMapCount; ++i ) + { + bw.Write( offset ); + offset += mipLength; + mipLength = Math.Max( 16, mipLength >> 2 ); + } + + for( var i = Header.MipMapCount; i < 13; ++i ) + { + bw.Write( 0 ); + } + + return format; + } + + private static byte[] ConvertBytes( byte[] ddsData, TexFile.TextureFormat format ) + { + return format switch + { + _ => ddsData, + }; + } + + private static (TexFile.TextureFormat, int) ConvertFormat( PixelFormat format, int height, int width, DXT10Header? dxt10 ) + => format.FourCC switch + { + PixelFormat.FourCCType.DXT1 => ( TexFile.TextureFormat.DXT1, height * width / 2 ), + PixelFormat.FourCCType.DXT3 => ( TexFile.TextureFormat.DXT3, height * width * 4 ), + PixelFormat.FourCCType.DXT5 => ( TexFile.TextureFormat.DXT5, height * width ), + PixelFormat.FourCCType.DX10 => dxt10!.Value.Format switch + { + Textures.DXT10Header.DXGIFormat.A8UNorm => ( TexFile.TextureFormat.A8, height * width ), + Textures.DXT10Header.DXGIFormat.R8G8B8A8UInt => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), + Textures.DXT10Header.DXGIFormat.R8G8UNorm => ( TexFile.TextureFormat.L8, height * width ), + Textures.DXT10Header.DXGIFormat.B8G8R8X8UNorm => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + Textures.DXT10Header.DXGIFormat.B4G4R4A4UNorm => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ), + Textures.DXT10Header.DXGIFormat.B5G5R5A1UNorm => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ), + Textures.DXT10Header.DXGIFormat.R32Float => ( TexFile.TextureFormat.R32F, height * width * 4 ), + Textures.DXT10Header.DXGIFormat.R32G32B32A32Float => ( TexFile.TextureFormat.R32G32B32A32F, height * width * 16 ), + Textures.DXT10Header.DXGIFormat.R16G16Float => ( TexFile.TextureFormat.R16G16F, height * width * 4 ), + Textures.DXT10Header.DXGIFormat.R16G16B16A16Float => ( TexFile.TextureFormat.R16G16B16A16F, height * width * 8 ), + Textures.DXT10Header.DXGIFormat.D16UNorm => ( TexFile.TextureFormat.D16, height * width * 2 ), + Textures.DXT10Header.DXGIFormat.D24UNormS8UInt => ( TexFile.TextureFormat.D24S8, height * width * 4 ), + _ => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), + }, + _ => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), + }; +} + +public class TextureImporter +{ + public static bool ReadPng( string inputFile, out byte[] texData ) + { + using var file = File.OpenRead( inputFile ); + var image = Image.Load< Bgra32 >( file ); + + var buffer = new byte[80 + image.Height * image.Width * 4]; + using( var mem = new MemoryStream( buffer ) ) + { + using( var bw = new BinaryWriter( mem ) ) + { + bw.Write( ( uint )TexFile.Attribute.TextureType2D ); + bw.Write( ( uint )TexFile.TextureFormat.A8R8G8B8 ); + bw.Write( ( ushort )image.Width ); + bw.Write( ( ushort )image.Height ); + bw.Write( ( ushort )1 ); + bw.Write( ( ushort )1 ); + bw.Write( 0 ); + bw.Write( 1 ); + bw.Write( 2 ); + bw.Write( 80 ); + for( var i = 1; i < 13; ++i ) + { + bw.Write( 0 ); + } + } + } + + var span = new Span< byte >( buffer, 80, buffer.Length - 80 ); + image.CopyPixelDataTo( span ); + + texData = buffer; + return true; + } + + public void Import( string inputFile ) + { } +} \ No newline at end of file diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 0626797f..eb2f204f 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -66,6 +66,7 @@ + diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs new file mode 100644 index 00000000..881e9d11 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -0,0 +1,348 @@ +using System; +using System.IO; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using Dalamud.Interface; +using Dalamud.Logging; +using Dalamud.Utility; +using ImGuiNET; +using ImGuiScene; +using Lumina.Data; +using Lumina.Data.Files; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.Import.Textures; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private string _pathLeft = string.Empty; + private string _pathRight = string.Empty; + private string _pathSave = string.Empty; + + private byte[]? _imageLeft; + private byte[]? _imageRight; + private byte[]? _imageCenter; + + private TextureWrap? _wrapLeft; + private TextureWrap? _wrapRight; + private TextureWrap? _wrapCenter; + + private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; + private Matrix4x4 _multiplierRight = Matrix4x4.Identity; + + private bool DrawMatrixInput( float width, ref Matrix4x4 matrix ) + { + using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); + if( !table ) + { + return false; + } + + var changes = false; + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGuiUtil.Center( "R" ); + ImGui.TableNextColumn(); + ImGuiUtil.Center( "G" ); + ImGui.TableNextColumn(); + ImGuiUtil.Center( "B" ); + ImGui.TableNextColumn(); + ImGuiUtil.Center( "A" ); + + var inputWidth = width / 6; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "R " ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##RR", ref matrix.M11, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##RG", ref matrix.M12, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##RB", ref matrix.M13, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##RA", ref matrix.M14, 0.001f, -1f, 1f ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "G " ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##GR", ref matrix.M21, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##GG", ref matrix.M22, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##GB", ref matrix.M23, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##GA", ref matrix.M24, 0.001f, -1f, 1f ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "B " ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##BR", ref matrix.M31, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##BG", ref matrix.M32, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##BB", ref matrix.M33, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##BA", ref matrix.M34, 0.001f, -1f, 1f ); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text( "A " ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##AR", ref matrix.M41, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##AG", ref matrix.M42, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##AB", ref matrix.M43, 0.001f, -1f, 1f ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( inputWidth ); + changes |= ImGui.DragFloat( "##AA", ref matrix.M44, 0.001f, -1f, 1f ); + + return changes; + } + + private bool PathInputBox( string label, string hint, string tooltip, ref string path ) + { + var tmp = path; + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); + ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); + ImGui.InputTextWithHint( label, hint, ref tmp, Utf8GamePath.MaxGamePathLength ); + var ret = ImGui.IsItemDeactivatedAfterEdit() && tmp != path; + ImGuiUtil.HoverTooltip( tooltip ); + ImGui.SameLine(); + ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, true ); + if( ret ) + { + path = tmp; + } + + return ret; + } + + private static (byte[]?, int, int) GetDdsRgbaData( string path ) + { + try + { + using var stream = File.OpenRead( path ); + if( !DdsFile.Load( stream, out var f ) ) + { + return ( null, 0, 0 ); + } + + f.ConvertToTex( out var bytes ); + using var ms = new MemoryStream( bytes ); + using var sq = new SqPackStream( ms ); + var x = sq.ReadFile< TexFile >( 0 ); + return ( x.GetRgbaImageData(), x.Header.Width, x.Header.Height ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse DDS {path} to RGBA:\n{e}" ); + return ( null, 0, 0 ); + } + } + + private static ( byte[]?, int, int) GetTexRgbaData( string path, bool fromDisk ) + { + try + { + var tex = fromDisk ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( path ) : Dalamud.GameData.GetFile< TexFile >( path ); + return tex == null + ? ( null, 0, 0 ) + : ( tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse TEX {path} to RGBA:\n{e}" ); + return ( null, 0, 0 ); + } + } + + private static (byte[]?, int, int) GetPngRgbaData( string path ) + { + try + { + using var stream = File.OpenRead( path ); + var png = Image.Load< Rgba32 >( stream ); + var bytes = new byte[png.Height * png.Width * 4]; + png.CopyPixelDataTo( bytes ); + return ( bytes, png.Width, png.Height ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse PNG {path} to RGBA:\n{e}" ); + return ( null, 0, 0 ); + } + } + + private void UpdateImage( string path, ref byte[]? data, ref TextureWrap? wrap ) + { + data = null; + wrap?.Dispose(); + wrap = null; + var width = 0; + var height = 0; + + if( Path.IsPathRooted( path ) ) + { + if( File.Exists( path ) ) + { + ( data, width, height ) = Path.GetExtension( path ) switch + { + ".dds" => GetDdsRgbaData( path ), + ".png" => GetPngRgbaData( path ), + ".tex" => GetTexRgbaData( path, true ), + _ => ( null, 0, 0 ), + }; + } + } + else + { + ( data, width, height ) = GetTexRgbaData( path, false ); + } + + if( data != null ) + { + wrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( data, width, height, 4 ); + } + + UpdateCenter(); + } + + private void AddPixels( int width, int x, int y ) + { + var offset = ( y * width + x ) * 4; + var rgbaLeft = _imageLeft != null + ? new Rgba32( _imageLeft[ offset ], _imageLeft[ offset + 1 ], _imageLeft[ offset + 2 ], _imageLeft[ offset + 3 ] ) + : new Rgba32(); + var rgbaRight = _imageRight != null + ? new Rgba32( _imageRight[ offset ], _imageRight[ offset + 1 ], _imageRight[ offset + 2 ], _imageRight[ offset + 3 ] ) + : new Rgba32(); + var transformLeft = Vector4.Transform( rgbaLeft.ToVector4(), _multiplierLeft ); + var transformRight = Vector4.Transform( rgbaRight.ToVector4(), _multiplierRight ); + var alpha = transformLeft.Z + transformRight.Z * ( 1 - transformLeft.Z ); + var rgba = alpha == 0 + ? new Rgba32() + : new Rgba32( ( transformLeft * transformLeft.Z + transformRight * transformRight.Z * ( 1 - transformLeft.Z ) ) / alpha ); + _imageCenter![ offset ] = rgba.R; + _imageCenter![ offset + 1 ] = rgba.G; + _imageCenter![ offset + 2 ] = rgba.B; + _imageCenter![ offset + 3 ] = rgba.A; + } + + private void UpdateCenter() + { + _wrapCenter?.Dispose(); + if( _imageLeft != null || _imageRight != null ) + { + var (width, height) = _imageLeft != null ? ( _wrapLeft!.Width, _wrapLeft.Height ) : ( _wrapRight!.Width, _wrapRight.Height ); + if( _imageRight == null || _wrapRight!.Width == width && _wrapRight!.Height == height ) + { + _imageCenter = new byte[4 * width * height]; + + for( var y = 0; y < height; ++y ) + { + for( var x = 0; x < width; ++x ) + { + AddPixels( width, x, y ); + } + } + + _wrapCenter = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _imageCenter, width, height, 4 ); + return; + } + } + + _imageCenter = null; + _wrapCenter = null; + } + + private static void ScaledImage( TextureWrap? wrap, Vector2 size ) + { + if( wrap != null ) + { + size = size with { Y = wrap.Height * size.X / wrap.Width }; + ImGui.Image( wrap.ImGuiHandle, size ); + } + else + { + ImGui.Dummy( size ); + } + } + + private void DrawTextureTab() + { + using var tab = ImRaii.TabItem( "Texture Import/Export" ); + if( !tab ) + { + return; + } + + var leftRightWidth = new Vector2( ( ImGui.GetWindowContentRegionWidth() - ImGui.GetStyle().FramePadding.X * 4 ) / 3, -1 ); + var imageSize = new Vector2( leftRightWidth.X - ImGui.GetStyle().FramePadding.X * 2 ); + using( var child = ImRaii.Child( "ImageLeft", leftRightWidth, true ) ) + { + if( PathInputBox( "##ImageLeft", "Import Image...", string.Empty, ref _pathLeft ) ) + { + UpdateImage( _pathLeft, ref _imageLeft, ref _wrapLeft ); + } + + ImGui.NewLine(); + if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) ) + { + UpdateCenter(); + } + + ImGui.NewLine(); + ScaledImage( _wrapLeft, imageSize ); + + } + + ImGui.SameLine(); + using( var child = ImRaii.Child( "ImageMix", leftRightWidth, true ) ) + { + ScaledImage( _wrapCenter, imageSize ); + } + + ImGui.SameLine(); + using( var child = ImRaii.Child( "ImageRight", leftRightWidth, true ) ) + { + if( PathInputBox( "##ImageRight", "Import Image...", string.Empty, ref _pathRight ) ) + { + UpdateImage( _pathRight, ref _imageRight, ref _wrapRight ); + } + + ImGui.NewLine(); + if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) ) + { + UpdateCenter(); + } + + ImGui.NewLine(); + ScaledImage( _wrapRight, imageSize ); + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 67c3b183..5d2fe47d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -61,6 +61,7 @@ public partial class ModEditWindow : Window, IDisposable DrawMissingFilesTab(); DrawDuplicatesTab(); DrawMaterialChangeTab(); + DrawTextureTab(); } // A row of three buttonSizes and a help marker that can be used for material suffix changing. From b29a362395b758ee75214b940e5c8a9f7aa5cd1d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jun 2022 22:33:31 +0200 Subject: [PATCH 08/22] Add new test release action with separate handling. --- .github/workflows/release.yml | 4 +- .github/workflows/test_release.yml | 84 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test_release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32c21a39..17c1b060 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,8 @@ name: Create Release on: push: - tags: - - '*' + tags-ignore: + - t* jobs: build: diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml new file mode 100644 index 00000000..cdc7b5ec --- /dev/null +++ b/.github/workflows/test_release.yml @@ -0,0 +1,84 @@ +name: Create Test Release + +on: + push: + tags: + - t* + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.100 + - name: Restore dependencies + run: dotnet restore + - name: Download Dalamud + run: | + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/net5/latest.zip -OutFile latest.zip + Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" + - name: Build + run: | + $ver = '${{ github.ref }}' -replace 'refs/tags/t','' + invoke-expression 'dotnet build --no-restore --configuration Debug --nologo -p:Version=$ver -p:FileVersion=$ver -p:AssemblyVersion=$ver' + - name: write version into json + run: | + $ver = '${{ github.ref }}' -replace 'refs/tags/t','' + $path = './Penumbra/bin/Debug/net5.0-windows/Penumbra.json' + $content = get-content -path $path + $content = $content -replace '1.0.0.0',$ver + set-content -Path $path -Value $content + - name: Archive + run: Compress-Archive -Path Penumbra/bin/Debug/net5.0-windows/* -DestinationPath Penumbra.zip + - name: Upload a Build Artifact + uses: actions/upload-artifact@v2.2.1 + with: + path: | + ./Penumbra/bin/Debug/net5.0-windows/* + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Penumbra ${{ github.ref }} + draft: false + prerelease: false + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./Penumbra.zip + asset_name: Penumbra.zip + asset_content_type: application/zip + + - name: Write out repo.json + run: | + $ver = '${{ github.ref }}' -replace 'refs/tags/t','' + $ver2 = '${{ github.ref }}' -replace 'refs/tags/','' + $path = './base_repo.json' + $new_path = './repo.json' + $content = get-content -path $path + $content = $content -replace '/1.0.0.0/',"/$ver2/" + $content = $content -replace '1.0.0.0',$ver + set-content -Path $new_path -Value $content + + - name: Commit repo.json + run: | + git config --global user.name "Actions User" + git config --global user.email "actions@github.com" + + git fetch origin master && git fetch origin test && git checkout test && git reset --hard master + git add repo.json + git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true + + git push origin test || true From 28b7bf91bcf0ad8f584ed828429e911bc7d041ed Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Jun 2022 00:35:15 +0200 Subject: [PATCH 09/22] fixup! Fix disabling a inheritance not removing the mod correctly. --- Penumbra/Collections/ModCollection.Cache.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 9a9e31bd..6e959e69 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -87,13 +87,21 @@ public partial class ModCollection ReloadMod( Penumbra.ModManager[ modIdx ], true ); break; case ModSettingChange.EnableState: - if( _collection.Settings[ modIdx ]!.Enabled ) + if( oldValue == 0 ) { - AddMod( Penumbra.ModManager[modIdx], true ); + AddMod( Penumbra.ModManager[ modIdx ], true ); + } + else if( oldValue == 1 ) + { + RemoveMod( Penumbra.ModManager[ modIdx ], true ); + } + else if( _collection[ modIdx ].Settings?.Enabled == true ) + { + ReloadMod( Penumbra.ModManager[ modIdx ], true ); } else { - RemoveMod( Penumbra.ModManager[modIdx], true ); + RemoveMod( Penumbra.ModManager[ modIdx ], true ); } break; From 7fea8d385416bf6008d38606fb80ea5ed2c0b47e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Jun 2022 00:43:38 +0200 Subject: [PATCH 10/22] Change test action yaml to reset correctly. --- .github/workflows/test_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index cdc7b5ec..bc242bf1 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -77,8 +77,8 @@ jobs: git config --global user.name "Actions User" git config --global user.email "actions@github.com" - git fetch origin master && git fetch origin test && git checkout test && git reset --hard master + git fetch origin master && git fetch origin test && git branch -f test origin/master && git checkout test git add repo.json git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true - git push origin test || true + git push origin test -f || true From 5f1dac98d69f80024d37e652c2f21556eb5e5178 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Jun 2022 01:11:50 +0200 Subject: [PATCH 11/22] Also reset test for actual releases --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17c1b060..fbad3c82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,8 +75,10 @@ jobs: git config --global user.name "Actions User" git config --global user.email "actions@github.com" - git fetch origin master && git checkout master + git fetch origin master && git fetch origin test && git branch -f test origin/master && git checkout master git add repo.json git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true git push origin master || true + git checkout test + git push origin test -f || true From 46c5d52a927ceb7d8f9126a9712ae47b82a62e8e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Jun 2022 11:59:53 +0200 Subject: [PATCH 12/22] Add some other AVFX collection identification. --- .../Interop/Resolver/PathResolver.Animation.cs | 14 ++++++++++++++ Penumbra/Interop/Resolver/PathResolver.Data.cs | 3 +++ 2 files changed, 17 insertions(+) diff --git a/Penumbra/Interop/Resolver/PathResolver.Animation.cs b/Penumbra/Interop/Resolver/PathResolver.Animation.cs index 026c4a5a..363ce1e2 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Animation.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Animation.cs @@ -58,4 +58,18 @@ public unsafe partial class PathResolver CharacterBaseLoadAnimationHook!.Original( drawObject ); _animationLoadCollection = last; } + + public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2 ); + + [Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )] + public Hook< LoadSomeAvfx >? LoadSomeAvfxHook; + + private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2 ) + { + var last = _animationLoadCollection; + _animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject ); + var ret = LoadSomeAvfxHook!.Original( a1, gameObject, gameObject2 ); + _animationLoadCollection = last; + return ret; + } } \ No newline at end of file diff --git a/Penumbra/Interop/Resolver/PathResolver.Data.cs b/Penumbra/Interop/Resolver/PathResolver.Data.cs index 11d09ad2..00b92905 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Data.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Data.cs @@ -92,6 +92,7 @@ public unsafe partial class PathResolver Penumbra.CollectionManager.CollectionChanged += CheckCollections; LoadTimelineResourcesHook?.Enable(); CharacterBaseLoadAnimationHook?.Enable(); + LoadSomeAvfxHook?.Enable(); } private void DisableDataHooks() @@ -103,6 +104,7 @@ public unsafe partial class PathResolver CharacterBaseDestructorHook?.Disable(); LoadTimelineResourcesHook?.Disable(); CharacterBaseLoadAnimationHook?.Disable(); + LoadSomeAvfxHook?.Disable(); } private void DisposeDataHooks() @@ -113,6 +115,7 @@ public unsafe partial class PathResolver CharacterBaseDestructorHook?.Dispose(); LoadTimelineResourcesHook?.Dispose(); CharacterBaseLoadAnimationHook?.Dispose(); + LoadSomeAvfxHook?.Dispose(); } // This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. From 1f46b4951ef45db9c8956bba70dd76f9c09b95db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Jun 2022 15:15:03 +0200 Subject: [PATCH 13/22] Reorder collections tab. --- OtterGui | 2 +- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 66 ++++++++++------------ 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/OtterGui b/OtterGui index 0bd85ed7..f48a4ecc 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0bd85ed72057b1941579d20a6f622cc2cd9c58ac +Subproject commit f48a4eccc654ec1ab1b72aa23324bdce13615f97 diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs index 45771ae7..efbb6532 100644 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -27,8 +27,8 @@ public partial class ConfigWindow return; } - DrawMainSelectors(); DrawCharacterCollectionSelectors(); + DrawMainSelectors(); } @@ -139,52 +139,46 @@ public partial class ConfigWindow private void DrawCharacterCollectionSelectors() { - using var child = ImRaii.Child( "##Collections", -Vector2.One, true ); - if( !child ) + ImGui.Dummy( _window._defaultSpace ); + if( ImGui.CollapsingHeader( "Active Collections", ImGuiTreeNodeFlags.DefaultOpen ) ) { - return; - } - - DrawDefaultCollectionSelector(); - - foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) - { - using var id = ImRaii.PushId( name ); - DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, ModCollection.Type.Character, true, name ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, - false, - true ) ) + ImGui.Dummy( _window._defaultSpace ); + DrawDefaultCollectionSelector(); + ImGui.Dummy( _window._defaultSpace ); + foreach( var name in Penumbra.CollectionManager.Characters.Keys.OrderBy( k => k ).ToArray() ) { - Penumbra.CollectionManager.RemoveCharacterCollection( name ); + using var id = ImRaii.PushId( name ); + DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, ModCollection.Type.Character, true, name ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, + false, + true ) ) + { + Penumbra.CollectionManager.RemoveCharacterCollection( name ); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted( name ); } - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( name ); + DrawNewCharacterCollection(); + ImGui.NewLine(); } - - DrawNewCharacterCollection(); } private void DrawMainSelectors() { - var size = new Vector2( -1, - ImGui.GetTextLineHeightWithSpacing() * (InheritedCollectionHeight + 1) - + _window._defaultSpace.Y * 2 - + ImGui.GetFrameHeightWithSpacing() * 4 - + ImGui.GetStyle().ItemSpacing.Y * 6 ); - using var main = ImRaii.Child( "##CollectionsMain", size, true ); - if( !main ) + ImGui.Dummy( _window._defaultSpace ); + if( ImGui.CollapsingHeader( "Collection Settings", ImGuiTreeNodeFlags.DefaultOpen ) ) { - return; + ImGui.Dummy( _window._defaultSpace ); + DrawCurrentCollectionSelector(); + ImGui.Dummy( _window._defaultSpace ); + DrawNewCollectionInput(); + ImGui.Dummy( _window._defaultSpace ); + DrawInheritanceBlock(); } - - DrawCurrentCollectionSelector(); - ImGui.Dummy( _window._defaultSpace ); - DrawNewCollectionInput(); - ImGui.Dummy( _window._defaultSpace ); - DrawInheritanceBlock(); } } } \ No newline at end of file From b104bc324961e0ce846a0e6d285635cdfad638a1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Jun 2022 15:18:20 +0200 Subject: [PATCH 14/22] Rename tab buttons. --- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 6c72a00a..d0ad872d 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -34,7 +34,7 @@ public partial class ConfigWindow private static readonly Utf8String DescriptionTabHeader = Utf8String.FromStringUnsafe( "Description", false ); private static readonly Utf8String SettingsTabHeader = Utf8String.FromStringUnsafe( "Settings", false ); private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false ); - private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod Meta", false ); + private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false ); private void DrawTabBar() { @@ -56,7 +56,7 @@ public partial class ConfigWindow DrawChangedItemsTab(); DrawConflictsTab(); DrawEditModTab(); - if( ImGui.TabItemButton( "Open Advanced Edit Window", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) + if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) { _window.ModEditPopup.ChangeMod( _mod ); _window.ModEditPopup.ChangeOption( -1, 0 ); @@ -69,7 +69,8 @@ public partial class ConfigWindow + "\t\t- file swaps\n" + "\t\t- metadata manipulations\n" + "\t\t- model materials\n" - + "\t\t- duplicates" ); + + "\t\t- duplicates\n" + + "\t\t- textures" ); } // Just a simple text box with the wrapped description, if it exists. From 80c717c9bc8fea942071591472dc032709b5db9d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 12 Jun 2022 15:41:12 +0200 Subject: [PATCH 15/22] No COM in OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index f48a4ecc..d97a2692 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f48a4eccc654ec1ab1b72aa23324bdce13615f97 +Subproject commit d97a26923981db2a27d0172367c9e2841767f9b1 From e994163637f07f3fb2fbd00789b11ad0f2e22d52 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 13 Jun 2022 22:26:15 +0200 Subject: [PATCH 16/22] Further work on texture importing. --- Penumbra/Import/Textures/TextureImporter.cs | 207 ++++++++--- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 338 ++++++++++++------ 2 files changed, 391 insertions(+), 154 deletions(-) diff --git a/Penumbra/Import/Textures/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs index 864bcb49..99708dc8 100644 --- a/Penumbra/Import/Textures/TextureImporter.cs +++ b/Penumbra/Import/Textures/TextureImporter.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; using Dalamud.Logging; using Lumina.Data.Files; using Lumina.Extensions; -using System.Drawing; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -37,10 +36,22 @@ public struct PixelFormat public FormatFlags Flags; public FourCCType FourCC; public int RgbBitCount; - public int RBitMask; - public int GBitMask; - public int BBitMask; - public int ABitMask; + public uint RBitMask; + public uint GBitMask; + public uint BBitMask; + public uint ABitMask; + + public void Write( BinaryWriter bw ) + { + bw.Write( Size ); + bw.Write( ( uint )Flags ); + bw.Write( ( uint )FourCC ); + bw.Write( RgbBitCount ); + bw.Write( RBitMask ); + bw.Write( GBitMask ); + bw.Write( BBitMask ); + bw.Write( ABitMask ); + } } [StructLayout( LayoutKind.Sequential )] @@ -82,30 +93,70 @@ public struct DdsHeader Volume = 0x200000, } - public int Size; - public DdsFlags Flags; - public int Height; - public int Width; - public int PitchOrLinearSize; - public int Depth; - public int MipMapCount; - public int Reserved1; - public int Reserved2; - public int Reserved3; - public int Reserved4; - public int Reserved5; - public int Reserved6; - public int Reserved7; - public int Reserved8; - public int Reserved9; - public int ReservedA; - public int ReservedB; - public PixelFormat PixelFormat; - public DdsCaps1 Caps1; - public DdsCaps2 Caps2; - public uint Caps3; - public uint Caps4; - public int ReservedC; + public const int Size = 124; + private int _size; + public DdsFlags Flags; + public int Height; + public int Width; + public int PitchOrLinearSize; + public int Depth; + public int MipMapCount; + public int Reserved1; + public int Reserved2; + public int Reserved3; + public int Reserved4; + public int Reserved5; + public int Reserved6; + public int Reserved7; + public int Reserved8; + public int Reserved9; + public int ReservedA; + public int ReservedB; + public PixelFormat PixelFormat; + public DdsCaps1 Caps1; + public DdsCaps2 Caps2; + public uint Caps3; + public uint Caps4; + public int ReservedC; + + public void Write( BinaryWriter bw ) + { + bw.Write( ( byte )'D' ); + bw.Write( ( byte )'D' ); + bw.Write( ( byte )'S' ); + bw.Write( ( byte )' ' ); + bw.Write( Size ); + bw.Write( ( uint )Flags ); + bw.Write( Height ); + bw.Write( Width ); + bw.Write( PitchOrLinearSize ); + bw.Write( Depth ); + bw.Write( MipMapCount ); + bw.Write( Reserved1 ); + bw.Write( Reserved2 ); + bw.Write( Reserved3 ); + bw.Write( Reserved4 ); + bw.Write( Reserved5 ); + bw.Write( Reserved6 ); + bw.Write( Reserved7 ); + bw.Write( Reserved8 ); + bw.Write( Reserved9 ); + bw.Write( ReservedA ); + bw.Write( ReservedB ); + PixelFormat.Write( bw ); + bw.Write( ( uint )Caps1 ); + bw.Write( ( uint )Caps2 ); + bw.Write( Caps3 ); + bw.Write( Caps4 ); + bw.Write( ReservedC ); + } + + public void Write( byte[] bytes, int offset ) + { + using var m = new MemoryStream( bytes, offset, bytes.Length - offset ); + using var bw = new BinaryWriter( m ); + Write( bw ); + } } [StructLayout( LayoutKind.Sequential )] @@ -407,32 +458,88 @@ public class DdsFile public class TextureImporter { - public static bool ReadPng( string inputFile, out byte[] texData ) + private static void WriteHeader( byte[] target, int width, int height ) + { + using var mem = new MemoryStream( target ); + using var bw = new BinaryWriter( mem ); + bw.Write( ( uint )TexFile.Attribute.TextureType2D ); + bw.Write( ( uint )TexFile.TextureFormat.A8R8G8B8 ); + bw.Write( ( ushort )width ); + bw.Write( ( ushort )height ); + bw.Write( ( ushort )1 ); + bw.Write( ( ushort )1 ); + bw.Write( 0 ); + bw.Write( 1 ); + bw.Write( 2 ); + bw.Write( 80 ); + for( var i = 1; i < 13; ++i ) + { + bw.Write( 0 ); + } + } + + public static unsafe bool RgbaBytesToDds( byte[] rgba, int width, int height, out byte[] ddsData ) + { + var header = new DdsHeader() + { + Caps1 = DdsHeader.DdsCaps1.Complex | DdsHeader.DdsCaps1.Texture | DdsHeader.DdsCaps1.MipMap, + Depth = 1, + Flags = DdsHeader.DdsFlags.Required | DdsHeader.DdsFlags.Pitch | DdsHeader.DdsFlags.MipMapCount, + Height = height, + Width = width, + PixelFormat = new PixelFormat() + { + Flags = PixelFormat.FormatFlags.AlphaPixels | PixelFormat.FormatFlags.RGB, + FourCC = 0, + BBitMask = 0x000000FF, + GBitMask = 0x0000FF00, + RBitMask = 0x00FF0000, + ABitMask = 0xFF000000, + Size = 32, + RgbBitCount = 32, + }, + }; + ddsData = new byte[DdsHeader.Size + rgba.Length]; + header.Write( ddsData, 0 ); + rgba.CopyTo( ddsData, DdsHeader.Size ); + for( var i = 0; i < rgba.Length; i += 4 ) + { + ( ddsData[ DdsHeader.Size + i ], ddsData[ DdsHeader.Size + i + 2 ] ) + = ( ddsData[ DdsHeader.Size + i + 2 ], ddsData[ DdsHeader.Size + i ] ); + } + + return true; + } + + public static bool RgbaBytesToTex( byte[] rgba, int width, int height, out byte[] texData ) + { + texData = Array.Empty< byte >(); + if( rgba.Length != width * height * 4 ) + { + return false; + } + + texData = new byte[80 + width * height * 4]; + WriteHeader( texData, width, height ); + // RGBA to BGRA. + for( var i = 0; i < rgba.Length; i += 4 ) + { + texData[ 80 + i + 0 ] = rgba[ i + 2 ]; + texData[ 80 + i + 1 ] = rgba[ i + 1 ]; + texData[ 80 + i + 2 ] = rgba[ i + 0 ]; + texData[ 80 + i + 3 ] = rgba[ i + 3 ]; + } + + return true; + } + + public static bool PngToTex( string inputFile, out byte[] texData ) { using var file = File.OpenRead( inputFile ); var image = Image.Load< Bgra32 >( file ); var buffer = new byte[80 + image.Height * image.Width * 4]; - using( var mem = new MemoryStream( buffer ) ) - { - using( var bw = new BinaryWriter( mem ) ) - { - bw.Write( ( uint )TexFile.Attribute.TextureType2D ); - bw.Write( ( uint )TexFile.TextureFormat.A8R8G8B8 ); - bw.Write( ( ushort )image.Width ); - bw.Write( ( ushort )image.Height ); - bw.Write( ( ushort )1 ); - bw.Write( ( ushort )1 ); - bw.Write( 0 ); - bw.Write( 1 ); - bw.Write( 2 ); - bw.Write( 80 ); - for( var i = 1; i < 13; ++i ) - { - bw.Write( 0 ); - } - } - } + WriteHeader( buffer, image.Width, image.Height ); var span = new Span< byte >( buffer, 80, buffer.Length - 80 ); image.CopyPixelDataTo( span ); diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 881e9d11..9a5c8f16 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -1,20 +1,21 @@ using System; +using System.Collections.Generic; using System.IO; using System.Numerics; using System.Reflection; -using System.Runtime.CompilerServices; using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Logging; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; -using Lumina.Data; using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.Import.Textures; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; namespace Penumbra.UI.Classes; @@ -23,7 +24,6 @@ public partial class ModEditWindow { private string _pathLeft = string.Empty; private string _pathRight = string.Empty; - private string _pathSave = string.Empty; private byte[]? _imageLeft; private byte[]? _imageRight; @@ -36,7 +36,22 @@ public partial class ModEditWindow private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; - private bool DrawMatrixInput( float width, ref Matrix4x4 matrix ) + private readonly FileDialogManager _dialogManager = new(); + + private static bool DragFloat( string label, float width, ref float value ) + { + var tmp = value; + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( width ); + if( ImGui.DragFloat( label, ref tmp, 0.001f, -1f, 1f ) ) + { + value = tmp; + } + + return ImGui.IsItemDeactivatedAfterEdit(); + } + + private static bool DrawMatrixInput( float width, ref Matrix4x4 matrix ) { using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit ); if( !table ) @@ -60,86 +75,66 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Text( "R " ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##RR", ref matrix.M11, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##RG", ref matrix.M12, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##RB", ref matrix.M13, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##RA", ref matrix.M14, 0.001f, -1f, 1f ); + changes |= DragFloat( "##RR", inputWidth, ref matrix.M11 ); + changes |= DragFloat( "##RG", inputWidth, ref matrix.M12 ); + changes |= DragFloat( "##RB", inputWidth, ref matrix.M13 ); + changes |= DragFloat( "##RA", inputWidth, ref matrix.M14 ); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Text( "G " ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##GR", ref matrix.M21, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##GG", ref matrix.M22, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##GB", ref matrix.M23, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##GA", ref matrix.M24, 0.001f, -1f, 1f ); + changes |= DragFloat( "##GR", inputWidth, ref matrix.M21 ); + changes |= DragFloat( "##GG", inputWidth, ref matrix.M22 ); + changes |= DragFloat( "##GB", inputWidth, ref matrix.M23 ); + changes |= DragFloat( "##GA", inputWidth, ref matrix.M24 ); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Text( "B " ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##BR", ref matrix.M31, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##BG", ref matrix.M32, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##BB", ref matrix.M33, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##BA", ref matrix.M34, 0.001f, -1f, 1f ); + changes |= DragFloat( "##BR", inputWidth, ref matrix.M31 ); + changes |= DragFloat( "##BG", inputWidth, ref matrix.M32 ); + changes |= DragFloat( "##BB", inputWidth, ref matrix.M33 ); + changes |= DragFloat( "##BA", inputWidth, ref matrix.M34 ); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Text( "A " ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##AR", ref matrix.M41, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##AG", ref matrix.M42, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##AB", ref matrix.M43, 0.001f, -1f, 1f ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( inputWidth ); - changes |= ImGui.DragFloat( "##AA", ref matrix.M44, 0.001f, -1f, 1f ); + changes |= DragFloat( "##AR", inputWidth, ref matrix.M41 ); + changes |= DragFloat( "##AG", inputWidth, ref matrix.M42 ); + changes |= DragFloat( "##AB", inputWidth, ref matrix.M43 ); + changes |= DragFloat( "##AA", inputWidth, ref matrix.M44 ); return changes; } - private bool PathInputBox( string label, string hint, string tooltip, ref string path ) + private void PathInputBox( string label, string hint, string tooltip, int which ) { - var tmp = path; + var tmp = which == 0 ? _pathLeft : _pathRight; using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - 3 * ImGuiHelpers.GlobalScale ); ImGui.InputTextWithHint( label, hint, ref tmp, Utf8GamePath.MaxGamePathLength ); - var ret = ImGui.IsItemDeactivatedAfterEdit() && tmp != path; - ImGuiUtil.HoverTooltip( tooltip ); - ImGui.SameLine(); - ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, true ); - if( ret ) + if( ImGui.IsItemDeactivatedAfterEdit() ) { - path = tmp; + UpdateImage( tmp, which ); } - return ret; + ImGuiUtil.HoverTooltip( tooltip ); + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), string.Empty, false, + true ) ) + { + var startPath = Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath : _mod?.ModPath.FullName; + + void UpdatePath( bool success, List< string > paths ) + { + if( success && paths.Count > 0 ) + { + UpdateImage( paths[ 0 ], which ); + } + } + + _dialogManager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); + } } private static (byte[]?, int, int) GetDdsRgbaData( string path ) @@ -153,10 +148,18 @@ public partial class ModEditWindow } f.ConvertToTex( out var bytes ); - using var ms = new MemoryStream( bytes ); - using var sq = new SqPackStream( ms ); - var x = sq.ReadFile< TexFile >( 0 ); - return ( x.GetRgbaImageData(), x.Header.Width, x.Header.Height ); + TexFile tex = new(); + tex.GetType().GetProperty( "Data", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) + ?.SetValue( tex, bytes ); + tex.GetType().GetProperty( "FileStream", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) + ?.SetValue( tex, new MemoryStream( tex.Data ) ); + tex.GetType().GetProperty( "Reader", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) + ?.SetValue( tex, new BinaryReader( tex.FileStream ) ); + tex.LoadFile(); + return ( tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height ); } catch( Exception e ) { @@ -186,7 +189,7 @@ public partial class ModEditWindow try { using var stream = File.OpenRead( path ); - var png = Image.Load< Rgba32 >( stream ); + using var png = Image.Load< Rgba32 >( stream ); var bytes = new byte[png.Height * png.Width * 4]; png.CopyPixelDataTo( bytes ); return ( bytes, png.Width, png.Height ); @@ -198,8 +201,23 @@ public partial class ModEditWindow } } - private void UpdateImage( string path, ref byte[]? data, ref TextureWrap? wrap ) + private void UpdateImage( string newPath, int which ) { + if( which is < 0 or > 1 ) + { + return; + } + + ref var path = ref which == 0 ? ref _pathLeft : ref _pathRight; + if( path == newPath ) + { + return; + } + + path = newPath; + ref var data = ref which == 0 ? ref _imageLeft : ref _imageRight; + ref var wrap = ref which == 0 ? ref _wrapLeft : ref _wrapRight; + data = null; wrap?.Dispose(); wrap = null; @@ -232,21 +250,35 @@ public partial class ModEditWindow UpdateCenter(); } + private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform ) + { + if( bytes == null ) + { + return Vector4.Zero; + } + + var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); + var transformed = Vector4.Transform( rgba.ToVector4(), transform ); + transformed.X = Math.Clamp( transformed.X, 0, 1 ); + transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); + transformed.Z = Math.Clamp( transformed.Z, 0, 1 ); + transformed.W = Math.Clamp( transformed.W, 0, 1 ); + return transformed; + } + private void AddPixels( int width, int x, int y ) { var offset = ( y * width + x ) * 4; - var rgbaLeft = _imageLeft != null - ? new Rgba32( _imageLeft[ offset ], _imageLeft[ offset + 1 ], _imageLeft[ offset + 2 ], _imageLeft[ offset + 3 ] ) - : new Rgba32(); - var rgbaRight = _imageRight != null - ? new Rgba32( _imageRight[ offset ], _imageRight[ offset + 1 ], _imageRight[ offset + 2 ], _imageRight[ offset + 3 ] ) - : new Rgba32(); - var transformLeft = Vector4.Transform( rgbaLeft.ToVector4(), _multiplierLeft ); - var transformRight = Vector4.Transform( rgbaRight.ToVector4(), _multiplierRight ); - var alpha = transformLeft.Z + transformRight.Z * ( 1 - transformLeft.Z ); - var rgba = alpha == 0 - ? new Rgba32() - : new Rgba32( ( transformLeft * transformLeft.Z + transformRight * transformRight.Z * ( 1 - transformLeft.Z ) ) / alpha ); + var left = CappedVector( _imageLeft, offset, _multiplierLeft ); + var right = CappedVector( _imageRight, offset, _multiplierRight ); + var alpha = right.W + left.W * ( 1 - right.W ); + if( alpha == 0 ) + { + return; + } + + var sum = ( right * right.W + left * left.W * ( 1 - right.W ) ) / alpha; + var rgba = new Rgba32( sum with { W = alpha } ); _imageCenter![ offset ] = rgba.R; _imageCenter![ offset + 1 ] = rgba.G; _imageCenter![ offset + 2 ] = rgba.B; @@ -255,7 +287,25 @@ public partial class ModEditWindow private void UpdateCenter() { - _wrapCenter?.Dispose(); + if( _imageLeft != null && _imageRight == null && _multiplierLeft.IsIdentity ) + { + _imageCenter = _imageLeft; + _wrapCenter = _wrapLeft; + return; + } + + if( _imageLeft == null && _imageRight != null && _multiplierRight.IsIdentity ) + { + _imageCenter = _imageRight; + _wrapCenter = _wrapRight; + return; + } + + if( !ReferenceEquals( _imageCenter, _imageLeft ) && !ReferenceEquals( _imageCenter, _imageRight ) ) + { + _wrapCenter?.Dispose(); + } + if( _imageLeft != null || _imageRight != null ) { var (width, height) = _imageLeft != null ? ( _wrapLeft!.Width, _wrapLeft.Height ) : ( _wrapRight!.Width, _wrapRight.Height ); @@ -280,21 +330,73 @@ public partial class ModEditWindow _wrapCenter = null; } - private static void ScaledImage( TextureWrap? wrap, Vector2 size ) + private static void ScaledImage( string path, TextureWrap? wrap, Vector2 size ) { if( wrap != null ) { size = size with { Y = wrap.Height * size.X / wrap.Width }; ImGui.Image( wrap.ImGuiHandle, size ); } + else if( path.Length > 0 ) + { + ImGui.TextUnformatted( "Could not load file." ); + } else { ImGui.Dummy( size ); } } + private void SaveAs( bool success, string path, int type ) + { + if( !success || _imageCenter == null || _wrapCenter == null ) + { + return; + } + + try + { + switch( type ) + { + case 0: + var img = Image.LoadPixelData< Rgba32 >( _imageCenter, _wrapCenter.Width, _wrapCenter.Height ); + img.Save( path, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression } ); + break; + case 1: + if( TextureImporter.RgbaBytesToTex( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var tex ) ) + { + File.WriteAllBytes( path, tex ); + } + + break; + case 2: + if( TextureImporter.RgbaBytesToDds( _imageCenter, _wrapCenter.Width, _wrapCenter.Height, out var dds ) ) + { + File.WriteAllBytes( path, dds ); + } + + break; + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save image to {path}:\n{e}" ); + } + } + + private void SaveAsPng( bool success, string path ) + => SaveAs( success, path, 0 ); + + private void SaveAsTex( bool success, string path ) + => SaveAs( success, path, 1 ); + + private void SaveAsDds( bool success, string path ) + => SaveAs( success, path, 2 ); + private void DrawTextureTab() { + _dialogManager.Draw(); + using var tab = ImRaii.TabItem( "Texture Import/Export" ); if( !tab ) { @@ -305,44 +407,72 @@ public partial class ModEditWindow var imageSize = new Vector2( leftRightWidth.X - ImGui.GetStyle().FramePadding.X * 2 ); using( var child = ImRaii.Child( "ImageLeft", leftRightWidth, true ) ) { - if( PathInputBox( "##ImageLeft", "Import Image...", string.Empty, ref _pathLeft ) ) + if( child ) { - UpdateImage( _pathLeft, ref _imageLeft, ref _wrapLeft ); + PathInputBox( "##ImageLeft", "Import Image...", string.Empty, 0 ); + + ImGui.NewLine(); + if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) ) + { + UpdateCenter(); + } + + ImGui.NewLine(); + ScaledImage( _pathLeft, _wrapLeft, imageSize ); } - - ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) ) - { - UpdateCenter(); - } - - ImGui.NewLine(); - ScaledImage( _wrapLeft, imageSize ); - } ImGui.SameLine(); using( var child = ImRaii.Child( "ImageMix", leftRightWidth, true ) ) { - ScaledImage( _wrapCenter, imageSize ); + if( child ) + { + if( _wrapCenter == null && _wrapLeft != null && _wrapRight != null ) + { + ImGui.TextUnformatted( "Images have incompatible resolutions." ); + } + else if( _wrapCenter != null ) + { + if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) ) + { + var fileName = Path.GetFileNameWithoutExtension( _pathLeft.Length > 0 ? _pathLeft : _pathRight ); + _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", SaveAsTex, _mod!.ModPath.FullName ); + } + + if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) ) + { + var fileName = Path.GetFileNameWithoutExtension( _pathRight.Length > 0 ? _pathRight : _pathLeft ); + _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", SaveAsPng, _mod!.ModPath.FullName ); + } + + if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) ) + { + var fileName = Path.GetFileNameWithoutExtension( _pathRight.Length > 0 ? _pathRight : _pathLeft ); + _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", SaveAsDds, _mod!.ModPath.FullName ); + } + + ImGui.NewLine(); + ScaledImage( string.Empty, _wrapCenter, imageSize ); + } + } } ImGui.SameLine(); using( var child = ImRaii.Child( "ImageRight", leftRightWidth, true ) ) { - if( PathInputBox( "##ImageRight", "Import Image...", string.Empty, ref _pathRight ) ) + if( child ) { - UpdateImage( _pathRight, ref _imageRight, ref _wrapRight ); - } + PathInputBox( "##ImageRight", "Import Image...", string.Empty, 1 ); - ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) ) - { - UpdateCenter(); - } + ImGui.NewLine(); + if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) ) + { + UpdateCenter(); + } - ImGui.NewLine(); - ScaledImage( _wrapRight, imageSize ); + ImGui.NewLine(); + ScaledImage( _pathRight, _wrapRight, imageSize ); + } } } } \ No newline at end of file From f6772af246ec7528bc7af26bc574b4df76407e03 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 13 Jun 2022 22:26:36 +0200 Subject: [PATCH 17/22] Prevent a weird case of null crash. --- Penumbra/Interop/Resolver/PathResolver.Material.cs | 3 ++- Penumbra/Interop/Resolver/PathResolver.cs | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 400d0a4c..160820e3 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; @@ -55,7 +56,7 @@ public unsafe partial class PathResolver } // Check specifically for shpk and tex files whether we are currently in a material load. - private bool HandleMaterialSubFiles( ResourceType type, out ModCollection? collection ) + private bool HandleMaterialSubFiles( ResourceType type, [NotNullWhen(true)] out ModCollection? collection ) { if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk ) { diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 77f413f6..c634755a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -40,16 +41,15 @@ public partial class PathResolver : IDisposable // A potential next request will add the path anew. var nonDefault = HandleMaterialSubFiles( type, out var collection ) || PathCollections.TryRemove( gamePath.Path, out collection ) - //|| HandlePapFile( type, gamePath, out collection ) || HandleAnimationFile( type, gamePath, out collection ) || HandleDecalFile( type, gamePath, out collection ); - if( !nonDefault ) + if( !nonDefault || collection == null) { collection = Penumbra.CollectionManager.Default; } // Resolve using character/default collection first, otherwise forced, as usual. - var resolved = collection!.ResolvePath( gamePath ); + var resolved = collection.ResolvePath( gamePath ); // Since mtrl files load their files separately, we need to add the new, resolved path // so that the functions loading tex and shpk can find that path and use its collection. @@ -59,7 +59,7 @@ public partial class PathResolver : IDisposable return true; } - private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ModCollection? collection ) + private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen(true)] out ModCollection? collection ) { if( type == ResourceType.Tex && _lastCreatedCollection != null @@ -73,7 +73,7 @@ public partial class PathResolver : IDisposable return false; } - private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, out ModCollection? collection ) + private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen(true)] out ModCollection? collection ) { if( _animationLoadCollection != null ) { From cc9f8cc84e8204e99514c804e835295b16e4a3f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jun 2022 18:03:42 +0200 Subject: [PATCH 18/22] Small cleanup. --- Penumbra/Interop/CharacterUtility.cs | 2 +- Penumbra/Interop/Resolver/PathResolver.Material.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/CharacterUtility.cs b/Penumbra/Interop/CharacterUtility.cs index d16b3094..1c26ebb8 100644 --- a/Penumbra/Interop/CharacterUtility.cs +++ b/Penumbra/Interop/CharacterUtility.cs @@ -54,7 +54,7 @@ public unsafe class CharacterUtility : IDisposable .Select( i => Array.IndexOf( RelevantIndices, i ) ).ToArray(); - public (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[RelevantIndices.Length]; + public readonly (IntPtr Address, int Size)[] DefaultResources = new (IntPtr, int)[RelevantIndices.Length]; public (IntPtr Address, int Size) DefaultResource( int fullIdx ) => DefaultResources[ ReverseIndices[ fullIdx ] ]; diff --git a/Penumbra/Interop/Resolver/PathResolver.Material.cs b/Penumbra/Interop/Resolver/PathResolver.Material.cs index 160820e3..e3f1ab56 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Material.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Material.cs @@ -83,12 +83,16 @@ public unsafe partial class PathResolver var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); if( Penumbra.CollectionManager.ByName( name, out var collection ) ) { +#if DEBUG PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path ); +#endif SetCollection( path, collection ); } else { +#if DEBUG PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path ); +#endif } // Force isSync = true for this call. I don't really understand why, From 5f8eac0ec162dab6f042ec5507fe1b348479eb45 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Jun 2022 12:28:31 +0200 Subject: [PATCH 19/22] More parsing, mostly untested. --- Penumbra/Import/Dds/DXT10Header.cs | 322 ++++++++++ Penumbra/Import/Dds/DdsFile.cs | 253 ++++++++ Penumbra/Import/Dds/DdsHeader.cs | 109 ++++ Penumbra/Import/Dds/ImageParsing.cs | 580 ++++++++++++++++++ Penumbra/Import/Dds/PixelFormat.cs | 134 ++++ Penumbra/Import/Dds/TextureImporter.cs | 103 ++++ Penumbra/Import/Textures/TextureImporter.cs | 553 ----------------- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 111 ++-- 8 files changed, 1573 insertions(+), 592 deletions(-) create mode 100644 Penumbra/Import/Dds/DXT10Header.cs create mode 100644 Penumbra/Import/Dds/DdsFile.cs create mode 100644 Penumbra/Import/Dds/DdsHeader.cs create mode 100644 Penumbra/Import/Dds/ImageParsing.cs create mode 100644 Penumbra/Import/Dds/PixelFormat.cs create mode 100644 Penumbra/Import/Dds/TextureImporter.cs delete mode 100644 Penumbra/Import/Textures/TextureImporter.cs diff --git a/Penumbra/Import/Dds/DXT10Header.cs b/Penumbra/Import/Dds/DXT10Header.cs new file mode 100644 index 00000000..005cf24c --- /dev/null +++ b/Penumbra/Import/Dds/DXT10Header.cs @@ -0,0 +1,322 @@ +using System; +using System.Runtime.InteropServices; + +namespace Penumbra.Import.Dds; + +[StructLayout( LayoutKind.Sequential )] +public struct DXT10Header +{ + public DXGIFormat Format; + public D3DResourceDimension ResourceDimension; + public D3DResourceMiscFlags MiscFlags; + public uint ArraySize; + public D3DAlphaMode AlphaMode; + + public ParseType ToParseType() + { + return Format switch + { + DXGIFormat.BC1Typeless => ParseType.DXT1, + DXGIFormat.BC1UNorm => ParseType.DXT1, + DXGIFormat.BC1UNormSRGB => ParseType.DXT1, + + DXGIFormat.BC2Typeless => ParseType.DXT3, + DXGIFormat.BC2UNorm => ParseType.DXT3, + DXGIFormat.BC2UNormSRGB => ParseType.DXT3, + + DXGIFormat.BC3Typeless => ParseType.DXT5, + DXGIFormat.BC3UNorm => ParseType.DXT5, + DXGIFormat.BC3UNormSRGB => ParseType.DXT5, + + DXGIFormat.BC4Typeless => ParseType.BC4, + DXGIFormat.BC4SNorm => ParseType.BC4, + DXGIFormat.BC4UNorm => ParseType.BC4, + + DXGIFormat.BC5Typeless => ParseType.BC5, + DXGIFormat.BC5SNorm => ParseType.BC5, + DXGIFormat.BC5UNorm => ParseType.BC5, + + DXGIFormat.R8G8B8A8Typeless => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8UNorm => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8UNormSRGB => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8UInt => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8SNorm => ParseType.R8G8B8A8, + DXGIFormat.R8G8B8A8SInt => ParseType.R8G8B8A8, + + DXGIFormat.B8G8R8A8Typeless => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8A8UNorm => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8A8UNormSRGB => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8X8Typeless => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8X8UNorm => ParseType.B8G8R8A8, + DXGIFormat.B8G8R8X8UNormSRGB => ParseType.B8G8R8A8, + + DXGIFormat.B4G4R4A4UNorm => ParseType.B4G4R4A4, + DXGIFormat.B5G5R5A1UNorm => ParseType.B5G5R5A1, + DXGIFormat.B5G6R5UNorm => ParseType.B5G6R5, + + DXGIFormat.BC6HSF16 => ParseType.Unsupported, + DXGIFormat.BC6HTypeless => ParseType.Unsupported, + DXGIFormat.BC6HUF16 => ParseType.Unsupported, + + DXGIFormat.BC7Typeless => ParseType.Unsupported, + DXGIFormat.BC7UNorm => ParseType.Unsupported, + DXGIFormat.BC7UNormSRGB => ParseType.Unsupported, + + DXGIFormat.Unknown => ParseType.Unsupported, + DXGIFormat.R32G32B32A32Typeless => ParseType.Unsupported, + DXGIFormat.R32G32B32A32Float => ParseType.Unsupported, + DXGIFormat.R32G32B32A32UInt => ParseType.Unsupported, + DXGIFormat.R32G32B32A32SInt => ParseType.Unsupported, + DXGIFormat.R32G32B32Typeless => ParseType.Unsupported, + DXGIFormat.R32G32B32Float => ParseType.Unsupported, + DXGIFormat.R32G32B32UInt => ParseType.Unsupported, + DXGIFormat.R32G32B32SInt => ParseType.Unsupported, + DXGIFormat.R16G16B16A16Typeless => ParseType.Unsupported, + DXGIFormat.R16G16B16A16Float => ParseType.Unsupported, + DXGIFormat.R16G16B16A16UNorm => ParseType.Unsupported, + DXGIFormat.R16G16B16A16UInt => ParseType.Unsupported, + DXGIFormat.R16G16B16A16SNorm => ParseType.Unsupported, + DXGIFormat.R16G16B16A16SInt => ParseType.Unsupported, + DXGIFormat.R32G32Typeless => ParseType.Unsupported, + DXGIFormat.R32G32Float => ParseType.Unsupported, + DXGIFormat.R32G32UInt => ParseType.Unsupported, + DXGIFormat.R32G32SInt => ParseType.Unsupported, + DXGIFormat.R32G8X24Typeless => ParseType.Unsupported, + DXGIFormat.D32FloatS8X24UInt => ParseType.Unsupported, + DXGIFormat.R32FloatX8X24Typeless => ParseType.Unsupported, + DXGIFormat.X32TypelessG8X24UInt => ParseType.Unsupported, + DXGIFormat.R10G10B10A2Typeless => ParseType.Unsupported, + DXGIFormat.R10G10B10A2UNorm => ParseType.Unsupported, + DXGIFormat.R10G10B10A2UInt => ParseType.Unsupported, + DXGIFormat.R11G11B10Float => ParseType.Unsupported, + DXGIFormat.R16G16Typeless => ParseType.Unsupported, + DXGIFormat.R16G16Float => ParseType.Unsupported, + DXGIFormat.R16G16UNorm => ParseType.Unsupported, + DXGIFormat.R16G16UInt => ParseType.Unsupported, + DXGIFormat.R16G16SNorm => ParseType.Unsupported, + DXGIFormat.R16G16SInt => ParseType.Unsupported, + DXGIFormat.R32Typeless => ParseType.Unsupported, + DXGIFormat.D32Float => ParseType.Unsupported, + DXGIFormat.R32Float => ParseType.Unsupported, + DXGIFormat.R32UInt => ParseType.Unsupported, + DXGIFormat.R32SInt => ParseType.Unsupported, + DXGIFormat.R24G8Typeless => ParseType.Unsupported, + DXGIFormat.D24UNormS8UInt => ParseType.Unsupported, + DXGIFormat.R24UNormX8Typeless => ParseType.Unsupported, + DXGIFormat.X24TypelessG8UInt => ParseType.Unsupported, + DXGIFormat.R8G8Typeless => ParseType.Unsupported, + DXGIFormat.R8G8UNorm => ParseType.Unsupported, + DXGIFormat.R8G8UInt => ParseType.Unsupported, + DXGIFormat.R8G8SNorm => ParseType.Unsupported, + DXGIFormat.R8G8SInt => ParseType.Unsupported, + DXGIFormat.R16Typeless => ParseType.Unsupported, + DXGIFormat.R16Float => ParseType.Unsupported, + DXGIFormat.D16UNorm => ParseType.Unsupported, + DXGIFormat.R16UNorm => ParseType.Unsupported, + DXGIFormat.R16UInt => ParseType.Unsupported, + DXGIFormat.R16SNorm => ParseType.Unsupported, + DXGIFormat.R16SInt => ParseType.Unsupported, + DXGIFormat.R8Typeless => ParseType.Unsupported, + DXGIFormat.R8UNorm => ParseType.Unsupported, + DXGIFormat.R8UInt => ParseType.Unsupported, + DXGIFormat.R8SNorm => ParseType.Unsupported, + DXGIFormat.R8SInt => ParseType.Unsupported, + DXGIFormat.A8UNorm => ParseType.Unsupported, + DXGIFormat.R1UNorm => ParseType.Unsupported, + DXGIFormat.R9G9B9E5SharedEXP => ParseType.Unsupported, + DXGIFormat.R8G8B8G8UNorm => ParseType.Unsupported, + DXGIFormat.G8R8G8B8UNorm => ParseType.Unsupported, + DXGIFormat.R10G10B10XRBiasA2UNorm => ParseType.Unsupported, + DXGIFormat.AYUV => ParseType.Unsupported, + DXGIFormat.Y410 => ParseType.Unsupported, + DXGIFormat.Y416 => ParseType.Unsupported, + DXGIFormat.NV12 => ParseType.Unsupported, + DXGIFormat.P010 => ParseType.Unsupported, + DXGIFormat.P016 => ParseType.Unsupported, + DXGIFormat.F420Opaque => ParseType.Unsupported, + DXGIFormat.YUY2 => ParseType.Unsupported, + DXGIFormat.Y210 => ParseType.Unsupported, + DXGIFormat.Y216 => ParseType.Unsupported, + DXGIFormat.NV11 => ParseType.Unsupported, + DXGIFormat.AI44 => ParseType.Unsupported, + DXGIFormat.IA44 => ParseType.Unsupported, + DXGIFormat.P8 => ParseType.Unsupported, + DXGIFormat.A8P8 => ParseType.Unsupported, + DXGIFormat.P208 => ParseType.Unsupported, + DXGIFormat.V208 => ParseType.Unsupported, + DXGIFormat.V408 => ParseType.Unsupported, + DXGIFormat.SamplerFeedbackMinMipOpaque => ParseType.Unsupported, + DXGIFormat.SamplerFeedbackMipRegionUsedOpaque => ParseType.Unsupported, + DXGIFormat.ForceUInt => ParseType.Unsupported, + _ => ParseType.Unsupported, + }; + } + + public enum DXGIFormat : uint + { + Unknown = 0, + R32G32B32A32Typeless = 1, + R32G32B32A32Float = 2, + R32G32B32A32UInt = 3, + R32G32B32A32SInt = 4, + R32G32B32Typeless = 5, + R32G32B32Float = 6, + R32G32B32UInt = 7, + R32G32B32SInt = 8, + R16G16B16A16Typeless = 9, + R16G16B16A16Float = 10, + R16G16B16A16UNorm = 11, + R16G16B16A16UInt = 12, + R16G16B16A16SNorm = 13, + R16G16B16A16SInt = 14, + R32G32Typeless = 15, + R32G32Float = 16, + R32G32UInt = 17, + R32G32SInt = 18, + R32G8X24Typeless = 19, + D32FloatS8X24UInt = 20, + R32FloatX8X24Typeless = 21, + X32TypelessG8X24UInt = 22, + R10G10B10A2Typeless = 23, + R10G10B10A2UNorm = 24, + R10G10B10A2UInt = 25, + R11G11B10Float = 26, + R8G8B8A8Typeless = 27, + R8G8B8A8UNorm = 28, + R8G8B8A8UNormSRGB = 29, + R8G8B8A8UInt = 30, + R8G8B8A8SNorm = 31, + R8G8B8A8SInt = 32, + R16G16Typeless = 33, + R16G16Float = 34, + R16G16UNorm = 35, + R16G16UInt = 36, + R16G16SNorm = 37, + R16G16SInt = 38, + R32Typeless = 39, + D32Float = 40, + R32Float = 41, + R32UInt = 42, + R32SInt = 43, + R24G8Typeless = 44, + D24UNormS8UInt = 45, + R24UNormX8Typeless = 46, + X24TypelessG8UInt = 47, + R8G8Typeless = 48, + R8G8UNorm = 49, + R8G8UInt = 50, + R8G8SNorm = 51, + R8G8SInt = 52, + R16Typeless = 53, + R16Float = 54, + D16UNorm = 55, + R16UNorm = 56, + R16UInt = 57, + R16SNorm = 58, + R16SInt = 59, + R8Typeless = 60, + R8UNorm = 61, + R8UInt = 62, + R8SNorm = 63, + R8SInt = 64, + A8UNorm = 65, + R1UNorm = 66, + R9G9B9E5SharedEXP = 67, + R8G8B8G8UNorm = 68, + G8R8G8B8UNorm = 69, + BC1Typeless = 70, + BC1UNorm = 71, + BC1UNormSRGB = 72, + BC2Typeless = 73, + BC2UNorm = 74, + BC2UNormSRGB = 75, + BC3Typeless = 76, + BC3UNorm = 77, + BC3UNormSRGB = 78, + BC4Typeless = 79, + BC4UNorm = 80, + BC4SNorm = 81, + BC5Typeless = 82, + BC5UNorm = 83, + BC5SNorm = 84, + B5G6R5UNorm = 85, + B5G5R5A1UNorm = 86, + B8G8R8A8UNorm = 87, + B8G8R8X8UNorm = 88, + R10G10B10XRBiasA2UNorm = 89, + B8G8R8A8Typeless = 90, + B8G8R8A8UNormSRGB = 91, + B8G8R8X8Typeless = 92, + B8G8R8X8UNormSRGB = 93, + BC6HTypeless = 94, + BC6HUF16 = 95, + BC6HSF16 = 96, + BC7Typeless = 97, + BC7UNorm = 98, + BC7UNormSRGB = 99, + AYUV = 100, + Y410 = 101, + Y416 = 102, + NV12 = 103, + P010 = 104, + P016 = 105, + F420Opaque = 106, + YUY2 = 107, + Y210 = 108, + Y216 = 109, + NV11 = 110, + AI44 = 111, + IA44 = 112, + P8 = 113, + A8P8 = 114, + B4G4R4A4UNorm = 115, + P208 = 130, + V208 = 131, + V408 = 132, + SamplerFeedbackMinMipOpaque, + SamplerFeedbackMipRegionUsedOpaque, + ForceUInt = 0xffffffff, + } + + public enum D3DResourceDimension : int + { + Unknown = 0, + Buffer = 1, + Texture1D = 2, + Texture2D = 3, + Texture3D = 4, + } + + [Flags] + public enum D3DResourceMiscFlags : uint + { + GenerateMips = 0x000001, + Shared = 0x000002, + TextureCube = 0x000004, + DrawIndirectArgs = 0x000010, + BufferAllowRawViews = 0x000020, + BufferStructured = 0x000040, + ResourceClamp = 0x000080, + SharedKeyedMutex = 0x000100, + GDICompatible = 0x000200, + SharedNTHandle = 0x000800, + RestrictedContent = 0x001000, + RestrictSharedResource = 0x002000, + RestrictSharedResourceDriver = 0x004000, + Guarded = 0x008000, + TilePool = 0x020000, + Tiled = 0x040000, + HWProtected = 0x080000, + SharedDisplayable, + SharedExclusiveWriter, + }; + + public enum D3DAlphaMode : int + { + Unknown = 0, + Straight = 1, + Premultiplied = 2, + Opaque = 3, + Custom = 4, + }; +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/DdsFile.cs b/Penumbra/Import/Dds/DdsFile.cs new file mode 100644 index 00000000..1b0f973a --- /dev/null +++ b/Penumbra/Import/Dds/DdsFile.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using Dalamud.Logging; +using Lumina.Data.Files; +using Lumina.Extensions; + +namespace Penumbra.Import.Dds; + +public class DdsFile +{ + public const int DdsIdentifier = 0x20534444; + + public readonly DdsHeader Header; + public readonly DXT10Header? DXT10Header; + private readonly byte[] _data; + + public ReadOnlySpan< byte > Data + => _data; + + public ReadOnlySpan< byte > MipMap( int level ) + { + var mipSize = ParseType switch + { + ParseType.Unsupported => 0, + ParseType.DXT1 => Header.Height * Header.Width / 2, + ParseType.BC4 => Header.Height * Header.Width / 2, + + ParseType.DXT3 => Header.Height * Header.Width, + ParseType.DXT5 => Header.Height * Header.Width, + ParseType.BC5 => Header.Height * Header.Width, + ParseType.Greyscale => Header.Height * Header.Width, + + ParseType.R4G4B4A4 => Header.Height * Header.Width * 2, + ParseType.B4G4R4A4 => Header.Height * Header.Width * 2, + ParseType.R5G5B5 => Header.Height * Header.Width * 2, + ParseType.B5G5R5 => Header.Height * Header.Width * 2, + ParseType.R5G6B5 => Header.Height * Header.Width * 2, + ParseType.B5G6R5 => Header.Height * Header.Width * 2, + ParseType.R5G5B5A1 => Header.Height * Header.Width * 2, + ParseType.B5G5R5A1 => Header.Height * Header.Width * 2, + + ParseType.R8G8B8 => Header.Height * Header.Width * 3, + ParseType.B8G8R8 => Header.Height * Header.Width * 3, + + ParseType.R8G8B8A8 => Header.Height * Header.Width * 4, + ParseType.B8G8R8A8 => Header.Height * Header.Width * 4, + _ => throw new ArgumentOutOfRangeException( nameof( ParseType ), ParseType, null ), + }; + + if( Header.MipMapCount < level ) + { + throw new ArgumentOutOfRangeException( nameof( level ) ); + } + + var sum = 0; + for( var i = 0; i < level; ++i ) + { + sum += mipSize; + mipSize = Math.Max( 16, mipSize >> 2 ); + } + + + if( _data.Length < sum + mipSize ) + { + throw new Exception( "Not enough data to encode image." ); + } + + return _data.AsSpan( sum, mipSize ); + } + + private byte[]? _rgbaData; + public readonly ParseType ParseType; + + public ReadOnlySpan< byte > RgbaData + => _rgbaData ??= ParseToRgba(); + + private DdsFile( ParseType type, DdsHeader header, byte[] data, DXT10Header? dxt10Header = null ) + { + ParseType = type; + Header = header; + DXT10Header = dxt10Header; + _data = data; + } + + private byte[] ParseToRgba() + { + return ParseType switch + { + ParseType.Unsupported => Array.Empty< byte >(), + ParseType.DXT1 => ImageParsing.DecodeDxt1( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.DXT3 => ImageParsing.DecodeDxt3( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.DXT5 => ImageParsing.DecodeDxt5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.BC4 => ImageParsing.DecodeBc4( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.BC5 => ImageParsing.DecodeBc5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.Greyscale => ImageParsing.DecodeUncompressedGreyscale( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R4G4B4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B4G4R4A4 => ImageParsing.DecodeUncompressedB4G4R4A4( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R5G5B5 => ImageParsing.DecodeUncompressedR5G5B5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B5G5R5 => ImageParsing.DecodeUncompressedB5G5R5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R5G6B5 => ImageParsing.DecodeUncompressedR5G6B5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B5G6R5 => ImageParsing.DecodeUncompressedB5G6R5( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R5G5B5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B5G5R5A1 => ImageParsing.DecodeUncompressedB5G5R5A1( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R8G8B8 => ImageParsing.DecodeUncompressedR8G8B8( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.B8G8R8 => ImageParsing.DecodeUncompressedB8G8R8( MipMap( 0 ), Header.Height, Header.Width ), + ParseType.R8G8B8A8 => _data.Length == Header.Width * Header.Height * 4 ? _data : _data[ ..( Header.Width * Header.Height * 4 ) ], + ParseType.B8G8R8A8 => ImageParsing.DecodeUncompressedB8G8R8A8( MipMap( 0 ), Header.Height, Header.Width ), + _ => throw new ArgumentOutOfRangeException(), + }; + } + + public static bool Load( Stream data, [NotNullWhen( true )] out DdsFile? file ) + { + file = null; + try + { + using var br = new BinaryReader( data ); + if( br.ReadUInt32() != DdsIdentifier ) + { + return false; + } + + var header = br.ReadStructure< DdsHeader >(); + var dxt10 = header.PixelFormat.FourCC == PixelFormat.FourCCType.DX10 ? ( DXT10Header? )br.ReadStructure< DXT10Header >() : null; + var type = header.PixelFormat.ToParseType( dxt10 ); + + file = new DdsFile( type, header, br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ), dxt10 ); + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load DDS file:\n{e}" ); + return false; + } + } + + public bool ConvertToTex( out byte[] texBytes ) + { + using var mem = new MemoryStream( _data.Length * 2 ); + using( var bw = new BinaryWriter( mem ) ) + { + var (format, mipLength) = WriteTexHeader( bw ); + var bytes = format == TexFile.TextureFormat.R8G8B8X8 ? RgbaData : _data; + + if( bytes.Length < mipLength ) + { + throw new Exception( "Broken file. Not enough data." ); + } + + bw.Write( _data.AsSpan( 0, mipLength ) ); + } + + texBytes = mem.ToArray(); + return true; + } + + private (TexFile.TextureFormat, int) WriteTexHeader( BinaryWriter bw ) + { + var (format, mipLength) = ConvertFormat( ParseType, Header.Height, Header.Width ); + if( mipLength == 0 ) + { + throw new Exception( "Invalid format to convert to tex." ); + } + + var mipCount = Header.MipMapCount; + if( format == TexFile.TextureFormat.R8G8B8X8 && ParseType != ParseType.R8G8B8A8 ) + { + mipCount = 1; + } + + bw.Write( ( uint )TexFile.Attribute.TextureType2D ); + bw.Write( ( uint )format ); + bw.Write( ( ushort )Header.Width ); + bw.Write( ( ushort )Header.Height ); + bw.Write( ( ushort )Header.Depth ); + bw.Write( ( ushort )mipCount ); + bw.Write( 0 ); + bw.Write( 1 ); + bw.Write( 2 ); + + var offset = 80; + var mipLengthSum = 0; + for( var i = 0; i < mipCount; ++i ) + { + bw.Write( offset ); + offset += mipLength; + mipLengthSum += mipLength; + mipLength = Math.Max( 16, mipLength >> 2 ); + } + + for( var i = mipCount; i < 13; ++i ) + { + bw.Write( 0 ); + } + + return ( format, mipLengthSum ); + } + + public static (TexFile.TextureFormat, int) ConvertFormat( ParseType type, int height, int width ) + { + return type switch + { + ParseType.Unsupported => ( TexFile.TextureFormat.Unknown, 0 ), + ParseType.DXT1 => ( TexFile.TextureFormat.DXT1, height * width / 2 ), + ParseType.DXT3 => ( TexFile.TextureFormat.DXT3, height * width * 2 ), + ParseType.DXT5 => ( TexFile.TextureFormat.DXT5, height * width * 2 ), + ParseType.BC4 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.BC5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.Greyscale => ( TexFile.TextureFormat.A8, height * width ), + ParseType.R4G4B4A4 => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ), + ParseType.B4G4R4A4 => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ), + ParseType.R5G5B5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.B5G5R5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.R5G6B5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.B5G6R5 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.R5G5B5A1 => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ), + ParseType.B5G5R5A1 => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ), + ParseType.R8G8B8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.B8G8R8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.R8G8B8A8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + ParseType.B8G8R8A8 => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), + _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), + }; + } +} + +public class TmpTexFile +{ + public TexFile.TexHeader Header; + public byte[] RgbaData; + + public void Load( BinaryReader br ) + { + Header = br.ReadStructure< TexFile.TexHeader >(); + var data = br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ); + RgbaData = Header.Format switch + { + TexFile.TextureFormat.L8 => ImageParsing.DecodeUncompressedGreyscale( data, Header.Height, Header.Width ), + TexFile.TextureFormat.A8 => ImageParsing.DecodeUncompressedGreyscale( data, Header.Height, Header.Width ), + TexFile.TextureFormat.DXT1 => ImageParsing.DecodeDxt1( data, Header.Height, Header.Width ), + TexFile.TextureFormat.DXT3 => ImageParsing.DecodeDxt3( data, Header.Height, Header.Width ), + TexFile.TextureFormat.DXT5 => ImageParsing.DecodeDxt5( data, Header.Height, Header.Width ), + TexFile.TextureFormat.A8R8G8B8 => ImageParsing.DecodeUncompressedB8G8R8A8( data, Header.Height, Header.Width ), + TexFile.TextureFormat.R8G8B8X8 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ), + TexFile.TextureFormat.A8R8G8B82 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ), + TexFile.TextureFormat.R4G4B4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( data, Header.Height, Header.Width ), + TexFile.TextureFormat.R5G5B5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( data, Header.Height, Header.Width ), + _ => throw new ArgumentOutOfRangeException(), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/DdsHeader.cs b/Penumbra/Import/Dds/DdsHeader.cs new file mode 100644 index 00000000..30f976d4 --- /dev/null +++ b/Penumbra/Import/Dds/DdsHeader.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Penumbra.Import.Dds; + +[StructLayout( LayoutKind.Sequential )] +public struct DdsHeader +{ + public const int Size = 124; + public const uint MagicNumber = 'D' | ( 'D' << 8 ) | ( 'S' << 16 ) | ( ' ' << 24 ); + + private int _size; + public DdsFlags Flags; + public int Height; + public int Width; + public int PitchOrLinearSize; + public int Depth; + public int MipMapCount; + public int Reserved1; + public int Reserved2; + public int Reserved3; + public int Reserved4; + public int Reserved5; + public int Reserved6; + public int Reserved7; + public int Reserved8; + public int Reserved9; + public int ReservedA; + public int ReservedB; + public PixelFormat PixelFormat; + public DdsCaps1 Caps1; + public DdsCaps2 Caps2; + public uint Caps3; + public uint Caps4; + public int ReservedC; + + [Flags] + public enum DdsFlags : uint + { + Caps = 0x00000001, + Height = 0x00000002, + Width = 0x00000004, + Pitch = 0x00000008, + PixelFormat = 0x00001000, + MipMapCount = 0x00020000, + LinearSize = 0x00080000, + Depth = 0x00800000, + + Required = Caps | Height | Width | PixelFormat, + } + + [Flags] + public enum DdsCaps1 : uint + { + Complex = 0x08, + MipMap = 0x400000, + Texture = 0x1000, + } + + [Flags] + public enum DdsCaps2 : uint + { + CubeMap = 0x200, + CubeMapPositiveEX = 0x400, + CubeMapNegativeEX = 0x800, + CubeMapPositiveEY = 0x1000, + CubeMapNegativeEY = 0x2000, + CubeMapPositiveEZ = 0x4000, + CubeMapNegativeEZ = 0x8000, + Volume = 0x200000, + } + + public void Write( BinaryWriter bw ) + { + bw.Write( MagicNumber ); + bw.Write( Size ); + bw.Write( ( uint )Flags ); + bw.Write( Height ); + bw.Write( Width ); + bw.Write( PitchOrLinearSize ); + bw.Write( Depth ); + bw.Write( MipMapCount ); + bw.Write( Reserved1 ); + bw.Write( Reserved2 ); + bw.Write( Reserved3 ); + bw.Write( Reserved4 ); + bw.Write( Reserved5 ); + bw.Write( Reserved6 ); + bw.Write( Reserved7 ); + bw.Write( Reserved8 ); + bw.Write( Reserved9 ); + bw.Write( ReservedA ); + bw.Write( ReservedB ); + PixelFormat.Write( bw ); + bw.Write( ( uint )Caps1 ); + bw.Write( ( uint )Caps2 ); + bw.Write( Caps3 ); + bw.Write( Caps4 ); + bw.Write( ReservedC ); + } + + public void Write( byte[] bytes, int offset ) + { + using var m = new MemoryStream( bytes, offset, bytes.Length - offset ); + using var bw = new BinaryWriter( m ); + Write( bw ); + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/ImageParsing.cs b/Penumbra/Import/Dds/ImageParsing.cs new file mode 100644 index 00000000..c3bdd482 --- /dev/null +++ b/Penumbra/Import/Dds/ImageParsing.cs @@ -0,0 +1,580 @@ +using System; +using System.Runtime.CompilerServices; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Dds; + +public static partial class ImageParsing +{ + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static Rgba32 Get565Color( ushort c ) + { + var ret = new Rgba32 + { + R = ( byte )( c & 0x1F ), + G = ( byte )( ( c >> 5 ) & 0x3F ), + B = ( byte )( c >> 11 ), + A = 0xFF, + }; + + ret.R = ( byte )( ( ret.R << 3 ) | ( ret.R >> 2 ) ); + ret.G = ( byte )( ( ret.G << 2 ) | ( ret.G >> 3 ) ); + ret.B = ( byte )( ( ret.B << 3 ) | ( ret.B >> 2 ) ); + + return ret; + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static (Rgba32, Rgba32) GetDxt1CombinedColors( bool c1Larger, Rgba32 c1, Rgba32 c2 ) + { + if( c1Larger ) + { + static byte C( byte a1, byte a2 ) + => ( byte )( ( 2 * a1 + a2 ) / 3 ); + + return ( new Rgba32( C( c1.R, c2.R ), C( c1.G, c2.G ), C( c1.B, c2.B ) ), + new Rgba32( C( c2.R, c1.R ), C( c2.G, c1.G ), C( c2.B, c1.B ) ) ); + } + else + { + static byte C( byte a1, byte a2 ) + => ( byte )( ( a1 + a2 ) / 2 ); + + return ( new Rgba32( C( c1.R, c2.R ), C( c1.G, c2.G ), C( c1.B, c2.B ) ), + new Rgba32( 0, 0, 0, 0 ) ); + } + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static unsafe byte* CopyBytes( byte* ptr, Rgba32 color, byte alpha ) + { + *ptr++ = color.R; + *ptr++ = color.G; + *ptr++ = color.B; + *ptr++ = alpha; + return ptr; + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static unsafe byte* CopyBytes( byte* ptr, Rgba32 color ) + => CopyBytes( ptr, color, color.A ); + + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static unsafe byte* CopyBytesAlphaDown( byte* ptr, Rgba32 color, byte alpha ) + => CopyBytes( ptr, color, ( byte )( ( alpha & 0x0F ) | ( alpha << 4 ) ) ); + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static unsafe byte* CopyBytesAlphaUp( byte* ptr, Rgba32 color, byte alpha ) + => CopyBytes( ptr, color, ( byte )( ( alpha & 0xF0 ) | ( alpha >> 4 ) ) ); + + private static void Verify( ReadOnlySpan< byte > data, int height, int width, int blockSize, int bytes ) + { + if( data.Length % bytes != 0 ) + { + throw new ArgumentException( $"Length {data.Length} not a multiple of {bytes} bytes.", nameof( data ) ); + } + + if( height * width > data.Length * blockSize * blockSize / bytes ) + { + throw new ArgumentException( $"Not enough data encoded in {data.Length} to fill image of dimensions {height} * {width}.", + nameof( data ) ); + } + + if( height % blockSize != 0 ) + { + throw new ArgumentException( $"Height must be a multiple of {blockSize}.", nameof( height ) ); + } + + if( width % blockSize != 0 ) + { + throw new ArgumentException( $"Height must be a multiple of {blockSize}.", nameof( height ) ); + } + } + + private static unsafe byte* GetDxt1Colors( byte* ptr, Span< Rgba32 > colors ) + { + var c1 = ( ushort )( *ptr | ( ptr[ 1 ] << 8 ) ); + var c2 = ( ushort )( ptr[ 2 ] | ( ptr[ 3 ] << 8 ) ); + colors[ 0 ] = Get565Color( c1 ); + colors[ 1 ] = Get565Color( c2 ); + ( colors[ 2 ], colors[ 3 ] ) = GetDxt1CombinedColors( c1 > c2, colors[ 0 ], colors[ 1 ] ); + return ptr + 4; + } + + public static unsafe byte[] DecodeDxt1( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 8 ); + + var ret = new byte[data.Length * 8]; + Span< Rgba32 > colors = stackalloc Rgba32[4]; + + fixed( byte* r = ret, d = data ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + inputPtr = GetDxt1Colors( inputPtr, colors ); + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + var colorMask = *inputPtr++; + outputPtr2 = CopyBytes( outputPtr2, colors[ colorMask & 0b11 ] ); + outputPtr2 = CopyBytes( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ] ); + outputPtr2 = CopyBytes( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ] ); + CopyBytes( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ] ); + } + } + } + } + + return ret; + } + + public static unsafe byte[] DecodeDxt3( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 16 ); + var ret = new byte[data.Length * 4]; + Span< Rgba32 > colors = stackalloc Rgba32[4]; + + fixed( byte* r = ret, d = data ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + var alphaPtr = inputPtr; + inputPtr = GetDxt1Colors( inputPtr + 8, colors ); + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + var colorMask = *inputPtr++; + outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ colorMask & 0b11 ], *alphaPtr ); + outputPtr2 = CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ], *alphaPtr++ ); + outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ], *alphaPtr ); + CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ], *alphaPtr++ ); + } + } + } + } + + return ret; + } + + private static unsafe byte* Dxt5AlphaTable( byte* ptr, Span< byte > alphaValues ) + { + var alphaLookup = stackalloc byte[8]; + alphaLookup[ 0 ] = *ptr++; + alphaLookup[ 1 ] = *ptr++; + if( alphaLookup[ 0 ] > alphaLookup[ 1 ] ) + { + alphaLookup[ 2 ] = ( byte )( ( 6 * alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 3 ] = ( byte )( ( 5 * alphaLookup[ 0 ] + 2 * alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 4 ] = ( byte )( ( 4 * alphaLookup[ 0 ] + 3 * alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 5 ] = ( byte )( ( 3 * alphaLookup[ 0 ] + 4 * alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 6 ] = ( byte )( ( 2 * alphaLookup[ 0 ] + 5 * alphaLookup[ 1 ] ) / 7 ); + alphaLookup[ 7 ] = ( byte )( ( alphaLookup[ 0 ] + 6 * alphaLookup[ 1 ] ) / 7 ); + } + else + { + alphaLookup[ 2 ] = ( byte )( ( 4 * alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 5 ); + alphaLookup[ 3 ] = ( byte )( ( 3 * alphaLookup[ 0 ] + 3 * alphaLookup[ 1 ] ) / 5 ); + alphaLookup[ 4 ] = ( byte )( ( 2 * alphaLookup[ 0 ] + 2 * alphaLookup[ 1 ] ) / 5 ); + alphaLookup[ 5 ] = ( byte )( ( alphaLookup[ 0 ] + alphaLookup[ 1 ] ) / 5 ); + alphaLookup[ 6 ] = byte.MinValue; + alphaLookup[ 7 ] = byte.MaxValue; + } + + var alphaLong = ( ulong )*ptr++; + alphaLong |= ( ulong )*ptr++ << 8; + alphaLong |= ( ulong )*ptr++ << 16; + alphaLong |= ( ulong )*ptr++ << 24; + alphaLong |= ( ulong )*ptr++ << 32; + alphaLong |= ( ulong )*ptr++ << 40; + + for( var i = 0; i < 16; ++i ) + { + alphaValues[ i ] = alphaLookup[ ( alphaLong >> ( i * 3 ) ) & 0x07 ]; + } + + return ptr; + } + + public static unsafe byte[] DecodeDxt5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 16 ); + var ret = new byte[data.Length * 4]; + Span< Rgba32 > colors = stackalloc Rgba32[4]; + Span< byte > alphaValues = stackalloc byte[16]; + + fixed( byte* r = ret, d = data, a = alphaValues ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + inputPtr = Dxt5AlphaTable( inputPtr, alphaValues ); + inputPtr = GetDxt1Colors( inputPtr, colors ); + var alphaPtr = a; + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + var colorMask = *inputPtr++; + outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ colorMask & 0b11 ], *alphaPtr++ ); + outputPtr2 = CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 2 ) & 0b11 ], *alphaPtr++ ); + outputPtr2 = CopyBytesAlphaDown( outputPtr2, colors[ ( colorMask >> 4 ) & 0b11 ], *alphaPtr++ ); + CopyBytesAlphaUp( outputPtr2, colors[ ( colorMask >> 6 ) & 0b11 ], *alphaPtr++ ); + } + } + } + } + + return ret; + } + + public static unsafe byte[] DecodeBc4( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 8 ); + var ret = new byte[data.Length * 8]; + Span< byte > channelValues = stackalloc byte[16]; + + fixed( byte* r = ret, d = data, a = channelValues ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + inputPtr = Dxt5AlphaTable( inputPtr, channelValues ); + var channelPtr = a; + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) ); + CopyBytes( outputPtr2, new Rgba32( *channelPtr, *channelPtr, *channelPtr++, 0xFF ) ); + } + } + } + } + + return ret; + } + + public static unsafe byte[] DecodeBc5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 4, 16 ); + var ret = new byte[data.Length * 4]; + Span< byte > channel1 = stackalloc byte[16]; + Span< byte > channel2 = stackalloc byte[16]; + + fixed( byte* r = ret, d = data, a = channel1, b = channel2 ) + { + var inputPtr = d; + for( var y = 0; y < height; y += 4 ) + { + var outputPtr = r + y * width * 4; + for( var x = 0; x < width; x += 4 ) + { + inputPtr = Dxt5AlphaTable( inputPtr, channel1 ); + inputPtr = Dxt5AlphaTable( inputPtr, channel2 ); + var channel1Ptr = a; + var channel2Ptr = b; + for( var j = 0; j < 4; ++j ) + { + var outputPtr2 = outputPtr + 4 * ( x + j * width ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) ); + outputPtr2 = CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) ); + CopyBytes( outputPtr2, new Rgba32( *channel1Ptr++, *channel2Ptr++, 0, 0xFF ) ); + } + } + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedGreyscale( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 1 ); + var ret = new byte[data.Length * 4]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var end = d + data.Length; + var input = d; + while( input != end ) + { + *ptr++ = *input; + *ptr++ = *input; + *ptr++ = *input++; + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR4G4B4A4( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var input = ( ushort* )d; + foreach( var b in data ) + { + *ptr++ = ( byte )( ( b << 4 ) | ( b & 0x0F ) ); + *ptr++ = ( byte )( ( b >> 4 ) | ( b & 0xF0 ) ); + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB4G4R4A4( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + *ptr++ = ( byte )( ( ( b >> 8 ) & 0x0F ) | ( ( b >> 4 ) & 0xF0 ) ); + *ptr++ = ( byte )( ( b & 0xF0 ) | ( ( b >> 4 ) & 0x0F ) ); + + *ptr++ = ( byte )( ( b & 0x0F ) | ( b << 4 ) ); + *ptr++ = ( byte )( ( ( b >> 8 ) & 0xF0 ) | ( b >> 12 ) ); + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR5G5B5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + var tmp = b & 0x03E0; + *ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) ); + tmp = b & 0x7C00; + *ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) ); + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB5G5R5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + var tmp = b & 0x7C00; + *ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) ); + tmp = b & 0x03E0; + *ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) ); + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR5G6B5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + var tmp = b & 0x07E0; + *ptr++ = ( byte )( ( tmp >> 3 ) | ( tmp >> 9 ) ); + tmp = b & 0xF800; + *ptr++ = ( byte )( ( tmp >> 14 ) | ( tmp >> 9 ) ); + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB5G6R5( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + var tmp = b & 0xF800; + *ptr++ = ( byte )( ( tmp >> 14 ) | ( tmp >> 9 ) ); + tmp = b & 0x07E0; + *ptr++ = ( byte )( ( tmp >> 3 ) | ( tmp >> 9 ) ); + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR5G5B5A1( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + var tmp = b & 0x03E0; + *ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) ); + tmp = b & 0x7C00; + *ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) ); + *ptr++ = 0xFF; + *ptr++ = ( byte )( b > 0x7FFF ? 0xFF : 0x00 ); + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB5G5R5A1( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 2 ); + var ret = new byte[data.Length * 2]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + foreach( var b in new Span< ushort >( d, data.Length / 2 ) ) + { + var tmp = b & 0x7C00; + *ptr++ = ( byte )( ( tmp >> 12 ) | ( tmp >> 7 ) ); + tmp = b & 0x03E0; + *ptr++ = ( byte )( ( tmp >> 2 ) | ( tmp >> 7 ) ); + *ptr++ = ( byte )( ( b & 0x1F ) | ( b << 3 ) ); + *ptr++ = ( byte )( b > 0x7FFF ? 0xFF : 0x00 ); + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedR8G8B8( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 3 ); + var ret = new byte[data.Length * 4 / 3]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var end = d + data.Length; + var input = d; + while( input != end ) + { + *ptr++ = *input++; + *ptr++ = *input++; + *ptr++ = *input++; + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static unsafe byte[] DecodeUncompressedB8G8R8( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 3 ); + var ret = new byte[data.Length * 4 / 3]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var end = d + data.Length; + var input = d; + while( input != end ) + { + var b = *input++; + var g = *input++; + *ptr++ = *input++; + *ptr++ = g; + *ptr++ = b; + *ptr++ = 0xFF; + } + } + + return ret; + } + + public static byte[] DecodeUncompressedR8G8B8A8( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 4 ); + var ret = new byte[data.Length]; + data.CopyTo( ret ); + return ret; + } + + public static unsafe byte[] DecodeUncompressedB8G8R8A8( ReadOnlySpan< byte > data, int height, int width ) + { + Verify( data, height, width, 1, 4 ); + var ret = new byte[data.Length]; + + fixed( byte* r = ret, d = data ) + { + var ptr = r; + var end = d + data.Length; + var input = d; + while( input != end ) + { + var b = *input++; + var g = *input++; + *ptr++ = *input++; + *ptr++ = g; + *ptr++ = b; + *ptr++ = *input++; + } + } + + return ret; + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/PixelFormat.cs b/Penumbra/Import/Dds/PixelFormat.cs new file mode 100644 index 00000000..6c99860e --- /dev/null +++ b/Penumbra/Import/Dds/PixelFormat.cs @@ -0,0 +1,134 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Penumbra.Import.Dds; + +public enum ParseType +{ + Unsupported, + DXT1, + DXT3, + DXT5, + BC4, + BC5, + + Greyscale, + R4G4B4A4, + B4G4R4A4, + R5G5B5, + B5G5R5, + R5G6B5, + B5G6R5, + R5G5B5A1, + B5G5R5A1, + R8G8B8, + B8G8R8, + R8G8B8A8, + B8G8R8A8, +} + +[StructLayout( LayoutKind.Sequential )] +public struct PixelFormat +{ + public int Size; + public FormatFlags Flags; + public FourCCType FourCC; + public int RgbBitCount; + public uint RBitMask; + public uint GBitMask; + public uint BBitMask; + public uint ABitMask; + + + [Flags] + public enum FormatFlags : uint + { + AlphaPixels = 0x000001, + Alpha = 0x000002, + FourCC = 0x000004, + RGB = 0x000040, + YUV = 0x000200, + Luminance = 0x020000, + } + + public enum FourCCType : uint + { + NoCompression = 0, + DXT1 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '1' << 24 ), + DXT2 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '2' << 24 ), + DXT3 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '3' << 24 ), + DXT4 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '4' << 24 ), + DXT5 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '5' << 24 ), + DX10 = 'D' | ( 'X' << 8 ) | ( '1' << 16 ) | ( '0' << 24 ), + ATI1 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '1' << 24 ), + BC4U = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( 'U' << 24 ), + BC45 = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( '5' << 24 ), + ATI2 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '2' << 24 ), + BC5U = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( 'U' << 24 ), + BC55 = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( '5' << 24 ), + } + + + + public void Write( BinaryWriter bw ) + { + bw.Write( Size ); + bw.Write( ( uint )Flags ); + bw.Write( ( uint )FourCC ); + bw.Write( RgbBitCount ); + bw.Write( RBitMask ); + bw.Write( GBitMask ); + bw.Write( BBitMask ); + bw.Write( ABitMask ); + } + + public ParseType ToParseType( DXT10Header? dxt10 ) + { + return FourCC switch + { + FourCCType.NoCompression => HandleUncompressed(), + FourCCType.DXT1 => ParseType.DXT1, + FourCCType.DXT2 => ParseType.Unsupported, + FourCCType.DXT3 => ParseType.DXT3, + FourCCType.DXT4 => ParseType.Unsupported, + FourCCType.DXT5 => ParseType.DXT5, + FourCCType.DX10 => dxt10?.ToParseType() ?? ParseType.Unsupported, + FourCCType.ATI1 => ParseType.BC4, + FourCCType.BC4U => ParseType.BC4, + FourCCType.BC45 => ParseType.BC4, + FourCCType.ATI2 => ParseType.BC5, + FourCCType.BC5U => ParseType.BC5, + FourCCType.BC55 => ParseType.BC5, + _ => ParseType.Unsupported, + }; + } + + private ParseType HandleUncompressed() + { + switch( RgbBitCount ) + { + case 8: return ParseType.Greyscale; + case 16: + if( ABitMask == 0xF000 ) + { + return RBitMask > GBitMask ? ParseType.B4G4R4A4 : ParseType.R4G4B4A4; + } + + if( Flags.HasFlag( FormatFlags.AlphaPixels ) ) + { + return RBitMask > GBitMask ? ParseType.B5G5R5A1 : ParseType.R5G5B5A1; + } + + if( GBitMask == 0x07E0 ) + { + return RBitMask > GBitMask ? ParseType.B5G6R5 : ParseType.R5G6B5; + } + + return RBitMask > GBitMask ? ParseType.B5G5R5 : ParseType.R5G5B5; + case 24: return RBitMask > GBitMask ? ParseType.B8G8R8 : ParseType.R8G8B8; + case 32: return RBitMask > GBitMask ? ParseType.B8G8R8A8 : ParseType.R8G8B8A8; + default: return ParseType.Unsupported; + } + } +} \ No newline at end of file diff --git a/Penumbra/Import/Dds/TextureImporter.cs b/Penumbra/Import/Dds/TextureImporter.cs new file mode 100644 index 00000000..7a28ee7a --- /dev/null +++ b/Penumbra/Import/Dds/TextureImporter.cs @@ -0,0 +1,103 @@ +using System; +using System.IO; +using Lumina.Data.Files; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Dds; + +public class TextureImporter +{ + private static void WriteHeader( byte[] target, int width, int height ) + { + using var mem = new MemoryStream( target ); + using var bw = new BinaryWriter( mem ); + bw.Write( ( uint )TexFile.Attribute.TextureType2D ); + bw.Write( ( uint )TexFile.TextureFormat.A8R8G8B8 ); + bw.Write( ( ushort )width ); + bw.Write( ( ushort )height ); + bw.Write( ( ushort )1 ); + bw.Write( ( ushort )1 ); + bw.Write( 0 ); + bw.Write( 1 ); + bw.Write( 2 ); + bw.Write( 80 ); + for( var i = 1; i < 13; ++i ) + { + bw.Write( 0 ); + } + } + + public static unsafe bool RgbaBytesToDds( byte[] rgba, int width, int height, out byte[] ddsData ) + { + var header = new DdsHeader() + { + Caps1 = DdsHeader.DdsCaps1.Complex | DdsHeader.DdsCaps1.Texture | DdsHeader.DdsCaps1.MipMap, + Depth = 1, + Flags = DdsHeader.DdsFlags.Required | DdsHeader.DdsFlags.Pitch | DdsHeader.DdsFlags.MipMapCount, + Height = height, + Width = width, + PixelFormat = new PixelFormat() + { + Flags = PixelFormat.FormatFlags.AlphaPixels | PixelFormat.FormatFlags.RGB, + FourCC = 0, + BBitMask = 0x000000FF, + GBitMask = 0x0000FF00, + RBitMask = 0x00FF0000, + ABitMask = 0xFF000000, + Size = 32, + RgbBitCount = 32, + }, + }; + ddsData = new byte[4 + DdsHeader.Size + rgba.Length]; + header.Write( ddsData, 0 ); + rgba.CopyTo( ddsData, DdsHeader.Size + 4 ); + for( var i = 0; i < rgba.Length; i += 4 ) + { + ( ddsData[ DdsHeader.Size + i ], ddsData[ DdsHeader.Size + i + 2 ] ) + = ( ddsData[ DdsHeader.Size + i + 2 ], ddsData[ DdsHeader.Size + i ] ); + } + + return true; + } + + public static bool RgbaBytesToTex( byte[] rgba, int width, int height, out byte[] texData ) + { + texData = Array.Empty< byte >(); + if( rgba.Length != width * height * 4 ) + { + return false; + } + + texData = new byte[80 + width * height * 4]; + WriteHeader( texData, width, height ); + // RGBA to BGRA. + for( var i = 0; i < rgba.Length; i += 4 ) + { + texData[ 80 + i + 0 ] = rgba[ i + 2 ]; + texData[ 80 + i + 1 ] = rgba[ i + 1 ]; + texData[ 80 + i + 2 ] = rgba[ i + 0 ]; + texData[ 80 + i + 3 ] = rgba[ i + 3 ]; + } + + return true; + } + + public static bool PngToTex( string inputFile, out byte[] texData ) + { + using var file = File.OpenRead( inputFile ); + var image = Image.Load< Bgra32 >( file ); + + var buffer = new byte[80 + image.Height * image.Width * 4]; + WriteHeader( buffer, image.Width, image.Height ); + + var span = new Span< byte >( buffer, 80, buffer.Length - 80 ); + image.CopyPixelDataTo( span ); + + texData = buffer; + return true; + } + + public void Import( string inputFile ) + { } +} \ No newline at end of file diff --git a/Penumbra/Import/Textures/TextureImporter.cs b/Penumbra/Import/Textures/TextureImporter.cs deleted file mode 100644 index 99708dc8..00000000 --- a/Penumbra/Import/Textures/TextureImporter.cs +++ /dev/null @@ -1,553 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Runtime.InteropServices; -using Dalamud.Logging; -using Lumina.Data.Files; -using Lumina.Extensions; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace Penumbra.Import.Textures; - -[StructLayout( LayoutKind.Sequential )] -public struct PixelFormat -{ - [Flags] - public enum FormatFlags : uint - { - AlphaPixels = 0x000001, - Alpha = 0x000002, - FourCC = 0x000004, - RGB = 0x000040, - YUV = 0x000200, - Luminance = 0x020000, - } - - public enum FourCCType : uint - { - DXT1 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '1' << 24 ), - DXT3 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '3' << 24 ), - DXT5 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '5' << 24 ), - DX10 = 'D' | ( 'X' << 8 ) | ( '1' << 16 ) | ( '0' << 24 ), - } - - public int Size; - public FormatFlags Flags; - public FourCCType FourCC; - public int RgbBitCount; - public uint RBitMask; - public uint GBitMask; - public uint BBitMask; - public uint ABitMask; - - public void Write( BinaryWriter bw ) - { - bw.Write( Size ); - bw.Write( ( uint )Flags ); - bw.Write( ( uint )FourCC ); - bw.Write( RgbBitCount ); - bw.Write( RBitMask ); - bw.Write( GBitMask ); - bw.Write( BBitMask ); - bw.Write( ABitMask ); - } -} - -[StructLayout( LayoutKind.Sequential )] -public struct DdsHeader -{ - [Flags] - public enum DdsFlags : uint - { - Caps = 0x00000001, - Height = 0x00000002, - Width = 0x00000004, - Pitch = 0x00000008, - PixelFormat = 0x00001000, - MipMapCount = 0x00020000, - LinearSize = 0x00080000, - Depth = 0x00800000, - - Required = Caps | Height | Width | PixelFormat, - } - - [Flags] - public enum DdsCaps1 : uint - { - Complex = 0x08, - MipMap = 0x400000, - Texture = 0x1000, - } - - [Flags] - public enum DdsCaps2 : uint - { - CubeMap = 0x200, - CubeMapPositiveEX = 0x400, - CubeMapNegativeEX = 0x800, - CubeMapPositiveEY = 0x1000, - CubeMapNegativeEY = 0x2000, - CubeMapPositiveEZ = 0x4000, - CubeMapNegativeEZ = 0x8000, - Volume = 0x200000, - } - - public const int Size = 124; - private int _size; - public DdsFlags Flags; - public int Height; - public int Width; - public int PitchOrLinearSize; - public int Depth; - public int MipMapCount; - public int Reserved1; - public int Reserved2; - public int Reserved3; - public int Reserved4; - public int Reserved5; - public int Reserved6; - public int Reserved7; - public int Reserved8; - public int Reserved9; - public int ReservedA; - public int ReservedB; - public PixelFormat PixelFormat; - public DdsCaps1 Caps1; - public DdsCaps2 Caps2; - public uint Caps3; - public uint Caps4; - public int ReservedC; - - public void Write( BinaryWriter bw ) - { - bw.Write( ( byte )'D' ); - bw.Write( ( byte )'D' ); - bw.Write( ( byte )'S' ); - bw.Write( ( byte )' ' ); - bw.Write( Size ); - bw.Write( ( uint )Flags ); - bw.Write( Height ); - bw.Write( Width ); - bw.Write( PitchOrLinearSize ); - bw.Write( Depth ); - bw.Write( MipMapCount ); - bw.Write( Reserved1 ); - bw.Write( Reserved2 ); - bw.Write( Reserved3 ); - bw.Write( Reserved4 ); - bw.Write( Reserved5 ); - bw.Write( Reserved6 ); - bw.Write( Reserved7 ); - bw.Write( Reserved8 ); - bw.Write( Reserved9 ); - bw.Write( ReservedA ); - bw.Write( ReservedB ); - PixelFormat.Write( bw ); - bw.Write( ( uint )Caps1 ); - bw.Write( ( uint )Caps2 ); - bw.Write( Caps3 ); - bw.Write( Caps4 ); - bw.Write( ReservedC ); - } - - public void Write( byte[] bytes, int offset ) - { - using var m = new MemoryStream( bytes, offset, bytes.Length - offset ); - using var bw = new BinaryWriter( m ); - Write( bw ); - } -} - -[StructLayout( LayoutKind.Sequential )] -public struct DXT10Header -{ - public enum DXGIFormat : uint - { - Unknown = 0, - R32G32B32A32Typeless = 1, - R32G32B32A32Float = 2, - R32G32B32A32UInt = 3, - R32G32B32A32SInt = 4, - R32G32B32Typeless = 5, - R32G32B32Float = 6, - R32G32B32UInt = 7, - R32G32B32SInt = 8, - R16G16B16A16Typeless = 9, - R16G16B16A16Float = 10, - R16G16B16A16UNorm = 11, - R16G16B16A16UInt = 12, - R16G16B16A16SNorm = 13, - R16G16B16A16SInt = 14, - R32G32Typeless = 15, - R32G32Float = 16, - R32G32UInt = 17, - R32G32SInt = 18, - R32G8X24Typeless = 19, - D32FloatS8X24UInt = 20, - R32FloatX8X24Typeless = 21, - X32TypelessG8X24UInt = 22, - R10G10B10A2Typeless = 23, - R10G10B10A2UNorm = 24, - R10G10B10A2UInt = 25, - R11G11B10Float = 26, - R8G8B8A8Typeless = 27, - R8G8B8A8UNorm = 28, - R8G8B8A8UNormSRGB = 29, - R8G8B8A8UInt = 30, - R8G8B8A8SNorm = 31, - R8G8B8A8SInt = 32, - R16G16Typeless = 33, - R16G16Float = 34, - R16G16UNorm = 35, - R16G16UInt = 36, - R16G16SNorm = 37, - R16G16SInt = 38, - R32Typeless = 39, - D32Float = 40, - R32Float = 41, - R32UInt = 42, - R32SInt = 43, - R24G8Typeless = 44, - D24UNormS8UInt = 45, - R24UNormX8Typeless = 46, - X24TypelessG8UInt = 47, - R8G8Typeless = 48, - R8G8UNorm = 49, - R8G8UInt = 50, - R8G8SNorm = 51, - R8G8SInt = 52, - R16Typeless = 53, - R16Float = 54, - D16UNorm = 55, - R16UNorm = 56, - R16UInt = 57, - R16SNorm = 58, - R16SInt = 59, - R8Typeless = 60, - R8UNorm = 61, - R8UInt = 62, - R8SNorm = 63, - R8SInt = 64, - A8UNorm = 65, - R1UNorm = 66, - R9G9B9E5SharedEXP = 67, - R8G8B8G8UNorm = 68, - G8R8G8B8UNorm = 69, - BC1Typeless = 70, - BC1UNorm = 71, - BC1UNormSRGB = 72, - BC2Typeless = 73, - BC2UNorm = 74, - BC2UNormSRGB = 75, - BC3Typeless = 76, - BC3UNorm = 77, - BC3UNormSRGB = 78, - BC4Typeless = 79, - BC4UNorm = 80, - BC4SNorm = 81, - BC5Typeless = 82, - BC5UNorm = 83, - BC5SNorm = 84, - B5G6R5UNorm = 85, - B5G5R5A1UNorm = 86, - B8G8R8A8UNorm = 87, - B8G8R8X8UNorm = 88, - R10G10B10XRBiasA2UNorm = 89, - B8G8R8A8Typeless = 90, - B8G8R8A8UNormSRGB = 91, - B8G8R8X8Typeless = 92, - B8G8R8X8UNormSRGB = 93, - BC6HTypeless = 94, - BC6HUF16 = 95, - BC6HSF16 = 96, - BC7Typeless = 97, - BC7UNorm = 98, - BC7UNormSRGB = 99, - AYUV = 100, - Y410 = 101, - Y416 = 102, - NV12 = 103, - P010 = 104, - P016 = 105, - F420Opaque = 106, - YUY2 = 107, - Y210 = 108, - Y216 = 109, - NV11 = 110, - AI44 = 111, - IA44 = 112, - P8 = 113, - A8P8 = 114, - B4G4R4A4UNorm = 115, - P208 = 130, - V208 = 131, - V408 = 132, - SamplerFeedbackMinMipOpaque, - SamplerFeedbackMipRegionUsedOpaque, - ForceUInt = 0xffffffff, - } - - public enum D3DResourceDimension : int - { - Unknown = 0, - Buffer = 1, - Texture1D = 2, - Texture2D = 3, - Texture3D = 4, - } - - [Flags] - public enum D3DResourceMiscFlags : uint - { - GenerateMips = 0x000001, - Shared = 0x000002, - TextureCube = 0x000004, - DrawIndirectArgs = 0x000010, - BufferAllowRawViews = 0x000020, - BufferStructured = 0x000040, - ResourceClamp = 0x000080, - SharedKeyedMutex = 0x000100, - GDICompatible = 0x000200, - SharedNTHandle = 0x000800, - RestrictedContent = 0x001000, - RestrictSharedResource = 0x002000, - RestrictSharedResourceDriver = 0x004000, - Guarded = 0x008000, - TilePool = 0x020000, - Tiled = 0x040000, - HWProtected = 0x080000, - SharedDisplayable, - SharedExclusiveWriter, - }; - - public enum D3DAlphaMode : int - { - Unknown = 0, - Straight = 1, - Premultiplied = 2, - Opaque = 3, - Custom = 4, - }; - - public DXGIFormat Format; - public D3DResourceDimension ResourceDimension; - public D3DResourceMiscFlags MiscFlags; - public uint ArraySize; - public D3DAlphaMode AlphaMode; -} - -public class DdsFile -{ - public const int DdsIdentifier = 0x20534444; - - public DdsHeader Header; - public DXT10Header? DXT10Header; - public byte[] MainSurfaceData; - public byte[] RemainingSurfaceData; - - private DdsFile( DdsHeader header, byte[] mainSurfaceData, byte[] remainingSurfaceData, DXT10Header? dXT10Header = null ) - { - Header = header; - DXT10Header = dXT10Header; - MainSurfaceData = mainSurfaceData; - RemainingSurfaceData = remainingSurfaceData; - } - - public static bool Load( Stream data, [NotNullWhen( true )] out DdsFile? file ) - { - file = null; - try - { - using var br = new BinaryReader( data ); - if( br.ReadUInt32() != DdsIdentifier ) - { - return false; - } - - var header = br.ReadStructure< DdsHeader >(); - var dxt10 = header.PixelFormat.FourCC == PixelFormat.FourCCType.DX10 ? ( DXT10Header? )br.ReadStructure< DXT10Header >() : null; - - file = new DdsFile( header, br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ), Array.Empty< byte >(), - dxt10 ); - return true; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load DDS file:\n{e}" ); - return false; - } - } - - public bool ConvertToTex( out byte[] texBytes ) - { - using var mem = new MemoryStream( MainSurfaceData.Length * 2 ); - using( var bw = new BinaryWriter( mem ) ) - { - var format = WriteTexHeader( bw ); - bw.Write( ConvertBytes( MainSurfaceData, format ) ); - } - - texBytes = mem.ToArray(); - return true; - } - - private TexFile.TextureFormat WriteTexHeader( BinaryWriter bw ) - { - var (format, mipLength) = ConvertFormat( Header.PixelFormat, Header.Height, Header.Width, DXT10Header ); - - bw.Write( ( uint )TexFile.Attribute.TextureType2D ); - bw.Write( ( uint )format ); - bw.Write( ( ushort )Header.Width ); - bw.Write( ( ushort )Header.Height ); - bw.Write( ( ushort )Header.Depth ); - bw.Write( ( ushort )Header.MipMapCount ); - bw.Write( 0 ); - bw.Write( 1 ); - bw.Write( 2 ); - - var offset = 80; - for( var i = 0; i < Header.MipMapCount; ++i ) - { - bw.Write( offset ); - offset += mipLength; - mipLength = Math.Max( 16, mipLength >> 2 ); - } - - for( var i = Header.MipMapCount; i < 13; ++i ) - { - bw.Write( 0 ); - } - - return format; - } - - private static byte[] ConvertBytes( byte[] ddsData, TexFile.TextureFormat format ) - { - return format switch - { - _ => ddsData, - }; - } - - private static (TexFile.TextureFormat, int) ConvertFormat( PixelFormat format, int height, int width, DXT10Header? dxt10 ) - => format.FourCC switch - { - PixelFormat.FourCCType.DXT1 => ( TexFile.TextureFormat.DXT1, height * width / 2 ), - PixelFormat.FourCCType.DXT3 => ( TexFile.TextureFormat.DXT3, height * width * 4 ), - PixelFormat.FourCCType.DXT5 => ( TexFile.TextureFormat.DXT5, height * width ), - PixelFormat.FourCCType.DX10 => dxt10!.Value.Format switch - { - Textures.DXT10Header.DXGIFormat.A8UNorm => ( TexFile.TextureFormat.A8, height * width ), - Textures.DXT10Header.DXGIFormat.R8G8B8A8UInt => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), - Textures.DXT10Header.DXGIFormat.R8G8UNorm => ( TexFile.TextureFormat.L8, height * width ), - Textures.DXT10Header.DXGIFormat.B8G8R8X8UNorm => ( TexFile.TextureFormat.R8G8B8X8, height * width * 4 ), - Textures.DXT10Header.DXGIFormat.B4G4R4A4UNorm => ( TexFile.TextureFormat.R4G4B4A4, height * width * 2 ), - Textures.DXT10Header.DXGIFormat.B5G5R5A1UNorm => ( TexFile.TextureFormat.R5G5B5A1, height * width * 2 ), - Textures.DXT10Header.DXGIFormat.R32Float => ( TexFile.TextureFormat.R32F, height * width * 4 ), - Textures.DXT10Header.DXGIFormat.R32G32B32A32Float => ( TexFile.TextureFormat.R32G32B32A32F, height * width * 16 ), - Textures.DXT10Header.DXGIFormat.R16G16Float => ( TexFile.TextureFormat.R16G16F, height * width * 4 ), - Textures.DXT10Header.DXGIFormat.R16G16B16A16Float => ( TexFile.TextureFormat.R16G16B16A16F, height * width * 8 ), - Textures.DXT10Header.DXGIFormat.D16UNorm => ( TexFile.TextureFormat.D16, height * width * 2 ), - Textures.DXT10Header.DXGIFormat.D24UNormS8UInt => ( TexFile.TextureFormat.D24S8, height * width * 4 ), - _ => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), - }, - _ => ( TexFile.TextureFormat.A8R8G8B8, height * width * 4 ), - }; -} - -public class TextureImporter -{ - private static void WriteHeader( byte[] target, int width, int height ) - { - using var mem = new MemoryStream( target ); - using var bw = new BinaryWriter( mem ); - bw.Write( ( uint )TexFile.Attribute.TextureType2D ); - bw.Write( ( uint )TexFile.TextureFormat.A8R8G8B8 ); - bw.Write( ( ushort )width ); - bw.Write( ( ushort )height ); - bw.Write( ( ushort )1 ); - bw.Write( ( ushort )1 ); - bw.Write( 0 ); - bw.Write( 1 ); - bw.Write( 2 ); - bw.Write( 80 ); - for( var i = 1; i < 13; ++i ) - { - bw.Write( 0 ); - } - } - - public static unsafe bool RgbaBytesToDds( byte[] rgba, int width, int height, out byte[] ddsData ) - { - var header = new DdsHeader() - { - Caps1 = DdsHeader.DdsCaps1.Complex | DdsHeader.DdsCaps1.Texture | DdsHeader.DdsCaps1.MipMap, - Depth = 1, - Flags = DdsHeader.DdsFlags.Required | DdsHeader.DdsFlags.Pitch | DdsHeader.DdsFlags.MipMapCount, - Height = height, - Width = width, - PixelFormat = new PixelFormat() - { - Flags = PixelFormat.FormatFlags.AlphaPixels | PixelFormat.FormatFlags.RGB, - FourCC = 0, - BBitMask = 0x000000FF, - GBitMask = 0x0000FF00, - RBitMask = 0x00FF0000, - ABitMask = 0xFF000000, - Size = 32, - RgbBitCount = 32, - }, - }; - ddsData = new byte[DdsHeader.Size + rgba.Length]; - header.Write( ddsData, 0 ); - rgba.CopyTo( ddsData, DdsHeader.Size ); - for( var i = 0; i < rgba.Length; i += 4 ) - { - ( ddsData[ DdsHeader.Size + i ], ddsData[ DdsHeader.Size + i + 2 ] ) - = ( ddsData[ DdsHeader.Size + i + 2 ], ddsData[ DdsHeader.Size + i ] ); - } - - return true; - } - - public static bool RgbaBytesToTex( byte[] rgba, int width, int height, out byte[] texData ) - { - texData = Array.Empty< byte >(); - if( rgba.Length != width * height * 4 ) - { - return false; - } - - texData = new byte[80 + width * height * 4]; - WriteHeader( texData, width, height ); - // RGBA to BGRA. - for( var i = 0; i < rgba.Length; i += 4 ) - { - texData[ 80 + i + 0 ] = rgba[ i + 2 ]; - texData[ 80 + i + 1 ] = rgba[ i + 1 ]; - texData[ 80 + i + 2 ] = rgba[ i + 0 ]; - texData[ 80 + i + 3 ] = rgba[ i + 3 ]; - } - - return true; - } - - public static bool PngToTex( string inputFile, out byte[] texData ) - { - using var file = File.OpenRead( inputFile ); - var image = Image.Load< Bgra32 >( file ); - - var buffer = new byte[80 + image.Height * image.Width * 4]; - WriteHeader( buffer, image.Width, image.Height ); - - var span = new Span< byte >( buffer, 80, buffer.Length - 80 ); - image.CopyPixelDataTo( span ); - - texData = buffer; - return true; - } - - public void Import( string inputFile ) - { } -} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index 9a5c8f16..6f0ec77f 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Numerics; using System.Reflection; +using System.Threading.Tasks; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Logging; @@ -13,7 +14,7 @@ using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; -using Penumbra.Import.Textures; +using Penumbra.Import.Dds; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; @@ -35,6 +36,10 @@ public partial class ModEditWindow private Matrix4x4 _multiplierLeft = Matrix4x4.Identity; private Matrix4x4 _multiplierRight = Matrix4x4.Identity; + private bool _invertLeft = false; + private bool _invertRight = false; + private int _offsetX = 0; + private int _offsetY = 0; private readonly FileDialogManager _dialogManager = new(); @@ -147,19 +152,7 @@ public partial class ModEditWindow return ( null, 0, 0 ); } - f.ConvertToTex( out var bytes ); - TexFile tex = new(); - tex.GetType().GetProperty( "Data", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) - ?.SetValue( tex, bytes ); - tex.GetType().GetProperty( "FileStream", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) - ?.SetValue( tex, new MemoryStream( tex.Data ) ); - tex.GetType().GetProperty( "Reader", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy ) - ?.SetValue( tex, new BinaryReader( tex.FileStream ) ); - tex.LoadFile(); - return ( tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height ); + return ( f.RgbaData.ToArray(), f.Header.Width, f.Header.Height ); } catch( Exception e ) { @@ -172,10 +165,26 @@ public partial class ModEditWindow { try { + if( fromDisk ) + { + var tmp = new TmpTexFile(); + using var stream = File.OpenRead( path ); + using var br = new BinaryReader( stream ); + tmp.Load(br); + return (tmp.RgbaData, tmp.Header.Width, tmp.Header.Height); + } + + var tex = fromDisk ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( path ) : Dalamud.GameData.GetFile< TexFile >( path ); - return tex == null - ? ( null, 0, 0 ) - : ( tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height ); + if( tex == null ) + { + return ( null, 0, 0 ); + } + + var rgba = tex.Header.Format == TexFile.TextureFormat.A8R8G8B8 + ? ImageParsing.DecodeUncompressedR8G8B8A8( tex.ImageData, tex.Header.Height, tex.Header.Width ) + : tex.GetRgbaImageData(); + return ( rgba, tex.Header.Width, tex.Header.Height ); } catch( Exception e ) { @@ -250,7 +259,7 @@ public partial class ModEditWindow UpdateCenter(); } - private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform ) + private static Vector4 CappedVector( IReadOnlyList< byte >? bytes, int offset, Matrix4x4 transform, bool invert ) { if( bytes == null ) { @@ -259,6 +268,11 @@ public partial class ModEditWindow var rgba = new Rgba32( bytes[ offset ], bytes[ offset + 1 ], bytes[ offset + 2 ], bytes[ offset + 3 ] ); var transformed = Vector4.Transform( rgba.ToVector4(), transform ); + if( invert ) + { + transformed = new Vector4( 1 - transformed.X, 1 - transformed.Y, 1 - transformed.Z, transformed.W ); + } + transformed.X = Math.Clamp( transformed.X, 0, 1 ); transformed.Y = Math.Clamp( transformed.Y, 0, 1 ); transformed.Z = Math.Clamp( transformed.Z, 0, 1 ); @@ -266,11 +280,32 @@ public partial class ModEditWindow return transformed; } + private Vector4 DataLeft( int offset ) + => CappedVector( _imageLeft, offset, _multiplierLeft, _invertLeft ); + + private Vector4 DataRight( int x, int y ) + { + if( _imageRight == null ) + { + return Vector4.Zero; + } + + x -= _offsetX; + y -= _offsetY; + if( x < 0 || x >= _wrapRight!.Width || y < 0 || y >= _wrapRight!.Height ) + { + return Vector4.Zero; + } + + var offset = ( y * _wrapRight!.Width + x ) * 4; + return CappedVector( _imageRight, offset, _multiplierRight, _invertRight ); + } + private void AddPixels( int width, int x, int y ) { - var offset = ( y * width + x ) * 4; - var left = CappedVector( _imageLeft, offset, _multiplierLeft ); - var right = CappedVector( _imageRight, offset, _multiplierRight ); + var offset = ( width * y + x ) * 4; + var left = DataLeft( offset ); + var right = DataRight( x, y ); var alpha = right.W + left.W * ( 1 - right.W ); if( alpha == 0 ) { @@ -287,14 +322,14 @@ public partial class ModEditWindow private void UpdateCenter() { - if( _imageLeft != null && _imageRight == null && _multiplierLeft.IsIdentity ) + if( _imageLeft != null && _imageRight == null && _multiplierLeft.IsIdentity && !_invertLeft ) { _imageCenter = _imageLeft; _wrapCenter = _wrapLeft; return; } - if( _imageLeft == null && _imageRight != null && _multiplierRight.IsIdentity ) + if( _imageLeft == null && _imageRight != null && _multiplierRight.IsIdentity && !_invertRight ) { _imageCenter = _imageRight; _wrapCenter = _wrapRight; @@ -308,22 +343,19 @@ public partial class ModEditWindow if( _imageLeft != null || _imageRight != null ) { - var (width, height) = _imageLeft != null ? ( _wrapLeft!.Width, _wrapLeft.Height ) : ( _wrapRight!.Width, _wrapRight.Height ); - if( _imageRight == null || _wrapRight!.Width == width && _wrapRight!.Height == height ) + var (totalWidth, totalHeight) = + _imageLeft != null ? ( _wrapLeft!.Width, _wrapLeft.Height ) : ( _wrapRight!.Width, _wrapRight.Height ); + _imageCenter = new byte[4 * totalWidth * totalHeight]; + + Parallel.For( 0, totalHeight - 1, ( y, _ ) => { - _imageCenter = new byte[4 * width * height]; - - for( var y = 0; y < height; ++y ) + for( var x = 0; x < totalWidth; ++x ) { - for( var x = 0; x < width; ++x ) - { - AddPixels( width, x, y ); - } + AddPixels( totalWidth, x, y ); } - - _wrapCenter = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _imageCenter, width, height, 4 ); - return; - } + } ); + _wrapCenter = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( _imageCenter, totalWidth, totalHeight, 4 ); + return; } _imageCenter = null; @@ -334,6 +366,7 @@ public partial class ModEditWindow { if( wrap != null ) { + ImGui.TextUnformatted( $"Image Dimensions: {wrap.Width} x {wrap.Height}" ); size = size with { Y = wrap.Height * size.X / wrap.Width }; ImGui.Image( wrap.ImGuiHandle, size ); } @@ -397,7 +430,7 @@ public partial class ModEditWindow { _dialogManager.Draw(); - using var tab = ImRaii.TabItem( "Texture Import/Export" ); + using var tab = ImRaii.TabItem( "Texture Import/Export (WIP)" ); if( !tab ) { return; @@ -412,7 +445,7 @@ public partial class ModEditWindow PathInputBox( "##ImageLeft", "Import Image...", string.Empty, 0 ); ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) ) + if( DrawMatrixInput( leftRightWidth.X, ref _multiplierLeft ) || ImGui.Checkbox( "Invert##Left", ref _invertLeft ) ) { UpdateCenter(); } @@ -465,7 +498,7 @@ public partial class ModEditWindow PathInputBox( "##ImageRight", "Import Image...", string.Empty, 1 ); ImGui.NewLine(); - if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) ) + if( DrawMatrixInput( leftRightWidth.X, ref _multiplierRight ) || ImGui.Checkbox( "Invert##Right", ref _invertRight ) ) { UpdateCenter(); } From 787c19a1700d5678aa79d559ef5ad2c6eb952504 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Jun 2022 17:03:40 +0200 Subject: [PATCH 20/22] Add mod root directory max length and warnings on non-ascii characters. --- Penumbra/UI/ConfigWindow.SettingsTab.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs index a7085b0f..de377896 100644 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; @@ -16,6 +17,7 @@ public partial class ConfigWindow { private partial class SettingsTab { + public const int RootDirectoryMaxLength = 64; private readonly ConfigWindow _window; public SettingsTab( ConfigWindow window ) @@ -65,11 +67,18 @@ public partial class ConfigWindow // Do not change the directory without explicitly pressing enter or this button. // Shows up only if the current input does not correspond to the current directory. - private static bool DrawPressEnterWarning( string old, float width ) + private static bool DrawPressEnterWarning( string newName, string old, float width, bool saved ) { - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); - var w = new Vector2( width, 0 ); - return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); + var w = new Vector2( width, 0 ); + var symbol = '\0'; + var (text, valid) = newName.Length > RootDirectoryMaxLength + ? ( $"Path is too long. The maximum length is {RootDirectoryMaxLength}.", false ) + : newName.Any( c => ( symbol = c ) > ( char )0x7F ) + ? ( $"Path contains invalid symbol {symbol}. Only ASCII is allowed.", false ) + : ( $"Press Enter or Click Here to Save (Current Directory: {old})", true ); + + return ( ImGui.Button( text, w ) || saved ) && valid; } // Draw a directory picker button that toggles the directory picker. @@ -128,7 +137,7 @@ public partial class ConfigWindow var spacing = 3 * ImGuiHelpers.GlobalScale; using var group = ImRaii.Group(); ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - _window._iconButtonSize.X ); - var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 64, ImGuiInputTextFlags.EnterReturnsTrue ); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) ); ImGui.SameLine(); DrawDirectoryPickerButton(); @@ -147,7 +156,7 @@ public partial class ConfigWindow if( Penumbra.Config.ModDirectory != _newModDirectory && _newModDirectory.Length != 0 - && ( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) ) ) + && DrawPressEnterWarning( _newModDirectory, Penumbra.Config.ModDirectory, pos, save ) ) { Penumbra.ModManager.DiscoverMods( _newModDirectory ); } From 0f2266963de88b6782ebd8c50efc018c8786d8c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Jun 2022 17:04:15 +0200 Subject: [PATCH 21/22] Add prototypes for advanced API. --- Penumbra/Api/IPenumbraApi.cs | 81 ++++++++++++++++++++++++++++++++++++ Penumbra/Api/PenumbraApi.cs | 41 ++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/Penumbra/Api/IPenumbraApi.cs b/Penumbra/Api/IPenumbraApi.cs index 9714b497..37265ce9 100644 --- a/Penumbra/Api/IPenumbraApi.cs +++ b/Penumbra/Api/IPenumbraApi.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.ComponentModel.Design; using Dalamud.Configuration; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Data; +using OtterGui.Classes; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Api; @@ -16,6 +21,22 @@ public interface IPenumbraApiBase public delegate void ChangedItemHover( object? item ); public delegate void ChangedItemClick( MouseButton button, object? item ); +public enum PenumbraApiEc +{ + Okay = 0, + NothingChanged = 1, + CollectionMissing = 2, + ModMissing = 3, + OptionGroupMissing = 4, + SettingMissing = 5, + + CharacterCollectionExists = 6, + LowerPriority = 7, + InvalidGamePath = 8, + FileMissing = 9, + InvalidManipulation = 10, +} + public interface IPenumbraApi : IPenumbraApiBase { // Obtain the currently set mod directory from the configuration. @@ -77,4 +98,64 @@ public interface IPenumbraApi : IPenumbraApiBase // Obtain a list of all installed mods. The first string is their directory name, the second string is their mod name. public IList< (string, string) > GetModList(); + + + // ############## Mod Settings ################# + + // Obtain the potential settings of a mod specified by its directory name first or mod name second. + // Returns null if the mod could not be found. + public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName ); + + // Obtain the enabled state, the priority, the settings of a mod specified by its directory name first or mod name second, + // and whether these settings are inherited, or null if the collection does not set them at all. + // If allowInheritance is false, only the collection itself will be checked. + public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName, + string modDirectory, string modName, bool allowInheritance ); + + // Try to set the inheritance state in the given collection of a mod specified by its directory name first or mod name second. + // Returns Okay, NothingChanged, CollectionMissing or ModMissing. + public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ); + + // Try to set the enabled state in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance. + // Returns Okay, NothingChanged, CollectionMissing or ModMissing. + public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ); + + // Try to set the priority in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance. + // Returns Okay, NothingChanged, CollectionMissing or ModMissing. + public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ); + + // Try to set a specific option group in the given collection of a mod specified by its directory name first or mod name second. Also removes inheritance. + // If the group is a Single Selection group, options should be a single string, otherwise the array of enabled options. + // Returns Okay, NothingChanged, CollectionMissing or ModMissing, OptionGroupMissing or SettingMissing. + // If any setting can not be found, it will not change anything. + public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ); + + public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, + string[] options ); + + + // Create a temporary collection without actual settings but with a cache. + // If character is non-zero and either no character collection for this character exists or forceOverwriteCharacter is true, + // associate this collection to a specific character. + // Can return Okay, CharacterCollectionExists or NothingChanged. + public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ); + + // Remove a temporary collection if it exists. + // Can return Okay or NothingChanged. + public PenumbraApiEc RemoveTemporaryCollection( string collectionName ); + + + // Set or remove a specific file redirection or meta manipulation under the name of Tag and with a given priority + // for a given collection, which may be temporary. + // Can return Okay, CollectionMissing, InvalidPath, FileMissing, LowerPriority, or NothingChanged. + public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority ); + + // Can return Okay, CollectionMissing, InvalidManipulation, LowerPriority, or NothingChanged. + public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority ); + + // Can return Okay, CollectionMissing, InvalidPath, or NothingChanged. + public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath ); + + // Can return Okay, CollectionMissing, InvalidManipulation, or NothingChanged. + public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 ); } \ No newline at end of file diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 082e9642..a81ab204 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.Mods; namespace Penumbra.Api; @@ -208,4 +209,44 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); return Penumbra.ModManager.Select( m => ( m.ModPath.Name, m.Name.Text ) ).ToArray(); } + + public Dictionary< string, (string[], SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) + => throw new NotImplementedException(); + + public (PenumbraApiEc, (bool, int, Dictionary< string, string[] >, bool)?) GetCurrentModSettings( string collectionName, string modDirectory, string modName, + bool allowInheritance ) + => throw new NotImplementedException(); + + public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ) + => throw new NotImplementedException(); + + public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ) + => throw new NotImplementedException(); + + public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ) + => throw new NotImplementedException(); + + public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ) + => throw new NotImplementedException(); + + public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string[] options ) + => throw new NotImplementedException(); + + public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ) + => throw new NotImplementedException(); + + public PenumbraApiEc RemoveTemporaryCollection( string collectionName ) + => throw new NotImplementedException(); + + public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority ) + => throw new NotImplementedException(); + + public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority ) + => throw new NotImplementedException(); + + public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath ) + => throw new NotImplementedException(); + + public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 ) + => throw new NotImplementedException(); } \ No newline at end of file From eff6c2e9af44a7eed180bb085f4b8250fc8b8d57 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 16 Jun 2022 15:07:02 +0000 Subject: [PATCH 22/22] [CI] Updating repo.json for refs/tags/0.5.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 07e12e72..3fca4ca9 100644 --- a/repo.json +++ b/repo.json @@ -4,8 +4,8 @@ "Name": "Penumbra", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "0.5.0.5", - "TestingAssemblyVersion": "0.5.0.5", + "AssemblyVersion": "0.5.1.0", + "TestingAssemblyVersion": "0.5.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 6, @@ -14,9 +14,9 @@ "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.0.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ]